Ian
Ian

Reputation: 6104

Tuple where a succeeding element's type is dependent on the value of a preceding element

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

Answers (2)

jcalz
jcalz

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!

Link to code

Upvotes: 3

Shanon Jackson
Shanon Jackson

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

Related Questions