javascript - Typescript strikt objekt nyckelnamn, men tillåt alternativ för nyckelnamnet


Jag försöker skapa en strikt mappning för ett tema, men jag vill att nycklarna i mappningen inte ska vara dynamiska, utan ha valfria värden för nycklarna. Det betyder att nyckeltypen "100 | AppBackground" kan ha en av dessa nycklar och samma värde. Här är några av de saker jag har försökt. Jag vill alltid tillåta strikt nyckel-värdepar som red:color.

type 300 = '300' | 'CardBackground';
export type ThemeMapping = {
  [key: '100' | 'AppBackground']: Color;
  [key in '200' | 'ContentBackground']: Color;
  [key: 300 ]: Color;
  ['400' | 'InactiveBackground']: Color;
  link: Color;
  red: Color;
  yellow: Color;
  green: Color;
  blue: Color;
}

Jag vet att för dynamiska värden kan du också göra {[key: string]: Color}. Jag vill bara att nyckeltypen i princip ska vara alternativ snarare än dynamisk eller bara en vanlig sträng.

Vänligen låt mig veta om du behöver att jag förklarar mer.

Svar #1

TypeScript stöder inte inbyggt konceptet "ett objekt med exakt en egenskap från en viss mängd", så om du vill efterlikna det måste du skriva något som fungerar på det sättet själv. Här är ett tillvägagångssätt:

type ExactlyOneProp<T> = {
    [K in keyof T]-?: Pick<T, K> & { [P in Exclude<keyof T, K>]?: never }
}[keyof T];

Idén är att ExactlyOneProp<T> ska resultera i en unionstyp med ett medlemsförslag för varje egenskap hos T, där varje medlemsförslag ska kräva en nyckel och förbjuda de andra. TypeScript kan heller inte exakt förbjuda en nyckel; istället kan du göra en valfri egenskap vars värde är den omöjliga typen never, så det enda acceptabla är att utelämna egenskapen (eller kanske sätta den till undefined beroende på kompilatoralternativ). Så ExactlyOneProp<{a: 0, b: 1, c: 2}> borde vara ekvivalent med {a: 0, b?: never, c?: never} | {a?: never, b: 1, c?: never} | {a?: never, b?: never, c: 2}.

ExactlyOneProp<T> fungerar genom att mappa över varje egenskapsnyckel K i T, och skapa en ny egenskap av typen Pick<T, K> & {[P in Exclude<keyof T, K>]?: never} (använder hjälp-typen Pick<T, K> för att vara ett objekt med bara en känd K-egenskap) och sedan korsar den med en annan mappad typ med nycklarna i Exclude<keyof T, K> (använder hjälp-typen Exclude<X, Y> för att filtrera bort K från nycklarna i T) med alla valfria egenskaper (från ?-mappningsmodifieraren) av typen never.


Det ger en ganska ful typ, så vi kommer att snygga till den via

type Pretty<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

vilket i princip bara kollapsar alla objektsnitt till deras motsvarande enkelobjektstyper (t.ex., {a: 0} & {b: 1} blir {a: 0, b: 1}.

Låt oss testa det:

type Test = Pretty<ExactlyOneProp<{a: 0, b: 1, c: 2}>>;
/* type Test = 
   { a: 0; b?: undefined; c?: undefined; } | 
   { b: 1; a?: undefined; c?: undefined; } | 
   { c: 2; a?: undefined; b?: undefined; };
*/

Ser bra ut.


Så nu, för att göra ThemeMapping på det sätt du vill, behöver vi korsbefrukta en massa olika delar, ungefär så här:

type ThemeMapping = Pretty<
    ExactlyOneProp<{ 100: Color, AppBackground: Color }> &
    ExactlyOneProp<{ 200: Color, ContentBackground: Color }> &
    ExactlyOneProp<{ 300: Color, CardBackground: Color }> &
    ExactlyOneProp<{ 400: Color, InteractiveBackground: Color }> &
    { link: Color; red: Color; yellow: Color; green: Color; blue: Color }
>;

Det blir en förening av 2×2×2×2×1=16 medlemmar, var och en av vilka har 2+2+2+2+5=13 egenskaper, så att visa allt på en gång är för mycket att göra här. Det ser ut som:

type ThemeMapping = {
    100: Color; AppBackground?: undefined;
    200: Color; ContentBackground?: undefined;
    300: Color; CardBackground?: undefined;
    400: Color; InteractiveBackground?: undefined;
    link: Color; red: Color; yellow: Color; green: Color; blue: Color;
} | {
    100: Color; AppBackground?: undefined;
    200: Color; ContentBackground?: undefined;
    300: Color; CardBackground?: undefined;
    InteractiveBackground: Color; 400?: undefined;
    link: Color; red: Color; yellow: Color; green: Color; blue: Color;
} | ⋯;

Du kan verifiera att det beter sig som önskat genom att prova olika möjligheter:

declare const c: Color;
const base = { link: c, red: c, yellow: c, green: c, blue: c };
let t: ThemeMapping;
t = { ...base, "100": c, "200": c, "300": c, "400": c }; // okay
t = { ...base, "100": c, "200": c, "CardBackground": c, "400": c }; // okay
t = { ...base, "100": c, "200": c, "400": c }; // error, missing prop
t = { ...base, "100": c, "200": c, "300": c, 
  "CardBackground": c, "400": c }; // error, extra prop

Som du ville, måste du definiera exakt en av 300 och CardBackground egenskaperna, och du kommer att få ett fel om du försöker definiera båda eller ingen. Och du kan testa de andra egenskaperna själv om du vill.

Playground-länk till kod

Svar #2

Först kan du definiera typerna för varje grupp och en bas typ för temakartläggning med fasta egenskaper, på detta sätt:

type Color = string; // Replace this with your actual Color type

type Key100 = '100' | 'AppBackground';
type Key200 = '200' | 'ContentBackground';
type Key300 = '300' | 'CardBackground';
type Key400 = '400' | 'InactiveBackground';

type ThemeMappingBase = {
  link: Color;
  red: Color;
  yellow: Color;
  green: Color;
  blue: Color;
};

Nästa, skapa dina mappade typer:

type ThemeMapping100 = { [key in Key100]: Color };
type ThemeMapping200 = { [key in Key200]: Color };
type ThemeMapping300 = { [key in Key300]: Color };
type ThemeMapping400 = { [key in Key400]: Color };

Slutligen, kombinera baskonceptet med de delvis kartlagda typerna:

export type ThemeMapping = ThemeMappingBase &
  Partial<ThemeMapping100> &
  Partial<ThemeMapping200> &
  Partial<ThemeMapping300> &
  Partial<ThemeMapping400>;

Jag är inte riktigt säker på om detta uppfyller dina behov, men hoppas att detta kan hjälpa dig.

Relaterade artiklar