Typescript-typ/gränssnitt - om en viss egenskap finns på ett objekt är alla andra egenskaper ogiltiga
Det här svaret har hjälpt mig mycket hittills, men min fråga är lite annorlunda. Jag vill ha en typ som definierar flera tillgängliga egenskaper, men varje gång ett objekt skapas med någon av dessa egenskaper, får det inte ha några andra. Den refererade frågan, och alla dess svar, behandlar bara fallet när det endast finns 2 egenskaper som utesluter varandra.
Minimal reproducerbart exempel:
export interface OperatorExpression {
equals?: any;
lessThan?: any;
lessThanOrEqualTo?: any;
greaterThan?: any;
greaterThanOrEqualTo?: any;
contains?: any;
// etc.
}
Jag vill att objektet som använder detta gränssnitt endast tillåter en av dessa egenskaper på det:
const op1 = { equals: {}, greaterThanOrEqualTo: {} }; // invalid, it should only allow one property
const op2 = { lessThanOrEqualTo: {} }; // valid
Okej, detta är lite påtvingat, men jag har faktiskt ett liknande användningsfall.
Följande det länkade svaret ovan, och även andra, började jag något liknande detta:
export interface EqualsOperatorExpression {
equals: any;
lessThan?: never;
lessThanOrEqualTo?: never;
greaterThan?: never;
greaterThanOrEqualTo?: never;
contains?: never;
}
export interface LessThanOperatorExpression {
equals?: never;
lessThan: any;
lessThanOrEqualTo?: never;
greaterThan?: never;
greaterThanOrEqualTo?: never;
contains?: never;
}
export interface LessThanOrEqualToOperatorExpression {
equals?: never;
lessThan?: never;
lessThanOrEqualTo: any;
greaterThan?: never;
greaterThanOrEqualTo?: never;
contains?: never;
}
export interface GreaterThanOperatorExpression {
equals?: never;
lessThan?: never;
lessThanOrEqualTo?: never;
greaterThan: any;
greaterThanOrEqualTo?: never;
contains?: never;
}
export interface GreaterThanOrEqualToOperatorExpression {
equals?: never;
lessThan?: never;
lessThanOrEqualTo?: never;
greaterThan?: never;
greaterThanOrEqualTo: any;
contains?: never;
}
export interface ContainsOperatorExpression {
equals?: never;
lessThan?: never;
lessThanOrEqualTo?: never;
greaterThan?: never;
greaterThanOrEqualTo?: never;
contains: any;
}
export type OperatorExpression =
| EqualsOperatorExpression
| LessThanOperatorExpression
| LessThanOrEqualToOperatorExpression
| GreaterThanOperatorExpression
| GreaterThanOrEqualToOperatorExpression
| ContainsOperatorExpression;
Nu fungerar detta, men enligt min åsikt är det väldigt klumpigt. Om jag behöver lägga till en ny operator, måste jag gå tillbaka genom varje annan operators gränssnitt och definiera den jag lägger till som never.
Finns det något renare sätt att göra detta i Typescript, eller är jag fast med berget av boilerplate som jag måste uppdatera vid varje nytt tillägg?
Svar #1
Om du har en objekttyp T och vill konvertera den till en union av objekttyper där varje objekttyp har exakt en egenskap från T som obligatorisk och resten som "förbjudna" (genom att göra dem till valfria egenskaper av den omöjliga typen never), kan du göra det på detta sätt:
type SingleProp<T extends object> = {
[K in keyof T]-?: (
{ [P in K]-?: T[K] } &
{ [P in keyof T as P extends K ? never : P]?: never }
)
}[keyof T];
där vi mappar varje nyckel K till en typ som är snittet av en typ med en obligatorisk K-egenskap ({[P in K]-?: T[K]}) och en typ med en förbjuden egenskap för allt utom K ({[P in keyof T as P extends K ? never : P]?: never}), med användning av ? och -? mapping-modifierare för att göra saker valfria/obligatoriska, och nyckelommapning för att undertrycka K från keyof T.
Och sedan indexerar vi i den resulterande mappade typen med keyof T för att få en union av de saker vi vill ha.
Detta ger korrekta men något osköna typer:
type Demo = SingleProp<{ a?: string, b?: number, c?: boolean }>;
/* type Demo =
({ a: string; } & { b?: undefined; c?: undefined; }) |
({ b: number; } & { a?: undefined; c?: undefined; }) |
({ c: boolean; } & { a?: undefined; b?: undefined; }) */
Om du vill slå samman dessa snittpunkter till enstaka objekttyper kan du använda en teknik som beskrivs i Hur kan jag se det fullständiga utökade kontraktet för en Typescript-typ?
type SingleProp<T extends object> = {
[K in keyof T]-?: (
{ [P in K]-?: T[K] } &
{ [P in keyof T as P extends K ? never : P]?: never }
) extends infer O ? { [P in keyof O]: O[P] } : never
}[keyof T];
type Demo = SingleProp<{ a?: string, b?: number, c?: boolean }>;
/* type Demo =
{ a: string; b?: undefined; c?: undefined; } |
{ b: number; a?: undefined; c?: undefined; } |
{ c: boolean; a?: undefined; b?: undefined; }*/
Och dessa kanske inte heller är idealiska eftersom nycklarna omordnas; om det spelar någon roll (och det gör det troligtvis inte) kan du omarbeta snittet så att varje egenskap visas i rätt ordning:
type SingleProp<T extends object> = {
[K in keyof T]-?: (
{ [P in keyof T]?: P extends K ? unknown : never } &
{ [P in K]-?: T[K] }
) extends infer O ? { [P in keyof O]: O[P] } : never
}[keyof T]
type Demo = SingleProp<{ a?: string, b?: number, c?: boolean }>;
/* type Demo =
{ a: string; b?: undefined; c?: undefined; } |
{ a?: undefined; b: number; c?: undefined; } |
{ a?: undefined; b?: undefined; c: boolean; } */
Typen {a?: never, b?: unknown, c?: never} & {b: number} är samma typ som {b: number} & {a?: never, c?: never}, men den förstnämnda kommer att behålla nyckelordningen.
Okej, låt oss prova på din exempeltyp:
type OperatorExpression = SingleProp<OrigOperatorExpression>;
/* type OperatorExpression = {
equals: any;
lessThan?: undefined;
lessThanOrEqualTo?: undefined;
greaterThan?: undefined;
greaterThanOrEqualTo?: undefined;
contains?: undefined;
} | {
equals?: undefined;
lessThan: any;
lessThanOrEqualTo?: undefined;
greaterThan?: undefined;
greaterThanOrEqualTo?: undefined;
contains?: undefined;
} | {
equals?: undefined;
lessThan?: undefined;
lessThanOrEqualTo: any;
greaterThan?: undefined;
greaterThanOrEqualTo?: undefined;
contains?: undefined;
} | {
equals?: undefined;
lessThan?: undefined;
lessThanOrEqualTo?: undefined;
greaterThan: any;
greaterThanOrEqualTo?: undefined;
contains?: undefined;
} | {
equals?: undefined;
lessThan?: undefined;
lessThanOrEqualTo?: undefined;
greaterThan?: undefined;
greaterThanOrEqualTo: any;
contains?: undefined;
} | {
equals?: undefined;
lessThan?: undefined;
lessThanOrEqualTo?: undefined;
greaterThan?: undefined;
greaterThanOrEqualTo?: undefined;
contains: any;
} */
Ser bra ut!
Playground-länk till kod
Svar #2
Ett enkelt sätt är att använda en type som förenar några olika gränssnitt.
Och sedan behöver du en gemensam egenskap som de alla har, men med unika värden. I mitt exempel använder jag tag, men i praktiken kan det vara vad som helst.
type OperatorExpression =
| { tag: 'equals', equals: any }
| { tag: 'lessThan', lessThan: any }
| { tag: 'lessThanOrEqualTo', lessThanOrEqualTo: any }
function example(data: OperatorExpression) {
if (data.tag === 'equals') {
return data.equals
} else if (data.tag === 'lessThan') {
return data.lessThan
} else if (data.tag === 'lessThanOrEqualTo') {
return data.lessThanOrEqualTo
}
}
Så då vet TypeScript att om ett objekt har egenskapen tag 'equals' så måste det ha en motsvarande egenskap equals och så vidare.
