reactjs - Kräver att minst en av två egenskaper ska anges i props
Med TypeScript är det möjligt att skapa en typ där minst en av två egenskaper är obligatorisk. Det finns (åtminstone) två sätt att göra detta på: genom att använda unionstyper och en något mer komplex generisk lösning. Dessa två lösningar fungerar bra. Jag kan skapa och få önskat kompileringsfel när ingen av de två egenskaperna anges.
Men när man använder den slutliga typen för att specificera props i en React-komponent, kan kompilatorn inte hitta de två "obligatoriska" egenskaperna. Här är ett förenklat exempel:
interface Base {
text?: string;
}
interface WithId extends Base {
id: number;
}
interface WithToken extends Base {
token: string;
}
type Identifiable = WithId | WithToken | (WithId & WithToken);
//OK
const item: Identifiable = {
text: "some text"
id: 4
};
//OK
const item: Identifiable = {
token: "some token"
};
//Error
const item: Identifiable = {
text: "some text"
};
//OK
export class MyComponent extends React.Component<Identifiable, any> {
render() {
//Error (see below)
return <p>{this.props.id}</p>
}
}
Felet som uppstår när man försöker komma åt en av de två nödvändiga egenskaperna (oavsett var inne i klassen) ser ut ungefär så här:
Property 'id' does not exist on type '(Readonly<{children?: ReactNode;}> & Readonly<WithId>) | (Readonly<{children?: ReactNode;}> & Readonly<WithToken>) | (Readonly<{children?: ReactNode;}> & Readonly<WithId & WithToken>)'
Property 'id' does not exist on type '(Readonly<{children?: ReactNode;}> & Readonly<WithToken>'.
Finns det ett sätt att kringgå detta och få kompilatorn att förstå dessa krav?
Observera: använder TypeScript 3.0.1, React 16.4.2, med de senaste tillgängliga typdefinitionerna till dags dato.
Svar #1
Du kan inte få åtkomst till fält i en union som inte är gemensamma för alla medlemmar i unionen.
De alternativ som kommer i åtanke är typvakter eller att använda en annan typ för props när du faktiskt använder dem.
Det enkla alternativet är en typvakt, bara kolla efter egenskapens existens med en in-typvakt:
export class MyComponent extends React.Component<Identifiable, any> {
render() {
if ('id' in this.props) {
// Ok
return <p>{this.props.id}</p>
}
}
}
Den andra mer komplicerade lösningen är att skapa en typ från Identifiable som är samma union, men med varje medlem av unionen utökad med de saknade fälten från hela unionen där de saknade fälten är valfria och av typen undefined. I princip vill vi gå från { id: string } | { token: string } till { id: string; token?:undefined } | { token: string; id?:undefined }. Detta skulle tillåta oss att komma åt vilket fält som helst i unionen samtidigt som det fortfarande är typ-säkert under strictNullChecks.
Denna nya typ kommer fortfarande att vara kompatibel med dina ursprungliga egenskaper, och därför skulle vi bara behöva en lokal variabel av denna typ och tilldela den med klassens egenskaper.
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type FlattenUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<keyof TAll, keyof T>, undefined>> : never;
type FlattenUnion<T> = FlattenUnionHelper<T, UnionToIntersection<T>>
export class MyComponent extends React.Component<Identifiable, any> {
render() {
let props: FlattenUnion<Identifiable> = this.props
return <p>{props.id}</p>
}
}
Jag tror att du kan använda den platta versionen direkt som props, men du bör testa att alla dina krav är uppfyllda. Från det jag testade fungerar det bra.
export class MyComponent extends React.Component<FlattenUnion<Identifiable>, any> { /*...*/}
let f = <MyComponent id={0} />
let f2 = <MyComponent token="" />
let f3 = <MyComponent id={0} token="" />
let f4 = <MyComponent text="" /> // error
Svar #2
Den vanliga idén som presenteras i de andra svaren, att användning av typvakter kan tillfredsställa kompilatorn, är korrekt och fungerar bra. Inspirerad av hur unionstyper fungerar och av de svar som refereras till i frågan, har jag dock beslutat att försöka bygga en generisk typ som skulle fungera utan att kräva typvakter (observera att även om typvakter inte är nödvändiga för TypeScript-kompilatorn, bör kontroll av om en egenskap finns på ett objekt i JavaScript fortfarande göras vid körning, beroende på ditt scenario).
Det här är lösningen som äntligen fungerade för mig:
interface Base {
text?: string;
id?: number;
token?: string;
}
type RequireProperty<T, Prop extends keyof T> = T & {[key in Prop]-?:T[key]};
type RequireIdOrToken = RequireProperty<Base, "id"> | RequireProperty<Base, "token">;
//Error
const o1: RequireIdOrToken = { };
//Error
const o2: RequireIdOrToken = {
text: "some text"
};
//OK
const o3: RequireIdOrToken = {
id: 4
};
//OK -> no type guard needed
o3.token = "a new token";
//OK
const o4: RequireIdOrToken = {
token: "some token"
};
Den allmänna idén är att ha en unionstyp mellan två typer som har samma egenskaper. Detta undviker behovet av att använda typvakter. På vardera av de två typerna markeras den ena eller andra egenskapen som obligatorisk. Detta kräver att båda egenskaperna markeras som valfria i "base"-gränssnittet.
Exemplet kunde göras generiskt
type RequireProperty<T, Prop extends keyof T> = T & {[key in Prop]-?:T[key]};
type RequireTwoProperties<T, Prop1 extends keyof T, Prop2 extends keyof T>
= RequireProperty<T, Prop1> | RequireProperty<T, Prop2>;
och så utökad för att acceptera flera egenskaper. En generisk typ som accepterar en "lista" av egenskapsnamn (en delmängd av keyof T) och omvandlar det till en unionstyp av RequireProperty-typ för varje specificerad egenskap, skulle vara den perfekta lösningen. Jag vet inte om det är möjligt för närvarande.
