Search code examples
typescriptgenericstype-inference

Infer the return type of a function within a type and use the return type in the type itself


I'm writing some types that I will use for a table component in React and are having some issues.

I want to feed the data to the component, and then infer the data type for some different purposes. The problem arises when I introduce a row formatter, which will modify the data and return a new type.

I have greatly simplified the type to illustrate the issue:

type Props<TRow, TFormattedRow> = {
  data: TRow[]
  getRow?: (row: TFormattedRow) => void
  rowFormatter: (row: TRow) => TFormattedRow
}

function createProps<TRow, TFormattedRow>(props: Props<TRow, TFormattedRow>) {
  return props
}

When using the createProps function by explicitly giving it the type of row in rowFormatter everything is working correctly - rowFormatter infers formattedRow correctly as { id: string, name: string, count: number }

createProps({
  data: [
    { id: '1', name: 'Mangus Karlsson', count: 4 },
    { id: '2', name: 'Elsa Nyström', count: 6 },
    { id: '3', name: 'Bengt Blyertz', count: 8 },
  ],
  // getData is infers the formatted row type correctly as { id: string, name: string, count: number }
  getRow: (formattedRow) => console.log(formattedRow),
  rowFormatter: (row: { id: string, name: string, count: number }) => {
    return { id: row.id, idAsNumber: parseInt(row.id) }
  },
})

Though, when not explicitly setting the argument of rowFormatter, getRow infers formattedRow as unknown:

createProps({
  data: [
    { id: '1', name: 'Mangus Karlsson', count: 4 },
    { id: '2', name: 'Elsa Nyström', count: 6 },
    { id: '3', name: 'Bengt Blyertz', count: 8 },
  ],
  // formattedRow is infered as unknown when not explicitly typing the argument of rowFormatter
  getRow: (formattedRow) => console.log(formattedRow),
  rowFormatter: (row) => {
    return { id: row.id, idAsNumber: parseInt(row.id) }
  },
})

Is there a way to solve this without explicitly typing the argument of rowFormatter? Or am I missing something fundamental here?


Solution

  • It works if you put rowFormatter first. ie. the compiler infers the type from that property first.

    const test = createProps({
      data: [
        { id: '1', name: 'Mangus Karlsson', count: 4 },
        { id: '2', name: 'Elsa Nyström', count: 6 },
        { id: '3', name: 'Bengt Blyertz', count: 8 },
      ],
      rowFormatter: (row) => ({ id: row.id, idAsNumber: parseInt(row.id) }),
      getRow: (formattedRow) => console.log(formattedRow),
    })
    
    // inferred type:
    // const test: Props<{
    //     id: string;
    //     name: string;
    //     count: number;
    // }, {
    //     id: string;
    //     idAsNumber: number;
    // }>
    

    Typescript Playground


    But depending on your use case, you may be better off explicitly typing the function call

    type MyRow = {id: string, name: string, count: number};
    type MyFormattedRow = {id: string, idAsNumber: number};
    
    const test = createProps<MyRow, MyFormattedRow>({
      data: [
        { id: '1', name: 'Mangus Karlsson', count: 4 },
        { id: '2', name: 'Elsa Nyström', count: 6 },
        { id: '3', name: 'Bengt Blyertz', count: 8 },
      ],
      getRow: (formattedRow) => console.log(formattedRow),
      rowFormatter: (row) => ({ id: row.id, idAsNumber: parseInt(row.id) }),
    })
    
    // inferred type:
    // const test: Props<MyRow, MyFormattedRow>
    

    Typescript Playground

    I generally prefer explicitly typing things over relying on inference. I would also recommend always explicitly declaring return values of functions. I find it better to declare your intentions in order to prevent mistakes in the implementation.

    For example, let's say you forgot to parse idAsNumber and just did idAsNumber: row.id. Your variable now has the string type instead of number. Typescript won't catch that if you are relying on inference. Now let's say you do idAsNumber + 1. Well now you have a string with 1 at the end instead of an incremented id.