sučelje - Typescript unija i presjek daju neintuitivne i pogrešne poruke o pogreškama


Ova izvezena vrsta savršeno provjerava sve, ali daje pogrešnu poruku o pogrešci prilikom provjere:

export type Animal = RequireSwimOrFly & RequireColorOrSizeOrVolume & (CantHaveBothColorOrSize | CanHaveNeitherColorOrSize)

const myAnimal: Animal = {
swim: "greatly",
color: "yellow",
size: "large",
}

Daje sljedeću pogrešku:

Property 'fly' is missing in type '{ swim: string; color: string; size: string; }' but required in type 'Required<Pick<IAnimal, "fly">>'.(2322)

Ali to nije ispravna provjera! Prihvatljivo je NEMAJATI krila, jer ima plivanje. Prava pogreška je imati boju i veličinu istovremeno. Postoji li način da se za buduće programere daju intuitivnije pogreške?

Evo radne verzije na TS Playgroundu

Koristim više vrsta da provjerim više uvjeta:

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 }

S zahtjevima definiranim kako slijedi:

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 sučelje:

interface IAnimal {
    swim?: string;
    fly?: string;
    color?: string;
    size?: string;
    volume?: string;
}

Odgovor #1

U konačnici, vaš Animal tip je veliki unija tipova objekata; presjeci se obično distribuiraju preko unija, a presjek objekata se općenito može sažeti zajedno. Možete čak napisati pomoćni "identity" korisnički tip da to eksplicitno napravite kad ga gledate u IntelliSenseu:

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

export type Animal = Id<RequireSwimOrFly & RequireColorOrSizeOrVolume & (
    CantHaveBothColorOrSize | CanHaveNeitherColorOrSize
)>

što vam daje ovo:

/* 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;
} */

Ako kompilatoru date vrijednost koja nije tipa Animal, to znači da se ne može dodijeliti nijednom članu unije. Ona ne uspijeva kod svakog od njih. Cjelovito izvješće o tome zašto je ta vrijednost neprikladna bilo bi vrlo, vrlo dugo, budući da kompilator spominje svakog člana unije i način na koji vaša vrijednost ne odgovara njemu. Nešto poput:

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'
*/

Neki sindikati imaju tisuće članova, pa nije realno očekivati da prevoditelj pruži potpuni izvještaj o svim načinima na koje vrijednost ne može biti dodijeljena njima. Mora učiniti nešto drugo.

Sada, mogao bi pokušati pronaći „najbližeg” člana unije vrijednosti prema nekoj metričkoj mjeri, a zatim prijaviti samo tamo pogrešku. Ili bi možda mogao pokušati pronaći „najčešći” razlog neuspjeha i prijaviti njega. Ili bi možda mogao primijeniti neku drugu, više korisnički prihvatljivu heuristiku. Ali ne radi ništa od toga; bira posljednjeg člana unije i prijavljuje njega:

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 */

Ova poruka nije pogrešna. Samo što nije osobito prosvjetljujuća ili intuitivna.


Na GitHubu je otvoren problem na microsoft/TypeScript#4451 tražeći nešto bolje, ali je zatvoren kao 'Neće se popraviti', a glavni objašnjavajući komentar bio je da ne bi bilo očito kako napisati dobar algoritam ovdje.

Dakle, nažalost, tako stvari stoje. Dobiješ neku poruku o pogrešci koja ispravno objašnjava dio razloga zašto je tvoja vrijednost loša, ali ne nužno "taj" razlog kojeg bi dao pametan čovjek.

Poveznica na Playground za kod

Povezani članci