Stefan Kanev
Stefan Kanev

Reputation: 3040

Check that arguments of a function are the keys of a correct type in a type parameter

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

Answers (1)

jcalz
jcalz

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!

Playground link to code

Upvotes: 3

Related Questions