Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to enforce mapped type to have all of the keys in a string literal

Tags:

typescript

Given the following list:

const list = ["A", "B", "C"] as const;
type List = typeof list[number];

I have a map that must have all of the possible keys of list:

const mapping: Record<List, unknown> = {
  A: true,
  B: 2,
  C: "three"
};

Just like I could enforce mapping to map over List, I would like to do the same for a type. Something like this (I'm aware it's an invalid syntax):

type MappedList: Record<List, unknown> = {
  A: boolean,
  B: number,
  C: string
}

My main goal is to prevent a situation when I add a new cell into list and forget to add it to MappedList.

See playground

like image 746
Eliya Cohen Avatar asked Sep 15 '25 19:09

Eliya Cohen


2 Answers

AFAIK, there is not such concept as type for type. However, you can use mapped types to create one type from another.

const list = ["A", "B", "C"] as const;

type ListKey = typeof list[number];

// type MappedList = {
//     A: "property";
//     B: "property";
//     C: "property";
// }
type MappedList = {
  [Prop in ListKey]: 'property'
}

As far as I understood, you also need to assure that A is a boolean, B is a number and C is a string. In order to do it, you need create a map and conditional type:

const list = ["A", "B", "C"] as const;

type ListKey = typeof list[number];

type TypeMap = {
  A: boolean,
  B: number,
  C: string
};

/**
 * If T is a subtype of TypeMap
 * and keyof T extends keyof TypeMap
 */
type BuildMappedList<T> = T extends TypeMap ? keyof T extends keyof TypeMap ? T : never : never;

/**
 * Ok
 */
type MappedList = BuildMappedList<{
  A: true,
  B: 2,
  C: "three",
}>

/**
 * Never
 */
type MappedList2 = BuildMappedList<{
  A: true,
  B: 2,
  C: "three",
  D: [2] // because of extra D property
}>

/**
 * Never
 */
type MappedList3 = BuildMappedList<{
  B: 2,
  C: "three",
}> // because no A property

/**
 * Never
 */
type MappedList4 = BuildMappedList<{
  A: false,
  B: [2], // because B is not a number
  C: "three",
}> 

enter link description here

like image 186
captain-yossarian Avatar answered Sep 17 '25 11:09

captain-yossarian


You can do this by creating a utility that would require generics to match:

type AssertKeysEqual<
  T1 extends Record<keyof T2, any>,
  T2 extends Record<keyof T1, any>
> = T2


const list = ["A", "B", "C"] as const;
type ListKey = typeof list[number];

const mapping: Record<ListKey, unknown> = {
  A: true,
  B: 2,
  C: "three",
};

type MappedList = AssertKeysEqual<Record<ListKey, unknown>, {
  A: boolean;
  B: number;
  C: string;
}>

Typescript playground

like image 44
BorisTB Avatar answered Sep 17 '25 09:09

BorisTB