interfață - Uniunea și intersecția în Typescript oferă mesaje de eroare neintuitive și greșite
Acest tip exportat validează totul perfect, dar oferă o eroare de validare greșită:
export type Animal = RequireSwimOrFly & RequireColorOrSizeOrVolume & (CantHaveBothColorOrSize | CanHaveNeitherColorOrSize)
const myAnimal: Animal = {
swim: "greatly",
color: "yellow",
size: "large",
}
Afișează următoarea eroare:
Property 'fly' is missing in type '{ swim: string; color: string; size: string; }' but required in type 'Required<Pick<IAnimal, "fly">>'.(2322)
Dar aceasta nu este validarea corectă! Este acceptabil să NU aibă aripi, deoarece poate înota. Adevărata eroare este să aibă culoare și mărime în același timp. Există vreo modalitate de a oferi erori mai intuitive pentru dezvoltatorii viitori?
Iată o versiune funcțională pe TS Playground
Folosesc mai multe tipuri pentru a verifica mai multe condiții:
type RequireSwimOrFly = RequireAtLeastOne<IAnimal, 'swim' | 'fly'>
type RequireColorOrSizeOrVolume = RequireAtLeastOne<IAnimal, 'color' | 'size' | 'volume'>
type CantHaveBothColorOrSize = RequireOnlyOne<IAnimal, 'color' | 'size'>
type CanHaveNeitherColorOrSize = Omit<IAnimal, 'color' | 'size'> & { color?: never, size?: never }
Cu cerințele definite după cum urmează:
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]
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]
Și interfața:
interface IAnimal {
swim?: string;
fly?: string;
color?: string;
size?: string;
volume?: string;
}
Răspuns #1
În cele din urmă, tipul tău Animal este un tip mare de uniune de tipuri de obiect; intersecțiile tind să fie distribuite peste uniuni și o intersecție de tipuri de obiect poate fi, în general, comprimată împreună. Poți chiar să scrii un tip utilitar „identity” pentru a face acest lucru în mod explicit atunci când îl vizualizezi cu IntelliSense:
type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
export type Animal = Id<RequireSwimOrFly & RequireColorOrSizeOrVolume & (
CantHaveBothColorOrSize | CanHaveNeitherColorOrSize
)>
ceea ce îți oferă asta:
/* type Animal = {
color: string; size?: undefined; volume?: string | undefined;
swim: string; fly?: string | undefined;
} | {
color?: undefined; size: string; volume?: string | undefined;
swim: string; fly?: string | undefined;
} | {
color: string; size?: undefined; volume: string;
swim: string; fly?: string | undefined;
} | {
color?: undefined; size: string; volume:
string; swim: string; fly?: string | undefined;
} | {
color?: undefined; size?: undefined; volume: string;
swim: string; fly?: string | undefined;
} | {
color: string; size?: undefined; volume?: string | undefined;
fly: string; swim?: string | undefined;
} | {
color?: undefined; size: string; volume?: string | undefined;
fly: string; swim?: string | undefined;
} | {
color: string; size?: undefined; volume: string;
fly: string; swim?: string | undefined;
} | {
color?: undefined; size: string; volume: string;
fly: string; swim?: string | undefined;
} | {
color?: undefined; size?: undefined; volume: string;
fly: string; swim?: string | undefined;
} */
Dacă îi dai compilatorului o valoare care nu este de tip Animal, asta înseamnă că nu poate fi atribuită niciunuia dintre membrii uniunii. Eșuează la fiecare dintre ei. Un raport complet despre motivul pentru care acea valoare este nepotrivită ar fi foarte, foarte lung, deoarece compilatorul menționează fiecare membru al uniunii și modul în care valoarea ta nu se potrivește cu el. Ceva de genul:
const myAnimal: Animal = { swim: "greatly", color: "yellow", size: "large", } // error!
/* Type { swim: string; color: string; size: string; } is not assignable to Animal.
- It can't be a { ✂ 1st union member ✂ } because its 'size' property is 'string' and not 'undefined'
- It can't be a { ✂ 2nd union member ✂ } because its 'color' property is 'string' and not 'undefined'
- It can't be a { ✂ 3rd union member ✂ } because its 'size' property is 'string' and not 'undefined'
- It can't be a { ✂ 4th union member ✂ } because its 'color' property is 'string' and not 'undefined'
- It can't be a { ✂ 5th union member ✂ } because its 'size' property is 'string' and not 'undefined'
- It can't be a { ✂ 6th union member ✂ } because its 'fly' property is 'undefined' and not 'string'
- It can't be a { ✂ 7th union member ✂ } because its 'fly' property is 'undefined' and not 'string'
- It can't be a { ✂ 8th union member ✂ } because its 'fly' property is 'undefined' and not 'string'
- It can't be a { ✂ 9th union member ✂ } because its 'fly' property is 'undefined' and not 'string'
- It can't be a { ✂ 10th union member ✂ } because its 'fly' property is 'undefined' and not 'string'
*/
Unele sindicate au mii de membri, așa că nu este realist ca compilatorul să ofere o contabilizare completă a tuturor modurilor în care o valoare nu poate fi atribuită acestuia. Trebuie să facă altceva.
Acum, ar putea încerca să găsească „cel mai apropiat” membru al uniunii față de valoare conform unei anumite metrici și apoi să raporteze numai o eroare acolo. Sau poate ar putea încerca să găsească „cea mai comună” cauză a eșecului și să o raporteze. Sau poate ar putea utiliza o altă euristică mai prietenoasă cu utilizatorul. Dar nu face niciuna dintre aceste lucruri; alege ultimul membru din uniune și raportează pe acela:
const myAnimal: Animal = { swim: "greatly", color: "yellow", size: "large", } // error!
/* Type '{ swim: string; color: string; size: string; }' is not assignable to type 'Animal'.
It is missing the following properties from type '{ ✂ 10th union member ✂ }' : volume, fly */
Acest mesaj nu este greșit. Pur și simplu nu este deosebit de iluminator sau intuitiv.
A fost deschis un issue pe GitHub la microsoft/TypeScript#4451 cerând ceva mai bun, dar a fost închis ca „Nu se va rezolva”, comentariul principal explicativ fiind că nu ar fi evident cum să scrii un algoritm bun aici.
Așa că, din păcate, așa stau lucrurile. Primești un mesaj de eroare care explică corect o parte din motivul pentru care valoarea ta este greșită, dar nu neapărat "motivul" pe care l-ar da o persoană inteligentă.
Link de teren de joacă către cod
