Home > Net >  Typescript : Generic type "extract keys with value of type X" does not behave as expected
Typescript : Generic type "extract keys with value of type X" does not behave as expected

Time:12-28

I have defined the following generic type which extracts from a type T the string keys whose value is a number :

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
    T[K] extends number ?
      K
      : never
    : never
}[keyof T];

I try to use this type in a generic function as following :

function setNumberField<T>(item: T, field: StringKeysMatchingNumber<T>): void {
  item[field] = 1;
}

But the line item[field] = 1; errors with Type 'number' is not assignable to type 'T[StringKeysMatchingNumber<T>]'.

I have tried a few different things such as narrowing the generic type T in the function to a type which explicitly contains some string keys with value number but this didn't help.

Anyone can see what the problem is ? Here is a TS playground with the code sample and some more details : https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwbmgXigbwLACgpQEMAuKAOwFcBbAIwgCcAaXfagfhIpvqbygGMSiOgEtSAcx74AJhyq06uAL65cAelVQAgvCgQAHpD7AIU2AmABpCCB3oARATtQAPlDtS7uUJDOIrNqHQAZWARcX94AFkCYD4AC1ExADk5egAeOERkSAA FRxvaBCwsQjo2ITxFK46DJzAzGYoAG0LKFEoAGtrAHsAM1gAXQBadig2-WNSKR0hRKhWJvwYVsHdPSmZslS6BaX8cf38DggAN3p9k-OFHEVm7pB oYBufL7yUiNhHtIoeAhgNV5AAxYQQAA2UjqAAphMZKCQYAwoH0wZCSMVEmUYvFEkD0jAcgBKEinHrCUzYXhwiCUZqoiFSNboACMr1uQA

CodePudding user response:

T in setNumberField is a black box. Nobody knows, even you, whether T has key with numeric value or not. There is not appropriate constraint. setNumberField allows you to provide even primitive value as a first argument. It means that inside function body, TS is unaware that item[field] is always a numerical value. However, TS is aware about it during function call. So function has two levels of typeings. One - is the function definition, when TS is unable to gues the T type and second one - during function call, when TS is aware about T type and able to infer it.

The easiest way to do it is to avoid mutation. You can return new object. Consider this example:

type TestType = {
  a: number,
  b?: number,
  c: string,
  d: number
}

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
  T[K] extends number ?
  K
  : never
  : never
}[keyof T];

const setNumberField = <
  Item,
  Field extends StringKeysMatchingNumber<Item>
>(item: Item, field: Field): Item => ({
  ...item,
  [field]: 1
})

declare let foo: TestType

// {
//     a: number;
//     b: string;
// }
const result = setNumberField({ a: 42, b: 'str' }, 'a')

Playground

Please keep in mind, TypeScript does not like mutations. See my article


If you still want mutate your argument, you should overload your function.

type TestType = {
  a: number,
  b?: number,
  c: string,
  d: number
}

type StringKeysMatchingNumber<T> = {
  [K in keyof T]-?: K extends string ?
  T[K] extends number ?
  K
  : never
  : never
}[keyof T];

function setNumberField<Item, Field extends StringKeysMatchingNumber<Item>>(item: Item, field: Field): void;
function setNumberField(item: Record<string, number>, field: string): void {
  item[field] = 2
}

declare let foo: TestType

const result1 = setNumberField({ a: 42, b: 'str' }, 'a') // ok
const result2 = setNumberField({ a: 42, b: 'str' }, 'b') // expected error

Playground

Function overloading is not so strict. As you might have noticed, this function type definition function setNumberField(item: Record<string, number>, field: string) allows you to use only object where all values are numbers. But this is not the case. This is why I have overloaded this function with another one layer. The bottom one is used for function body. The top one, with StringKeysMatchingNumber controls function arguments.

  • Related