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>
}
addPrefixnimmt einen bestehendenTKeyund fügt ihm einTPrefixhinzu, wennTKeyden Typ string erweitert. Wenn er den Typstringnicht erweitert, wird stattdessenneverals Typ zurückgegeben.removePrefixnimmt einTPrefixedKeyund einTPrefixund entferntTPrefixaus dem Schlüssel, indeminferverwendet wird, um den ursprünglichen Schlüssel abzurufen, der zur Erstellung desTPrefixedKeyverwendet wurde.prefixedValuenimmt einTObjectentgegen, das nicht mit einem Präfix versehen ist. Dann leitet esTValueaus demTPrefixedKeyab, nachdem das Präfix mitremovePrefixentfernt wurde. Wenn dies gelingt, wirdTValuezurückgegeben. Andernfalls wird eine leere Zeichenkette zurückgegeben, die dennoch eine gültige Objekt-Signatur ist.addPrefixToObjectkombiniert all das Obige. Es geht alle Schlüssel durch, die sich derzeit inTObjectbefinden, und fügt ihnen ein Präfix hinzu. Der Wert wird durch die Verwendung vonprefixedValueabgerufen.
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.
