I have some scenarios where I have an object that wields strings, numbers and booleans, and a getter that returns a value with correct type, eg something like this:
interface AllowedMapTypings {
'str': string;
'lon': number;
'str2': string;
}
const obj: AllowedMapTypings = {
'str': 'foo',
'lon': 123,
'str2': 'foo'
};
function foo<T extends keyof AllowedMapTypings>(key: T): AllowedMapTypings[T] {
return obj[key];
}
let str = foo('str'); // correctly inferred type 'string'
However, if I use the inferred interface type as an argument, it doesn't work:
function fn<T extends keyof AllowedMapTypings>(key: string, kind: T, value: AllowedMapTypings[T]) {
if (kind === 'str') {
console.log(value.length); // Property 'length' does not exist on type 'AllowedMapTypings[T]'.
}
}
It looks like the condition kind === 'str'
doesn't do it's job as a type guard correctly. Am I missing something, or is this a missing feature/bug in TS?
This is a known limitation, see microsoft/TypeScript#13995 and microsoft/TypeScript#24085 . The kind of control-flow narrowing that happens when you do if (kind === 'str') {}
does not act on generic type parameters or values of such types. I think one could argue that if (kind === 'str') {}
should narrow the type of kind
from T
to T & 'str'
, but even if the compiler did that for you it would not narrow the type of value
. Even though you know the type of kind
is correlated with that of value
, the compiler doesn't.
You can always get around this with liberal uses of type assertions . If you want a bit more type safety you can use a workaround that widens the values of generic types to concrete unions, and packs correlated values into a single variable which can be narrowed the way you expect. For example:
type KindValuePair<K extends keyof AllowedMapTypings = keyof AllowedMapTypings> =
K extends any ? [K, AllowedMapTypings[K]] : never;
The type KindValuePair
expands to ["str", string] | ["lon", number] | ["str2", string]
["str", string] | ["lon", number] | ["str2", string]
["str", string] | ["lon", number] | ["str2", string]
, which is the union of things you actually want to allow as kind
and value
. (I could have just manually set KindValuePair
to that union, but instead I'm using a distributive conditional type to have the compiler figure it out for me.)
Then you can do this:
function fn<T extends keyof AllowedMapTypings>(key: string, kind: T, value: AllowedMapTypings[T]) {
const kindValuePair = [kind, value] as KindValuePair; // assertion here
if (kindValuePair[0] === 'str') {
console.log(kindValuePair[1].length); // okay
}
}
You assert that [kind, value]
is of type KindValuePair
, and then you can use control flow narrowing on kindValuePair
to maintain the relationship you expect to see between its elements after you check against 'str'
. If this works for you, you can even make the function concrete and not generic using rest parameters :
function fn(key: string, ...kindValuePair: KindValuePair) {
if (kindValuePair[0] === 'str') {
console.log(kindValuePair[1].length); // okay
}
}
This completely avoids assertions and is as type-safe as I can imagine making. It also has the side effect of outlawing calls like this:
fn("", Math.random() < 0.5 ? 'str' : 'lon', 1); // error
which are allowed in the generic version (it specifies T
as 'str' | 'lon'
).
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.