I’m fairly new to generic/typescrypt.
Context: Im trying to pass data from a page (page.tsx) to a generic component (header.tsx). The data is fetched from a react-query function.
Issue: In the header.tsx, the Checkbox component, Typescrypt is saying:
Property every does not exist on type T
Parameter 'record' implicitly has an 'any' type
And I am quite confused as to why that is the case when the data I’m passing is guaranteed to be an array and exists.
What I thought: TypeScript also notifies that record has an any type, but my thought process was that since the component doesn’t recognize the type of the data
prop, if I was able to somehow get the type for the data
prop in headers.tsx, TypeScript can then infer it.
I have attached a link to the playground.
export type ProductRow = {
id: string;
name?: string;
sku?: string;
};
export const page = () => {
//data fetched from react-query useQuery
const data = [
{
brand: {
id: 'clywipo320000cayabq4iub7e',
name: 'Nero',
minMargin: 30,
maxMargin: 40,
},
id: '1',
name: "100mm Round Tile Insert Floor Waste 50mm Outlet Brushed Gold",
priceRecords: [
{
price: 120,
date: 'Fri Aug 23 2024 10:00:00 GMT+1000 (Australian Eastern Standard Time)'
}
],
sku: 'NRFW003GD'
},
{
brand: {
id: 'clywipo320000cayabq4iub7e',
name: 'Nero',
minMargin: 30,
maxMargin: 40,
},
id: '2',
name: "100mm Round Tile Insert Floor Waste 50mm Outlet Brushed Bronze",
priceRecords: [
{
price: 112,
date: 'Fri Aug 23 2024 10:00:00 GMT+1000 (Australian Eastern Standard Time)'
}
],
sku: 'NRFW003BZ'
}
]
const [selectedRows, setSelectedRows] = useState<ProductRow[]>([]);
return (
<div>
<Header data={data} selectedRows={selectedRows} />
{ data.map(record =>
<input
type={`checkbox`}
checked={selectedRows.map(row => row.id).includes(record.id)}
onClick={() => setSelectedRows(prevValue => [...prevValue, { id: record.id, name: record.name, sku: record.sku }])} />
)}
</div>
)
}
type HeaderProps<T> = {
data: T,
selectedRows: ProductRow[]
};
export const Header = <T,>({ data, selectedRows }: HeaderProps<T>) => {
return (
<div className={``}>
<div className={`flex gap-3 items-start`}>
<input
type={`checkbox`}
checked={data.every(record => selectedRows.map(row => row.id).includes(record.id))}
/>
<p>SKU</p>
</div>
<p className={`col-span-3`}>Product Name</p>
<p>Brand</p>
<p className={`text-center`}>RRP</p>
<p className={`text-center`}>Max Discount (%)</p>
<p className={`text-center`}>Buildmat Price</p>
<p className={`text-center`}>TheBlueSpace Price</p>
</div>
)
};
This particular concept was discussed in the handbook under Generic Constraints. [typescriptlang.org]
Basically, if want to write a generic function that works on a set of types where you have some knowledge about what capabilities that set of types will have, it is completely normal. The only issue with this is that the compiler cannot prove that every type has a given property, so it must warn you that you can’t make this assumption which seems reasonable because in your case, what if typeof data.priceRecords
doesn’t return a type that has a property every()
?
The concept of generic constraints is basically the idea that we should constrain our generic procedure to work with any type T
but for any property that must be used on T
, that property must be assured to exist.
interface EveryWise {
every(callbackfn: (value, index: number, array) => boolean, thisArg?: any): boolean;
}
export const Header = <T extends EveryWise>({ data, selectedRows }: HeaderProps<T>) => { /* •••Rest of code••• */ }
Example of Generic Constraint. [1]
[1] The interface EveryWise
should be constrained even more with a type U
representing the type of value
but since the type of value
is left out, this is the strictest we can go. The reason for this mention is that record
is of type any
hence record.id
is used without type safety.