rozhranie TypeScript vyžaduje, aby existovala jedna z dvoch vlastností


Snažím sa vytvoriť rozhranie, ktoré by mohlo mať

export interface MenuItem {
  title: string;
  component?: any;
  click?: any;
  icon: string;
}
  1. Existuje spôsob, ako vyžadovať nastavenie component alebo click
  2. Existuje spôsob, ako vyžadovať, aby nemohli byť nastavené obe vlastnosti?

Odpoveď č. 1

S pomocou typu Exclude, ktorý bol pridaný v TypeScripte 2.8, je poskytnutý zovšeobecniteľný spôsob, ako vyžadovať aspoň jednu z množiny vlastností:

type RequireAtLeastOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>> 
    & {
        [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>
    }[Keys]

A čiastočný, ale nie absolútny spôsob, ako vyžadovať, aby bol poskytnutý jeden a iba jeden, je:

type RequireOnlyOne<T, Keys extends keyof T = keyof T> =
    Pick<T, Exclude<keyof T, Keys>>
    & {
        [K in Keys]-?:
            Required<Pick<T, K>>
            & Partial<Record<Exclude<Keys, K>, undefined>>
    }[Keys]

Tu je odkaz na TypeScript ihrisko, ktorý ukazuje oboje v akcii.

Upozornenie pri RequireOnlyOne je, že TypeScript často nevie počas kompilácie o všetkých vlastnostiach, ktoré budú existovať počas behu. Takže je jasné, že RequireOnlyOne nemôže nič urobiť, aby zabránil pridaným vlastnostiam, o ktorých nevie. Na konci odkazu na playground som poskytol príklad, ako RequireOnlyOne môže niečo prehliadnuť.

Rýchly prehľad toho, ako to funguje, pomocou nasledujúceho príkladu:

interface MenuItem {
  title: string;
  component?: number;
  click?: number;
  icon: string;
}

type ClickOrComponent = RequireAtLeastOne<MenuItem, 'click' | 'component'>
  1. Pick<T, Exclude<keyof T, Keys>> z RequireAtLeastOne sa stáva { title: string, icon: string}, čo sú nezmenené vlastnosti kľúčov, ktoré nie sú zahrnuté v 'click' | 'component'

  2. { [K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>> }[Keys] z RequireAtLeastOne sa stáva

    { 
        component: Required<{ component?: number }> & { click?: number }, 
        click: Required<{ click?: number }> & { component?: number } 
    }[Keys]
    

    Čo sa stáva

    {
        component: { component: number, click?: number },
        click: { click: number, component?: number }
    }['component' | 'click']
    

    Ktorý nakoniec sa stáva

    {component: number, click?: number} | {click: number, component?: number}
    
  3. Prienik krokov 1 a 2 vyššie

    { title: string, icon: string} 
    & 
    ({component: number, click?: number} | {click: number, component?: number})
    

    zjednodušuje sa na

    { title: string, icon: string, component: number, click?: number} 
    | { title: string, icon: string, click: number, component?: number}
    

Odpoveď č. 2

Nie pomocou jedného rozhrania, pretože typy nemajú podmienenú logiku a nemôžu na sebe závisieť, ale môžete to urobiť rozdelením rozhraní:

export interface BaseMenuItem {
  title: string;
  icon: string;
}

export interface ComponentMenuItem extends BaseMenuItem {
  component: any;
}

export interface ClickMenuItem extends BaseMenuItem {
    click: any;
}

export type MenuItem = ComponentMenuItem | ClickMenuItem;

Odpoveď č. 3

Existuje jednoduchšie riešenie. Nie je potrebné spoliehať sa na any alebo zložité podmienené typy (pozri odpoveď):

  1. Existuje spôsob, ako vyžadovať nastavenie komponentu alebo kliknutia? (Vrátane OR)
type MenuItemOr = {
    title: string;
    icon: string;
} & ({ component: object } | { click: boolean }) 
// brackets are important here: "&" has precedence over "|"

let testOr: MenuItemOr;
testOr = { title: "t", icon: "i" } // error, none are set
testOr = { title: "t", icon: "i", component: {} } // ✔
testOr = { title: "t", icon: "i", click: true } // ✔
testOr = { title: "t", icon: "i", click: true, component: {} } // ✔

Typ zjednotenia (|) zodpovedá inkluzívnemu OR. Pretína sa s nepodmienenými vlastnosťami.

Použite operátor in na zúženie hodnoty späť na jedno z jej zložiek:

if ("click" in testOr) testOr.click // works 
  1. Existuje spôsob, ako vyžadovať, aby nemohli byť nastavené obe vlastnosti? (Exkluzívne OR/XOR)
type MenuItemXor = {
    title: string;
    icon: string;
} & (
        | { component: object; click?: never }
        | { component?: never; click: boolean }
    )

let testXor: MenuItemXor;
testXor = { title: "t", icon: "i" } // error, none are set
testXor = { title: "t", icon: "i", component: {} } // ✔
testXor = { title: "t", icon: "i", click: true } // ✔
testXor = { title: "t", icon: "i", click: true, component: {} } //error,both set

V podstate môže byť nastavený buď component alebo click, druhý by nikdy 1 nemal byť pridaný súčasne. TS môže vytvoriť diskriminovaný typ z MenuItemXor, ktorý zodpovedá XOR.

Táto XOR podmienka pre MenuItemXor nie je možná s prijatou odpoveďou.


Detské ihrisko

1 Technicky vzaté, prop?: never sa vyrieši ako prop?: undefined, hoci prvé sa často používa na ilustráciu.

Odpoveď č. 4

Alternatíva bez viacerých rozhraní je

export type MenuItem = {
  title: string;
  component: any;
  icon: string;
} | {
  title: string;
  click: any;
  icon: string;
};

const item: MenuItem[] = [
  { title: "", icon: "", component: {} },
  { title: "", icon: "", click: "" },
  // Shouldn't this error out because it's passing a property that is not defined
  { title: "", icon: "", click: "", component: {} },
  // Does error out :)
  { title: "", icon: "" }
];

Opýtal som sa podobnú otázku na Ako vytvoriť niečo ako Partial, čo vyžaduje nastavenie jednej vlastnosti

Vyššie uvedené by sa dalo zjednodušiť, ale nemusí to byť jednoduchšie na čítanie

export type MenuItem = {
  title: string;
  icon: string;
} & (
 {component: any} | {click: string}
)

Všimnite si, že nič z toho vám nebráni pridať obe, pretože TypeScript povoľuje ďalšie vlastnosti na objektoch, ktoré používajú AND/OR. Pozrite si https://github.com/Microsoft/TypeScript/issues/15447

Odpoveď č. 5

Používam toto:

type RequireField<T, K extends keyof T> = T & Required<Pick<T, K>>

Použitie :

let a : RequireField<TypeA, "fieldA" | "fieldB">;

To robí fieldA a fieldB povinnými.

Súvisiace články