union typer - Typescript - Send ét element af UnionType til en funktion, men ikke begge
Jeg har to interfaces og vil oprette en funktion, hvor enten interface A eller interface B er inkluderet, men ikke begge på samme tid. Brug af en simpel UnionType virker ikke, da det også tillader, at begge interfaces bliver inkluderet.
Eksempel (https://stackblitz.com/edit/typescript-qgytsy)
interface A{
a: string;
}
interface B{
b: string;
}
function aOrB(modifier: A | B) {}
aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '', b: '' }); // also works but should not work!
Er der en måde at opnå dette på, så de første to kald til aOrB(...) virker, men det sidste ikke?
Med venlig hilsen, Benedikt
Svar #1
Du kan definere en type, som udelukker visse egenskaber:
type IFoo = {
bar: string; can?: never
} | {
bar?: never; can: number
};
let val0: IFoo = { bar: "hello" } // OK only bar
let val1: IFoo = { can: 22 } // OK only can
let val2: IFoo = { bar: "hello", can: 22 } // Error foo and can
let val3: IFoo = { } // Error neither foo or can
Brug derefter den type til modifier-parameteren.
Svar #2
TS-type systemet er strukturelt, det betyder, at {a: 'a', b: 'b'} er et korrekt medlem af A | B, fordi det kan tildeles til A, da det har alle A egenskaber og kan tildeles til B af samme grund. Der er ikke noget problem i at bruge et sådant objekt, hvor A eller B kræves, da det har alt, hvad begge har brug for med hensyn til struktur. Overvej følgende kodeeksempel:
function fOnA(modifier: A) { } // we require only A
const a = { a: '', b: '' } // we create object with additional props
fOnA(a) // no error
Vi kan udføre typekontrol af typen { a: '', b: '' }.
type ExtendsA = { a: '', b: '' } extends A ? true : false // evaluates true
type ExtendsB = { a: '', b: '' } extends B ? true : false // evaluates true
Som du kan se, er { a: '', b: '' } et gyldigt objekt, der kan bruges til A og til B.
For at være klar gør det ingen skade at bruge en sådan konstruktion med ekstra egenskaber, da vi altid har, hvad vi har brug for.
Det tilfælde, hvor vi ønsker at skelne, om vi har A eller B, er et tilfælde, hvor vi bør oprette en diskriminant for at kunne kontrollere, hvilken type objekt der gik ind i funktionen. Overvej:
interface A{
kind: 'A', // discriminant
a: string;
}
interface B{
kind: 'B', // discriminant
b: string;
}
function aOrB(modifier: A | B) {}
aOrB({ kind: 'A', a: '' }); // works!
aOrB({ kind: 'B', b: '' }); // works!
aOrB({ kind: 'B', a: '' }); // error as it should be
aOrB({ kind: 'B', b: '', a: '' }); // error as it should be
Jeg har tilføjet kind for at kunne skelne mellem de to versioner. En sådan tilgang har mange fordele, da vi nu nemt kan tjekke, om vi har A eller B blot ved at kigge på kind.
En anden løsning er at blokere egenskaber, vi ikke ønsker, med never-nøgleordet. Overvej:
interface A{
a: string;
b?: never;
}
interface B{
b: string;
a?: never;
}
function aOrB(modifier: A | B) {}
aOrB({ a: '' }); // works!
aOrB({ b: '' }); // works!
aOrB({ a: '', b: ''}); // error as it should be
Ved at tilføje prop?: never; undgår jeg at tillade, at objektet opfylder A og B kravene.
