Your definition of NumberKeys<T>
faces a major issue due to the use of a homomorphic mapped type. This mapped type retains information about whether a property is optional, leading to the inclusion of undefined
when dealing with optional properties in your code:
type Ex = NumberKeys<{ a: number, b?: string }>;
// type Ex = "a" | undefined
This presence of undefined
causes issues as the compiler complains that indexing with K
can't be done on MyObject
due to the potential for it to be undefined
. To resolve this, you need to eliminate undefined
. One approach is to utilize the mapping modifier -?
to make all properties required in the result:
type NumberKeys<T> = {
[K in keyof T]-?: T[K] extends number ? K : never
}[keyof T];
type Ex = NumberKeys<{ a: number, b?: string }>;
// type Ex = "a"
With this adjustment, your error is resolved:
function process<K extends NumberKeys<MyObject>>(obj: MyObject, key: K): number {
const value = obj[key]; // okay
return value !== undefined ? value * 2 : 0;
}
However, you still aren't achieving the desired behavior:
type NKMO = NumberKeys<MyObject>;
// type NKMO = "mandatoryNumber"
The removal of undefined
doesn't address the absence of optionalNumber
. This stems from the fact that -?
only eliminates undefined
from the result of the mapping and not the original property. In TypeScript, this behavior is labeled as a bug which might never get fixed. To rectify this, consider checking for number | undefined
instead of just number
:
type NumberKeys<T> = {
[K in keyof T]-?: T[K] extends number | undefined ? K : never
}[keyof T];
type NKMO = NumberKeys<MyObject>;
// type NKMO = "mandatoryNumber" | "optionalNumber"
This solution works as expected.
An alternative approach is to work with Required<MyObject>
instead of MyObject
, utilizing the Required
utility type (which internally uses -?
):
type NKMO = NumberKeys<Required<MyObject>>;
// type NKMO = "mandatoryNumber" | "optionalNumber"
function process<K extends NumberKeys<Required<MyObject>>>(
obj: MyObject, key: K): number {
const value = obj[key]; // okay
return value !== undefined ? value * 2 : 0;
}
Both approaches deal with the issue of optional properties introducing undefined
, just in slightly different manners.
Playground link to code