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?
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;
// }>
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>
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.