Reputation: 7317
A class like:
class Extractor<T, K extends keyof T> {
constructor(private item: T, private key: K) { }
extract(): T[K] {
return this.item[this.key];
}
}
is unnatural to use/extend, because the type parameter K takes a string literal type.
I want the type parameters to be <T, R>
where R
is effectively what T[K]
previously computed to. However, TypeScript doesn't allow R
to refer to a constructor argument, and the type expression for the key
parameter is not allowed to refer to the parameter itself. Also, the constructor cannot have generic type parameters.
So where can I specify the constraint that R = T[typeof key]
?
Upvotes: 0
Views: 868
Reputation: 328473
I'm not exactly sure what you're trying to achieve, so I don't know which solution would work the best for you:
It is possible, using conditional types to calculate K
from the types T
and your desired R
, but it might create more headaches than it solves:
type ValueOf<T> = T[keyof T];
type KeyofMatching<T, R extends ValueOf<T>> =
ValueOf<{ [K in keyof T]: T[K] extends R ? K : never }>;
The KeyofMatching
type alias takes a type T
and one of its property value types R
, and returns all the keys K
which return that type. So T[KeyofMatching<T, R>]
is always R
. Unfortunately, TypeScript is not smart enough to realize that, so if you have a value of the former type and want to return it as a value of the latter type, you have to use an assertion. Here's a function to do that:
function asR<T, R extends ValueOf<T>>(x: T[KeyofMatching<T, R>]): R {
return x as any;
}
Now you can define your class:
class Extractor<T, R extends T[keyof T]> {
constructor(private item: T, private key: KeyofMatching<T, R>) { }
extract(): R {
return asR(this.item[this.key]); // note the asR()
}
}
This works as as far as it goes, but when you create a new Extractor
, the compiler will really not be able to infer the narrow value for R
you expect:
const extractorNotSoGood = new Extractor({a: "you", b: true}, "b");
// inferred as Extractor<{a: string, b: boolean}, string | boolean>;
If you want the narrowest possible R
, you will have to explicitly specify it:
const e = new Extractor<{a: string, b: boolean}, boolean>({a: "you", b: true}, "b");
So, that works, but has some downsides.
Another way to attack this is to give up on using the constructor, and instead use a static builder method. Furthermore, since TypeScript doesn't understand that this.item[this.key]
has type R
, we can sidestep this by storing not a key but a function that uses the key. Like this:
class Extractor<T, R extends T[keyof T]> {
constructor(private item: T, private extractorFunction: (x: T) => R) { }
extract(): R {
return this.extractorFunction(this.item);
}
static make<T, K extends keyof T>(item: T, key: K): Extractor<T, T[K]> {
return new Extractor(item, i => i[key]);
}
}
If you use make
instead of the constructor, you get the behavior and inference you're looking for, I think:
const e = Extractor.make({ a: "you", b: true }, "b");
// e is an Extractor<{a: string, b: boolean}, boolean>
So, this works, and avoids the problems of the earlier method, but maybe adds some problems of its own. 🤷♀️
Okay, hope one of those helps you make progress. Good luck!
Upvotes: 2