0

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?

2 Answers 2

3

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.

Playground

Sign up to request clarification or add additional context in comments.

6 Comments

This is a better answer than mine. Less generics are better than more, typically.
Although the rules for when it merges and when constrains seems little more nuanced: tsplay.dev/NDyDVm
@AlexWayne interesting. I wonder what the logic is here
Should I be getting rid of any of the readonly or as const?
if you get rid of readonly, you will lose the ability the pass any premade arrays (created with as const to the function. Since non-readonly arrays can still be passed as to the function even if arguments are marked as readonly, I would say it's good to keep the readonly.
|
2

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

See Playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.