There are no built-in existential types in TypeScript, so you can't say "I don't care which key key
is but it needs to be some key of T
".
The only way to do this is to make Type
generic in both T
and K extends keyof T
, like this:
type Type<T, K extends keyof T> = {
key: K,
doStuff: (value: T[K]) => void
};
Then you can specify Test
like this:
const Test: Type<{ var1: string, var2: number }, "var1"> = {
key: 'var1',
doStuff: (value) => { } // value is inferred as string
}
This works but maybe you're unhappy about having to specify "var1"
manually in both the type and the key
property. Unfortunately you can't just specify T
and leave K
to be inferred, at least for now. Eventually there should be a way to have partial type argument inference, possibly as soon as August 2018, for TypeScript 3.1.
For now you can do a workaround involving currying, where a generic function returns a generic function. You specify one and leave the other to be inferred. Like this:
const typeFor = <T>() => <K extends keyof T>(type: Type<T, K>) => type;
// T is specified manually
const typeForVar1StringVar2Number = typeFor<{ var1: string, var2: number }>();
// K is inferred from the argument
const Test2 = typeForVar1StringVar2Number({
key: 'var1',
doStuff: (value) => { } // value is inferred as string
});
It's a bit more involved but it does save you from writing out 'var1'
for K
.
Okay, hope that helps. Good luck!
Edit: I see that you really do want existential types since you need an array of these things. One way to get existential-like types when you have a union of literals (like keyof T
if T
doesn't have a string index) is to use distributive conditional types:
type PossibleTypes<T> = keyof T extends infer K ?
K extends any ? Type<T, K> : never : never;
PossibleTypes<T>
becomes a union of all Type<T, K>
for every K
in keyof T
. Let's use it to make that array:
type ArrayOfPossibleTypes<T> = Array<PossibleTypes<T>>
const asArrayOfPossibleTypes = <T>(arr: ArrayOfPossibleTypes<T>) => arr;
const testArray = asArrayOfPossibleTypes<{ var1: string, var2: number }>([
{
key: 'var1', doStuff(value) { /* value is string */ }
}, {
key: 'var2', doStuff(value) { /* value is number */ }
}
]);
The inference looks good there. If you're not too afraid of distributive conditional types I'd say that could work for you.
If all else fails, there is an implementation of existential types in TypeScript, but it involves continuation passing which is probably too much for your use case. I'll include an example for you for completeness:
type ExistentialType<T> = <R>(f: <K extends keyof T>(x: Type<T, K>) => R) => R;
Instead of saying "I'm a Type<T, K>
for some K
", it's saying "If you give me a function which operates on any Type<T, K>
, I can call that function for you and give you the result". Let's create one:
const exType: ExistentialType<{ var1: string, var2: number }> =
(f) => f({ key: 'var1', doStuff(value) { } });
And then use it:
const obj = {var1: "hey", var2: 123};
exType(function (t) { if (t.doStuff) t.doStuff(obj[t.key]) });
The inside-out nature of this is wacky and fun, and occasionally useful... but probably overkill here.
Okay, hope that helps. Good luck again.