Issue
One thing I'm really struggling to understand is how to write a function that takes a key of a particular interface, and returns the value with the correct type. Something like the following:
interface Transformers {
boolean: (value: string) => boolean;
string: (value: string) => string;
}
const transformers: Transformers = {
boolean: (value) => Boolean(value),
string: (value) => value,
}
function transformValue<T extends keyof Transformers>(value: string, type: T): ReturnType<Transformers[T]> {
return transformers[type](value);
}
Here, I would expect that if I pass in transformValue('lorem', 'boolean')
I would get a boolean back, and if I pass in transformValue('lorem', 'string')
I would get a string. But I'm getting the error Type 'string | boolean' is not assignable to type 'ReturnType<Transformers[T]>'
and I can't work out why.
Here is a TS Playground link if it helps.
Solution
Unfortunately the type checker isn't smart enough to understand the abstract relationship between generic T
and ReturnType<Transformers[T]>
. The ReturnType
utility type is implemented as a conditional type, and TypeScript is notoriously unable to do much analysis on generic conditional types. It tends to defer evaluation of such types, leaving them as opaque black boxes.
The recommended refactoring is described at microsoft/TypeScript#47109. Instead of using conditional types, we write a "base" mapping interface, and then express all our operations as generic indexes into that type, or into mapped types over that type. In your example that looks like this:
interface TypeMap {
boolean: boolean;
string: string;
}
type Transformers =
{ [K in keyof TypeMap]: (value: string) => TypeMap[K] }
const transformers: Transformers = {
boolean: (value) => Boolean(value),
string: (value) => value,
}
function transformValue<K extends keyof Transformers>(value: string, type: K): TypeMap[K] {
return transformers[type](value); // okay
}
That works because inside transformValue
, the type of transformers[type]
can be readily interpreted as a single function of type (value: string) => TypeMap[K]
. Note that this interpretation only happens because Transformers
is itself a mapped type over TypeMap
. If you left your original definition, the type would be equivalent, but the compiler would have no idea how to maintain the relationship between transformers[type]
and K
, and you'd end up with a useless union of functions like ((value: string) => boolean) | ((value: string) => string)
.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.