typescript - Härled värden på specifik nyckel i objekt-array
Jag vill skriva en DataTable-klass som är mycket smart
export interface ColumnDefinition<?> {
readonly name: ?;
title: string;
align?: 'left' | 'center' | 'right';
sortable?: boolean;
hideable?: boolean;
// ...
}
export interface DataTableOptions<?> {
readonly columnDefinitions: ColumnDefinition<?>[];
// ...
}
export class DataTable<?> {
private readonly columnDefinitions: ReadonlyArray<ColumnDefinition>;
constructor(options: DataTableOptions<?>) {
this.columnDefinitions = options.columnDefinitions;
// ...
}
// ...
public title(columnName: ?): string {
return this.columnDefinitions.find(({ name }) => name === columnName)?.title ?? '';
}
// ...
}
Jag satte några ? på ställen där jag inte vet hur jag ska ange generiska typer
Målet är att kalla följande
const table = new DataTable({
columnDefinitions: [
{ name: 'id', title: 'ID' },
{ name: 'v1', title: 'Value 1' },
{ name: 'v2', title: 'Value 2' }
],
// ...
});
Då ska tabellen skrivas som följer: DataTable<'id' | 'v1' | 'v2'>
Om någon försöker använda flera kolumndefinitioner med samma namn, uppstår ett fel
Dessutom bör vissa medlemsfunktioner dra nytta av det
table.title('id'); // 'ID'
table.title('test'); // compile time error
Svar #1
Att säkerställa att vi fångar de faktiska namn som skickats in kan göras enkelt. Vi kommer att fånga kolumndefinitionerna som helhet i en tupltyp (Detta kommer också att hjälpa oss när vi försöker testa för unikhet)
För att fånga typen av columnDefinitions som en tuppeltyp, behöver vi en typparameter, låt oss kalla den C med en begränsning av [ColumnDefinition] | ColumnDefinition[]. Den första [ColumnDefinition] säkerställer att vi får en tuppeltyp, medan ColumnDefinition[] säkerställer att vi tillåter en tuppel av vilken typ som helst.
För att fånga namnet på varje egenskap behöver vi göra lite mer. För det första behöver ColumnDefinition en typparameter som utökar string (interface ColumnDefinition<N extends string>{...}). Detta skulle tillåta oss att skriva något som ColumnDefinition<'id'>
Genom att ha möjligheten att behålla strängliteraltypen för namnet i ColumnDefinition behöver vi återgå till begränsningen av C. Vi behöver nu specificera en typparameter i begränsningen. [ColumnDefinition<string>] | ColumnDefinition<string>[] skulle vara giltigt, men det kommer inte att fånga strängliteraltyper, det kommer bara att härleda string för alla objekt i tuplen. För att få strängliteraltyper skulle vi behöva en extra typparameter begränsad till string (Detta kommer att få kompilatorn att behålla strängliteraltyper.
Så den slutliga definitionen av DataTable skulle vara class DataTable<C extends [ColumnDefinition<V>] | ColumnDefinition<V>[], V extends string> {... }
Med typen C i handen kan vi typa parametern till title relativt C. Så vi skulle kunna skriva title(columnName: C[number]['name']): string
Unikhetsdelen är lite svårare att garantera. Vi kommer att behöva en rekursiv villkorstyp (vilket har alla möjliga varningar kopplade till sig). Men det kan göras. Nedan kommer typen IsUnique att returnera {} om det inte finns några dubbletter eller en tuple som innehåller ett anpassat felmeddelande som kommer att orsaka ett fel när konstruktorn anropas.
Den resulterande lösningen:
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
Lekplatslänk
Svar #2
Jag tror att typvariabelgenerics är vad du letar efter! Ett enkelt exempel på klassen som arbetar med typvariabeln skulle vara:
interface Data<T> {
val: T;
}
class DataTable<T> {
private data: Data<T>;
constructor(val: T) {
this.data = {
val,
}
}
}
ts lekplats
Du har uppdaterat din fråga, och jag är inte säker på vad du exakt gör längre 100%, men jag tror att du vill använda typalias och utöka det med typvariabel på följande sätt:
type Id = "id";
type IdObject = {
id: Id,
title: string
}
type V1 = "v1";
type V1Object = {
name: V1;
title: string;
}
type EitherOne = V1Object | IdObject;
interface Data<T extends EitherOne> {
val: T;
}
class DataTable<T> {
private data: Data<T>;
constructor(val: T) {
this.data = {
val,
}
}
}
ts lekplats
