Búsqueda de sitios web

interfaz - Las uniones e intersecciones en Typescript dan mensajes de error poco intuitivos y incorrectos


Este tipo exportado valida todo perfectamente, pero da un error de validación incorrecto:

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

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

Da el siguiente error:

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

¡Pero esa no es la validación correcta! Es aceptable no tener vuelo, porque tiene natación. El verdadero error es tener color y tamaño al mismo tiempo. ¿Hay alguna manera de dar errores más intuitivos para futuros desarrolladores?

Aquí hay una versión funcional en TS Playground

Estoy usando varios tipos para verificar múltiples condiciones:

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 }

Con los requisitos definidos como sigue:

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]

Y la interfaz:

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

Respuesta #1

En última instancia, tu tipo Animal es un gran tipo unión de tipos de objeto; las intersecciones tienden a distribuirse sobre las uniones y una intersección de tipos de objeto generalmente se puede colapsar juntos. Incluso puedes escribir un tipo de utilidad "identidad" auxiliar para hacer que eso ocurra explícitamente cuando lo veas con IntelliSense:

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

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

lo que te da esto:

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

Si le das al compilador un valor que no es del tipo Animal, eso significa que no se puede asignar a ninguno de los miembros de la unión. Falla en cada uno de ellos. Un informe completo de por qué ese valor es inapropiado sería muy, muy largo, ya que el compilador menciona cada miembro de la unión y cómo tu valor no coincide con él. Algo así:

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

Algunos sindicatos tienen miles de miembros, por lo que no es realista que el compilador proporcione un informe completo de todas las formas en que un valor deja de ser asignable a él. Tiene que hacer otra cosa.

Ahora, podría intentar encontrar "el miembro más cercano" de la unión al valor según alguna métrica, y luego informar solo un error allí. O tal vez podría intentar encontrar "la razón más común" de fallo y reportar eso. O quizás podría hacer alguna otra heurística más amigable para los humanos. Pero no hace ninguna de esas cosas; elige el último miembro de la unión y reporta ese:

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

Este mensaje no está equivocado. Simplemente no es particularmente iluminador o intuitivo.


Se presentó un problema en GitHub en microsoft/TypeScript#4451 solicitando algo mejor, pero se cerró como "No se arreglará", siendo el comentario explicativo principal que no sería obvio cómo escribir un buen algoritmo aquí.

Así que, desafortunadamente, así es. Recibes algún mensaje de error que explica correctamente parte de la razón por la cual tu valor es incorrecto, pero no necesariamente "la" razón que un ser humano inteligente podría dar.

Enlace de Playground al código

Artículos relacionados