Home > Blockchain >  Typescript generics keyof doesn't match type
Typescript generics keyof doesn't match type

Time:12-16

I have this interface, that just stores a key of another interface (modelKey) and the value of that key (value):

interface ValueHolder<T, H extends keyof T> {
  modelKey: H;
  value: T[H];
}

Now I want to store the horsePower from the following model, with the matching type in a ValueHolder:

interface Car {
  id: number;
  horsePower?: number;
  date: Date;
};

This looks like this:

const test: ValueHolder<Car, keyof Car> = {
  modelKey: 'horsePower',
  value: 1000,
};

At this point no error happens and it would store the value fine. But you could also pass a value of type Date:

const test: ValueHolder<Car, keyof Car> = {
  modelKey: 'horsePower',
  value: new Date(),
};

Because for whatever reason the value can accept all types of any key in the model provided:

(property) ValueHolder<Car, keyof Car>.value: string | number | Date | undefined


How can I make the value key of the interface ValueHolder only accepts values of type undefined | number, if you provide the modelKey horsePower?

Demo

CodePudding user response:

ValueHolder second generic argument H allows all keys which in turn leads to allowing all values.

You need slightly modify your main utility type to make illegal state unrepresentable.

Consider this example:

type Values<T> = T[keyof T]

type ValueHolder<T> = Values<{
  [Prop in keyof T]: {
    modelKey: Prop;
    value: T[Prop]
  }
}>

// type Test = {
//     modelKey: "id";
//     value: number;
// } | {
//     modelKey: "horsePower";
//     value: number | undefined;
// } | {
//     modelKey: "date";
//     value: Date;
// } | undefined
type Test = ValueHolder<Car>

interface Car {
  id: number;
  horsePower?: number;
  date: Date;
};

// ok
const test: ValueHolder<Car> = {
  modelKey: 'horsePower',
  value: 1000,
};

// error 
const test2: ValueHolder<Car> = {
  modelKey: 'horsePower',
  value: new Date(),
};

Playground

See type Test. ValueHolder creates a union of all allowed values. It iterates through each key and creates this interface {Prop:{modelKey:P, value:T[P]}}. Then Values obtains each object value {modelKey:P, value:T[P]} and makes a union of them.

UPDATE

Thanks, works fine! What if I want that T[Prop] can only be of the type string? Is that also possible?

Yes it is. There is two ways to achieve it. You can allow ValueHolder only to receive objects where values are strings.

type ValueHolder<T extends Record<string, string>> = Values<{
  [Prop in keyof T]: {
    modelKey: Prop;
    value: T[Prop]
  }
}>

Or, you can check inside iteration whether T[Prop] is string or not.

type ValueHolder<T> = Values<{
  [Prop in keyof T]: T[Prop] extends string ? {
    modelKey: Prop;
    value: T[Prop]
  } : never
}>
  • Related