I believe I've managed to use generics so that the DataPresentation
component's searchKey
prop can only be one of the values of field
in columns
type Props<RowType extends AnyObject, ColType extends string> = {
columns: ReadonlyArray<{ readonly field: ColType }>;
rows: RowType[];
searchKey?: ColType;
};
const DataPresentation = <RowType extends AnyObject, ColType extends string>({
columns, rows, searchKey,
}: Props<RowType, ColType>) => { ... }
Here I am using the component:
export const playerPropsColumns = [
{ field: 'sportId' }, // todo: get sport name. from backend?
{ field: 'playerName' },
{ field: 'line' },
{ field: 'status' },
] as const;
// inside a component
<DataPresentation
columns={playerPropsColumns}
rows={data}
searchKey={'XXXXX'}
/>
That invalid searchKey passes the type check, but it's more interesting than simply "it's not working." When I hover over searchKey
, its type is:
searchKey?: "line" | "sportId" | "playerName" | "status" | "XXXXX" | undefined
and playerPropsColumns
's type is
columns: readonly {readonly field: "line" | "sportId" | "playerName" | "status" | "XXXXX"}[]
So it's getting the acceptable values from the playerPropsColumns
array, but it's also accepting whatever I feed into searchKey
which obviously completely defeats the point of type checking.
What's going on here and how do I fix it?
Note: I will say that all the read-only stuff is so I could use as const
in my definition of the columns, so that I could do type FieldType = typeof playerPropsColumns[number]['field'];
. However, after setting all this up and using generics I realize I of course didn't need that typedef at all, so maybe I can get rid of the read only stuff, and maybe they're the problem?
CodePudding user response:
What you are observing here is intended behaviour. The readonly
modifiers are not the problem here. When you use a generic type for multiple parameters, the types of those parameters will join together in a union.
function test<T extends string>(a: T, b: T) {}
test("a", "b")
// function test<"a" | "b">(a: "a" | "b", b: "a" | "b"): void
So how can you solve this problem?
Instead of using T
to store a union of possible values, you could use T
to store the whole tuple.
type Props<ColTypes extends readonly { field: string }[]> = {
columns: ColTypes;
searchKey?: ColTypes[number];
};
const DataPresentation = <
ColTypes extends readonly { field: string }[]
>({
columns, searchKey,
}: Props<ColTypes>) => {}
We can now use ColTypes[number]["field"]
to constain the type of the field searchKey
.
CodePudding user response:
You can make SearchKey
a separate generic parameter which extends ColType
so Typescript knows which is to be inferred as an input type, and which is a constraint.
For example:
(Note I removed rows
since it has nothing to do with the actual issue here)
type Props<
ColType extends string,
SearchKey extends ColType
> = {
columns: ReadonlyArray<{ readonly field: ColType }>;
searchKey?: SearchKey;
};
const DataPresentation = <
ColType extends string,
SearchKey extends ColType
>({
columns,
searchKey,
}: Props<ColType, SearchKey>) => {
return <></>
}
Now the example works as you would expect:
export const playerPropsColumns = [
{ field: 'sportId' },
{ field: 'playerName' },
{ field: 'line' },
{ field: 'status' },
] as const;
const testA = <DataPresentation
columns={playerPropsColumns}
searchKey='line'
/> // fine
const testB = <DataPresentation
columns={playerPropsColumns}
searchKey='XXXXX'
/> // error