5

I have following type:

type Example = {
  key1: number,
  key2: string
}

and I need to create type based on Example type to be one of key: value pair. Of course I'm looking for generic solution. Here is what this type should return:

type Example2 = { ... };
const a: Example2 = { key3: 'a' } // incorrect
const b: Example2 = { key1: 'a' } // incorrect
const c: Example2 = { key2: 1 } // incorrect
const d: Example2 = { key1: 1 } // correct
const e: Example2 = { key2: 'a' } // correct
const f: Example2 = { key1: 1, key2: 'a' } // incorrect

I was trying using this:

type GetOne<T> = { [P in keyof T]: T[P] };
type Example2 = GetOne<Example>;

but it returns all the properties and example with const f not working as expected.

ichi
  • 171
  • 1
  • 8

2 Answers2

7

We will generate a union of all possibilities:

type Example = {
   key1: number,
   key2: string
}

type PickOne<T> = { [P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>> }[keyof T]
type Example2 = PickOne<Example>;
const a: Example2 = { key3: 'a' } // incorrect
const b: Example2 = { key1: 'a' } // incorrect
const c: Example2 = { key2: 1 } // incorrect
const d: Example2 = { key1: 1 } // correct
const e: Example2 = { key2: 'a' } // correct
const f: Example2 = { key1: 1, key2: 'a' } // incorrect

The way we do it, is we first create a new type that for every key, we have an object property with just that key (ignore the & Partial<Record<Exclude<keyof T, P>, undefined>> for now). So { [P in keyof T]: Record<P, T[P]> } for example will be :

type Example2 = {
    key1: Record<"key1", number>;
    key2: Record<"key2", string>;
}

Then use use an index operation [keyof T] to get a union of all values in this new type, so we get Record<"key1", number> | Record<"key2", string>

This type will work for all except the last test, where you want to not allow multiple properties from the original type. Because of the way excess property checks work with union types (see) it will allow a key if it is present in any of the union constituents.

To remedy this we intersect Record<P, T[P]> with a type that optionally contains the rest of the properties (Exclude<keyof T, P>) but forces all of them to be undefined.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
1

It can be achieved also by the below code:

export type PickOne<T> = {
    [P in keyof T]?: Record<P, T[P]>
}[keyof T]

type Example = {
    key1: number;
    key2: string;
}

type Example2 = PickOne<Example>;

const a: Example2 = { key1: 1 } // correct
const b: Example2 = { key1: "1" } // incorrect - string cannot be assigned to number
const c: Example2 = { key2: 'a' } // correct
const d: Example2 = { key3: 'a' } // incorrect - unknown property