Reputation: 6104
I think I have a simple question, but I also am not sure that this is possible in TypeScript.
Essentially I want to define a tuple type which has two elements and the second element depends on the value of the first.
As an example of this, I want to make a type where the first tuple element is a key of an interface, and the second tuple element is then tied to the type of that property. For example:
interface ExampleI {
a: number;
b: string;
}
const one: KeyedTuple<ExampleI> = ["a", 34]; // good
const two: KeyedTuple<ExampleI> = ["a", "not a number"]; // bad
const three: KeyedTuple<ExampleI> = ["b", 47]; // bad
I tried to do the following:
type KeyedTuple<T, K extends keyof T> = [K, T[K]];
This almost works, but the compiler only considers the type of K
, not the value of K
, so the second element always has type number | string
.
Is this possible? If so, how?
Upvotes: 2
Views: 478
Reputation: 327994
Conceptually I think you want KeyedTuple<T>
to be a union of [K, T[K]]
tuples for all K
in keyof T
. This can be achieved with mapped and lookup types, like this:
type KeyedTuple<T> = { [K in keyof T]: [K, T[K]] }[keyof T];
Let's test it:
interface ExampleI {
a: number;
b: string;
}
type KeyedTupleExampleI = KeyedTuple<ExampleI>;
// type KeyedTupleExampleI = ["a", number] | ["b", string]
It gives you exactly the behavior you were asking for:
const one: KeyedTuple<ExampleI> = ["a", 34]; // okay
const two: KeyedTuple<ExampleI> = ["a", "not a number"]; // error
const three: KeyedTuple<ExampleI> = ["b", 47]; // error
Furthermore, since assignments act as type guards on union types, the compiler will remember which key/value pair a variable is:
one[1].toFixed(); // okay, remembers one[1] is a number
Hope that helps; good luck!
Upvotes: 3
Reputation: 6531
const keyedTuple = <T, K extends keyof T>(obj: T, key: K): [T, T[K]] => {
return [obj, obj[key]]
}
interface IPerson {
name: string;
age: number
}
declare const Person: IPerson
const test = keyedTuple(Person, "name") // [Person, string]
Is one way to achieve this, i'm more in favor of having a function to achieve this than remembering to write out a colon or "as" cast to the correct type.
Your code wont work unless the key is known it can't be inferred from a variable but can be inferred from a function.
IE Your code would have to change to something like ObjKeyed<OBJ, KEY> = [Obj, Key]
EDIT: With types:
type KeyedTuple<T, K extends keyof T> = [K, T[K]];
interface ExampleI {
a: number;
b: string;
}
const one: KeyedTuple<ExampleI, "a"> = ["a", 34]; // good
const two: KeyedTuple<ExampleI, "a"> = ["a", "not a number"]; // bad
const three: KeyedTuple<ExampleI, "b"> = ["b", 47]; // bad
Upvotes: 0