Website-Suche

Generics - Definieren Sie Präfix für Objektkeys mit Typen in TypeScript


Ich habe die folgende Schnittstelle, die ein Objekt definiert, bei dem die Eigenschaften zwei verschiedene Typen haben können.

export interface OptionsA {
    name: string;
}

export interface OptionsB {
    parts: number;
}

export interface OptionsConfig {
    [key: string]: OptionsA | OptionsB;
}

Das funktioniert einwandfrei, aber es gibt eine Einschränkung, dass Eigenschaften vom Typ OptionsB mit "@" vorangestellt sein müssen.

Zum Beispiel;

const example: OptionsConfig = {
    '@sample': {parts: 1},
    other: {name: 'example'}
};

Also funktioniert das Obige gut, aber das folgende Beispiel wäre falsch.

const example: OptionsConfig = {
    '@sample': {parts: 1},
    other: {name: 'example'},
    '@wrong': {name: 'error'}
};

Ich frage mich, ob es mit TypeScript möglich ist zu erklären, dass @wrong nur das Interface OptionsB implementieren kann, weil es das @-Präfix hat.

Alternativ gibt es einen anderen Ansatz, um eine ähnliche Art von Einschränkung zu erreichen.

Antwort #1

Typescript 4.1 gab uns Schlüssel-Neuzuordnung, sodass man so etwas machen kann:

type addPrefixToObject<T, P extends string> = {
  [K in keyof T as K extends string ? `${P}${K}` : never]: T[K]
}

Dann wird Ihre OptionsConfig zu

export interface OptionsConfig {
    [key: string]: OptionsA | addPrefixToObject<OptionsB, '@'>;
}

Beispiel im Typescript-Spielplatz verfügbar.

Antwort #2

TL;DR: Kopiere den ersten Codeblock

Auch wenn dies zum Zeitpunkt der Fragestellung vielleicht nicht möglich war, bin ich darauf gestoßen, während ich nach einer Lösung suchte, die ich einfach kopieren und einfügen konnte. TypeScript 4.1 hat uns Template-Literale gegeben, die diese Implementierung möglich machen.

Ein vollständiges Beispiel auf dem TypeScript-Spielplatz ist hier.


Zuerst müssen wir einige Hilfstypen definieren, um Objekttypen automatisch zu präfixen. Dies kann mit dem folgenden Codeblock erreicht werden. Wenn Sie damit noch nicht vertraut sind, schlage ich vor, dass Sie sich zuerst über Template-Literal- und bedingte Typen informieren, die beide im nachfolgenden Code stark verwendet werden.

type addPrefix<TKey, TPrefix extends string> = TKey extends string
  ? `${TPrefix}${TKey}`
  : never;

type removePrefix<TPrefixedKey, TPrefix extends string> = TPrefixedKey extends addPrefix<infer TKey, TPrefix>
  ? TKey
  : '';

type prefixedValue<TObject extends object, TPrefixedKey extends string, TPrefix extends string> = TObject extends {[K in removePrefix<TPrefixedKey, TPrefix>]: infer TValue}
  ? TValue
  : never;

type addPrefixToObject<TObject extends object, TPrefix extends string> = {
  [K in addPrefix<keyof TObject, TPrefix>]: prefixedValue<TObject, K, TPrefix>
} 
  • addPrefix nimmt einen bestehenden TKey und fügt ihm ein TPrefix hinzu, wenn TKey den Typ string erweitert. Wenn er den Typ string nicht erweitert, wird stattdessen never als Typ zurückgegeben.
  • removePrefix nimmt ein TPrefixedKey und ein TPrefix und entfernt TPrefix aus dem Schlüssel, indem infer verwendet wird, um den ursprünglichen Schlüssel abzurufen, der zur Erstellung des TPrefixedKey verwendet wurde.
  • prefixedValue nimmt ein TObject entgegen, das nicht mit einem Präfix versehen ist. Dann leitet es TValue aus dem TPrefixedKey ab, nachdem das Präfix mit removePrefix entfernt wurde. Wenn dies gelingt, wird TValue zurückgegeben. Andernfalls wird eine leere Zeichenkette zurückgegeben, die dennoch eine gültige Objekt-Signatur ist.
  • addPrefixToObject kombiniert all das Obige. Es geht alle Schlüssel durch, die sich derzeit in TObject befinden, und fügt ihnen ein Präfix hinzu. Der Wert wird durch die Verwendung von prefixedValue abgerufen.

Wenn Sie dies in die Tat umsetzen, scheint es ganz gut zu funktionieren:

const myConfig: OptionsConfig = {};

// Works:
myConfig.attr1 = {name: 'name'};
myConfig.attr2 = {"@parts": 1};
myConfig.attr3 = {"@parts": 1, name: 'name'};

// Error: Type '{ parts: number; }' is not assignable to type 'OptionsA | addPrefixToObject<OptionsB, "@">'.
myConfig.attr4 = {"parts": 1};

// Prints as:
// type prefixedOptionB = {
//    "@parts": number;
// }
type PrefixedOptionB = addPrefixToObject<OptionsB, '@'>;

Und die Autovervollständigung funktioniert ebenfalls entsprechend:


All dies kann wahrscheinlich weiter optimiert werden, also wenn Sie irgendwelche Vorschläge haben, hinterlassen Sie bitte einen Kommentar.

Ein vollständiges Beispiel auf dem TypeScript-Spielplatz ist hier.

Verwandte Artikel