Ensuring that we capture the actual names passed in can be easily done. We will capture the column definitions as whole in a tuple type (This will also help us when we try to test for uniqueness)
To capture the type of columnDefinitions as a tuple type, we need a type parameter, lets call it C with a constraint of [ColumnDefinition] | ColumnDefinition[]. The first [ColumnDefinition] ensures we get a tuple type, while the ColumnDefinition[] ensures we allow a tuple of any type.
To capture the name of each property we need to do a bit more. Firstly ColumnDefinition needs a type parameter extending string (interface ColumnDefinition<N extends string>{...}). This would allow us to write something like ColumnDefinition<'id'>
Having the possibility to keep the string literal type for the name in ColumnDefinition we need to go back to the constraint of C. We now need to specify a type parameter in the constraint.[ColumnDefinition<string>] | ColumnDefinition<string>[] would be valid but it will not capture string literal types, it will just infer string for all the items in the tuple. To get string literal types we would need an extra type parameter constrained to string (This will trigger the compiler to keep string literal types.
So the final definition of DataTable would be class DataTable<C extends [ColumnDefinition<V>] | ColumnDefinition<V>[], V extends string> {... }
With the C type in hand we can type the parameter to title relative to C. So we could write title(columnName: C[number]['name']): string
The uniqueness part is a bit more difficult to guarantee. We will need a recursive conditional type (which have all sorts of warnings attached to them). But it can be done. Below the IsUnique type will return {} if there are no duplicates or a tuple containing a custom error message which will cause an error when invoking the constructor.
The resulting solution:
export interface ColumnDefinition<N extends string> {
readonly name: N;
title: string;
align?: 'left' | 'center' | 'right';
sortable?: boolean;
hideable?: boolean;
// ...
}
export interface DataTableOptions<C extends ColumnDefinition<string>[]> {
readonly columnDefinitions: C;
// ...
}
type ColumnName<T> = T extends ColumnDefinition<infer N> ? N : never;
type IsUnique<T extends any[], E = never> = {
next: ((...a: T) => void) extends ((h: infer H, ...t: infer R) => void) ?
[ColumnName<H>] extends [E] ? ["Names are not unique", ColumnName<H>, "was found twice"] :
IsUnique<R, E | ColumnName<H>>: ["NO", T]
stop: {}
}[T extends [] ? "stop" : "next"];
export class DataTable<C extends [ColumnDefinition<V>] | ColumnDefinition<V>[], V extends string> {
private readonly columnDefinitions: Readonly<C>;
constructor(options: DataTableOptions<C> & IsUnique<C> ) {
this.columnDefinitions = options.columnDefinitions;
// ...
}
public title(columnName: C[number]['name']): string {
return this.columnDefinitions.find(({ name }) => name === columnName)?.title ?? '';
}
// ...
}
const table = new DataTable({
columnDefinitions: [
{ name: 'id', title: 'ID' },
{ name: 'v1', title: 'Value 1', align: "right" },
{ name: 'v2', title: 'Value 2', align: "right" }
// { name: 'id', title: 'ID' },
// Comment the line above to get the error below
// Type '{ columnDefinitions: [{ name: "id"; title: string; }, { name: "v1"; title: string; }, { name: "v2"; title: string; }, { name: "id"; title: string; }]; }' is not assignable to type '["Names are not unique", "id", "was found twice"]'.(2345)
],
// ...
});
table.title("Id") // err
table.title("id") // ok
Playground Link