I need help with these type definitions. TS is not showing me a warning but I believe I'm violating the type.
type ElementType = "ShortText" | "Status" | "Number";
type BoardType = {
id: string;
name: string;
columns: Array<ColumnType>;
};
type Column = {
id: string;
name: string;
element: ElementType;
};
type StatusType = Column & {
options: { option: string; color: string }[];
};
type ColumnType = Column | StatusType;
type ColumnValue<T extends ElementType> = T extends "ShortText"
? string
: T extends "Status"
? string
: T extends "Number"
? number
: never;
type PulseType<Columns extends ColumnType[]> = {
id: string;
board: string;
data: {
[K in Columns[number]["id"]]?: K extends Columns[number]["id"]
? ColumnValue<Extract<Columns[number], { id: K }>["element"]>
: never;
};
};
const board: BoardType = {
id: "board-1",
name: "Sample Board",
columns: [
{ id: "Name", name: "Full Name", element: "ShortText" },
{
id: "Status",
name: "To DOs",
element: "Status",
options: [
{ option: "To Do", color: "grey" },
{ option: "In Progress", color: "blue" },
{ option: "Done", color: "green" },
],
},
{ id: "Age", name: "Age", element: "Number" },
],
};
const pulses: PulseType<typeof board.columns> = {
id: "1",
board: "board-1",
data: {
Name: "John Doe",
Status: "Done",
Age: 25,
// Uncommented 'ehh', which is not a valid column ID
ehh: "error", // Error: 'ehh' is not assignable to 'never'
},
};
The goal is to have a board. In that board there's an array called columns
. Based on the columns it should determine the type of a pulse. It's kind like a list and items.
In BoardType
it takes an array of ColumnType
, which is the union Column | Status | Relationship
. The reason for this is basically, instead of keeping all the settings in the pulse, any common info is kept in the board. For example the status has options; those options are the same for each pulse in the board, so the options are kept in the column which is extended by Status
.
The ElementType
is just all the possible values of board.columns[].element
.
ColumnValue
is a conditional type that returns the type of each element
The issue I'm running into is, when I pass PulseType
the argument, it doesn't enforce that the data object in it only has the keys of the ids in the board's column array.
As you see, the ehh
property should cause TypeScript to give a warning, but it's not.
First of all, it won't be possible with the current board, since the compiler needs to know the exact shape of the board
, which is impossible just by typing it as BoardType
. Instead, we will need to use const assertion that will prevent the compiler from widening the types to their primitives. With const assertion there will no longer be type checking, which can be fixed by using the satisfies operator. Since const assertion will turn every array to their read-only versions, we will need to update all array/tuple properties to become read-only:
type BoardType = {
id: string;
name: string;
columns: readonly ColumnType[];
groups?: readonly (readonly string[])[];
access?: boolean;
members?: readonly [{ id: string; access: readonly ['admin', 'viewer'] }];
owner: string;
apps?: readonly string[];
created_at: string;
updated_at: string;
};
const board = {
id: 'board-1',
name: 'Sample Board',
columns: [
{ id: 'Name', name: 'Full Name', element: 'string.ShortText' },
{ id: 'Age', name: 'Age', element: 'number.Number' },
],
groups: [['Admins'], ['Editors', 'Viewers']],
access: true,
members: [{ id: 'user-1', access: ['admin', 'viewer'] }],
owner: 'user-1',
apps: ['app-1', 'app-2'],
created_at: '2023-07-01',
updated_at: '2023-07-15',
} as const satisfies BoardType;
In the mapped type instead of mapping through Columns[number]["id"]]
just map through the whole elements (Columns[number]
) and you can change the key to the value of id
property by using the key remapping. There is also no point in checking if K extends X
where X
is what you are mapping. Since not all columns have element
property, we will check whether the current item has this property and if it does then we will pass it to ColumnValue
, otherwise the value will be never
:
type PulseType<Columns extends readonly ColumnType[]> = {
id: string;
board: string;
data: {
[K in Columns[number] as K['id']]?: K extends {
element: ElementType;
}
? ColumnValue<K['element']>
: never;
};
created_at: string;
updated_at: string;
};
Testing:
const pulses: PulseType<typeof board.columns> = {
id: '1',
board: 'board-1',
data: {
Name: 'John Doe',
Age: +new Date(),
ehh: 'error', // expected error
},
created_at: '2022-01-01',
updated_at: '2022-01-01',
};