TypeScript-typ beroende av egenskapsnamn?
Det här är en lite konstig fråga, men jag blev ändå nyfiken på om det är möjligt.
Jag har en JSON-struktur från ett API som definierar en karta så här:
"sizes: {
"thumbnail": "xxxx",
"thumbnail-width: 150,
"thumbnail-height: 150,
"medium: "xxx",
"medium-width": 500,
"medium-height": 500,
...
}
Namnen är delvis fördefinierade, men inte riktigt.
Min fråga är om det är möjligt att definiera vilken egenskap som helst som slutar med -width eller -height som nummer och allt annat som sträng. Som ett regex (.*)-(width|height): number.
TypeScript kan göra alla slags magiska saker, men kan det göra något sådant här?
Svar #1
Just nu, i TS4.1, finns det ingen specifik, konkret typ i TypeScript (låt oss kalla den Values) som representerar de värden som kan tilldelas den values-egenskapen. För protokollets skull tolkar jag detta som: vilken strängnyckel som helst är acceptabel; om nyckeln slutar med -width eller -height, måste egenskapen vara ett number; annars måste det vara en string. Det finns inget krav på att sådana nycklar måste visas i tripletter; till exempel, om "key" är en nyckel, är det inte nödvändigt att "key-height" eller "key-width" också är närvarande.
Kanske när microsoft/TypeScript#42192 åtgärdas kommer du att kunna använda "pattern"-templatelitraler (som implementerats i microsoft/TypeScript#40598) av formen `$ {string}-width` som nycklar. För närvarande kan du inte det.
UPPDATERING för TS4.4: även med mönstermall-literals indexsignaturer som implementerats i microsoft/TypeScript#44512, kan du inte skapa en specifik typ för Values. Du kan nu representera delen där godtyckliga strängar som slutar med -height eller -width måste vara number:
type NumericValues = {
[key: `${string}-${"height" | "width"}`]: number;
}
men det finns inget sätt att representera begränsningen "allt annat måste vara en string"... se microsoft/TypeScript#17867. Så följande fungerar inte:
type BadValues = {
[key: `${string}-${"height" | "width"}`]: number; // error! incompatible
[key: `${string}`]: string; // can't say "everything else" here
}
Jaja.
Så du kan endast representera din begränsning som en generell restriktion, i formen Values<K> för en lämplig K:
type Values<K extends string> = {
[P in K]: P extends `${string}-${"height" | "width"}` ? number : string
};
Om K är mängden av nycklar med strängvärden i Values<K>, måste varje egenskapsvärde antingen vara ett number eller en string beroende på om K slutar med "-height"/"-width" eller inte.
Istället för att tvingas ange ett värde som typ Values<K> för något konkret K, kan du använda en hjälpfunktion för att låta kompilatorn härleda K åt dig:
const asValues = <K extends string>(values: Values<K>) => values;
Låt oss prova det:
const sizes = asValues({
"thumbnail": "xxxx",
"thumbnail-width": 150,
"thumbnail-height": 150,
"medium": "xxx",
"medium-width": 500,
"medium-height": 500,
}); // okay
const badSizes = asValues({
"toenail": 123, // error! Type 'number' is not assignable to type 'string'
"toenail-width": 456,
"psychic": "yyy",
"psychic-height": "tall" // Type 'string' is not assignable to type 'number'
})
Ser bra ut. sizes accepteras, men badSizes resulterar i fel på de egenskaper vars typer är felaktiga.
Självklart innebär det faktum att vi använder generics att du måste dra runt den generiska typparametern K överallt där du vill upprätthålla denna begränsning. Detta kan bli otympligt om det används för hela din kodbas. Istället kanske du vill använda Values<K> för att validera objektliteraler som skickas in, och sedan, när du manipulerar dem, bredda till den närmaste specifika typ som TypeScript kan representera, som en string-indexerbar typ vars egenskaper är string | number:
type WideValues = { [k: string]: string | number };
const wideValues: WideValues = sizes; // okay
Så du använder Values<K> för validering, och WideValues för bearbetning efteråt.
Lekplatslänk till kod
Svar #2
Från TypeScript 4.1 är det möjligt, med hjälp av literala typer. Ett exempel:
type AddSuffixToEachProps<T, Suffix extends string, Value extends any> = T & {
[P in keyof T as `${string & P}-${Suffix}`]: Value
}
Detta är en typ som accepterar någon form av schema och lägger till ett suffix för var och en av dessa egenskaper.
Användning :
type Result = AddSuffixToEachProps<{
foo: string,
bar: string[]
}, 'width', number>
const test: Result = {
foo: 'some string',
"foo-width": 123, // required
bar: ['one', 'two', 'three'],
"bar-width": 456 // required
}
