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')
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
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.