Reputation: 3040
I'm trying to build a nice, Rails-like form builder in React with TypeScript. I'm fascinated by the possible compile compile-time checks, and I'm keen to see if there is a certain one I can implement. I'm not sure how to phrase this question abstractly, but I think a small example will illustrate it better anyway.
Let's say I have a generic Input
type that is an object with some properties. I'd like to have a function, numericField
, that takes an input object and key K
where Input[K]
is of type number
. If I pass a mistyped key or a key of type string, I'd like to get a compiler error.
For example:
interface Person {
name: string
age: number
}
I'd like to get the following:
decimalField<Person>({input: person, key: 'age'}) // works
decimalField<Person>({input: person, key: 'agge'}) // compiler error
decimalField<Person>({input: person, key: 'name'}) // compiler error
I've managed to do so with the following type:
export type PropertiesOfSubtype<T, P> = {
[K in keyof T]-?: Exclude<T[K], undefined | null> extends P ? K : never
}[keyof T]
If I define decimalField
as:
function decimalField<Input>(props: {input: Input, key: PropertiesOfType<Input, number>})
..it kinda works. But there is an issue.
I'd like typescript to know that input[key]
returns a number. Currently it doesn't. I think there might be a way to rewrite it where it can typecheck as TS knows it will return a string.
I guess my question is: is there a better way to do this?
P.P.: Here's a playground with the example, with what my next step is – an optional/required argument depending if there's a predefined label or on for the field.
Upvotes: 3
Views: 472
Reputation: 329523
When you say you want input[key]
to be understood to be a number
, you mean inside the implementation of decimalField()
, right? Inside the implementation, Input
is an unspecifed generic type parameter. When types depend on unspecified generic type parameters, the compiler will have a better time understanding that input[key]
is of type number
if input
's type is explicitly constrained to Record<typeof key, number>
. Theoretically the compiler could figure that out itself, but in practice it doesn't perform such higher order analysis with unspecified generics.
You could possibly write it this way:
type PropertyHaver<T, P> = { [K in PropertiesOfSubtype<T, P>]: P };
And then decimalField
's generic type parameter can be constrained like this:
function decimalField<Input extends PropertyHaver<Input, number>>(
props: { input: Input, key: PropertiesOfSubtype<Input, number> }) {
const value: number = props.input[props.key]
return "whatever"
}
The implementation understand that input[key]
is a number
now. (Note, it's really props.input[props.number]
.)
Okay, hope that helps; good luck!
Upvotes: 3