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
}