Issue
Looking for a way to typecheck possible values in an interface based on another property array type values:
export interface RootLayoutTab {
id: string;
title: string;
}
interface RootLayoutProps<T extends Array<RootLayoutTab>> {
tabs: T;
defaultTab: (T extends Array<infer U> ? U : never)['id'];
}
const tabs: RootLayoutTab[] = [
{ id: 'database', title: 'Database' },
{ id: 'gateways', title: 'Gateways' },
];
const props: RootLayoutProps<typeof tabs> = {
tabs,
defaultTab: 'database-test',
// expected typecheck error due to valid values 'database' and 'gateways', but currently passes typecheck
};
Solution
If you annotate the tabs
variable as RootLayoutTab[]
, then that's all the compiler will know about its type afterward. Since the id
property of RootLayoutTab
is just string
, then you've thrown away the information you need to restrict defaultTab
to "database" | "gateways"
. So as a first step, let's not annotate the variable:
const tabs = [
{ id: 'database', title: 'Database' },
{ id: 'gateways', title: 'Gateways' },
];
// const tabs: {id: string; title: string}[]
This still isn't going to work, because the type inferred for tabs
is equivalent to RootLayoutTab[]
. TypeScript has no idea that you care about the literal types of the id
properties, and so it infers string
(usually people want {a: "abc"}
to be inferred as type {a: string}
). The easiest way to tell TypeScript that you do care about such things is to use a const
assertion:
const tabs = [
{ id: 'database', title: 'Database' },
{ id: 'gateways', title: 'Gateways' },
] as const;
/* const tabs: readonly [{
readonly id: "database";
readonly title: "Database";
}, {
readonly id: "gateways";
readonly title: "Gateways";
}] */
The const
assertion says "I don't plan to make any changes at all to this value, so please infer the most specific type you can for it, including literal types". The inferred type here is very close to what you want, although TypeScript tends to infer readonly
arrays and your types apparently require mutable arrays. Let's loosen that restriction:
interface RootLayoutProps<T extends readonly RootLayoutTab[]> {
tabs: T;
defaultTab: T[number]['id'];
}
I widened RootLayoutTab[]
to readonly RootLayoutTab[]
(and, for good measure, changed your computation of defaultTab
to just index into T
with number
to get the element type, instead of using a more complex conditional type). Now we have enough for the rest of your code to function as desired:
const props: RootLayoutProps<typeof tabs> = {
tabs,
defaultTab: 'database-test', // error!
//~~~~~~~~ <-- Type '"database-test"' is not
//assignable to type '"database" | "gateways"'
};
That looks good.
Still, you presumably want to know that tabs
is a valid array of RootLayoutTab
elements right at its declaration and not only later. Right now if you make a mistake like
const tabs = [
{ id: 'database', titel: 'Database' }, // no error?!
{ id: 'gateways', title: 'Gateways' },
] as const;
there's no error inside tabs
, and you have to wait until you define props
to see the problem:
declare const props: RootLayoutProps<typeof tabs>; // error!
// --------------------------------> ~~~~~~~~~~~
// Property 'title' is missing in type '{ readonly id: "database"; readonly titel: "Database"; }'
// but required in type 'RootLayoutTab'.
Really you want to check tabs
against RootLayoutTab[]
without widening it to RootLayoutTab[]
. To do that you can use the satisfies
operator:
const tabs = [
{ id: 'database', titel: 'Database' }, // error!
// -------------> ~~~~~
// Object literal may only specify known properties, but 'titel'
// does not exist in type 'RootLayoutTab'. Did you mean to write 'title'?
{ id: 'gateways', title: 'Gateways' }, // error!
] as const satisfies RootLayoutTab[];
Now you get the error right on the source of the mistake, which makes it easier to fix:
const tabs = [
{ id: 'database', title: 'Database' },
{ id: 'gateways', title: 'Gateways' },
] as const satisfies RootLayoutTab[]; // okay
Looks good!
Note: the above works in TypeScript 5.3 due to a feature implemented in microsoft/TypeScript#55229 where [⋯] as const satisfies T[]
allows the const
-asserted array literal to be inferred as a mutable tuple instead of the readonly tuple normally produced. In TS5.2 and below, [⋯] as const satisfies T[]
would be an error, and you'd have to change it to [⋯] as const satisfies readonly T[]
. It's not really a big deal either way, but this is easier, since now we can leave your definition of RootLayoutProps
alone and still have it work:
interface RootLayoutProps<T extends RootLayoutTab[]> { ⋯ }
const tabs = [
{ id: 'database', title: 'Database' },
{ id: 'gateways', title: 'Gateways' },
] as const satisfies RootLayoutTab[]; // okay
const props: RootLayoutProps<typeof tabs> = {
tabs,
defaultTab: 'database-test', // error!
//~~~~~~~~ <-- Type '"database-test"' is not
//assignable to type '"database" | "gateways"'
};
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.