typescript - Guardias de tipo de TS con tipo complejo
Me he encontrado con un comportamiento interesante de los guardias de tipos de TS y me gustaría preguntarles si simplemente tengo que aceptarlo o si estoy haciendo algo mal. He preparado un ejemplo mínimo (bueno, casi mínimo).
enum LegType {
LegA = 'LegA',
LegB = 'LegB',
}
interface LegA {
type: LegType.LegA
foo: string
}
interface LegB {
type: LegType.LegB
bar: string
}
type Leg = LegA | LegB
enum PlanType {
T = 'T',
C = 'C',
}
interface TPlan {
type: PlanType.T
legs: LegA[]
}
interface CPlan {
type: PlanType.C
legs: (LegA | LegB)[]
}
type Plan = TPlan | CPlan
const isLegA = (o: Leg): o is LegA => o.type === LegType.LegA
const getFoo = (plan: Plan): string | undefined => {
// const legs: Leg[] = plan.legs // With this line the code builds with no errors.
const legs = plan.legs // With this line the code contains error, the type of `legs` is resolved as: LegA[] | (LegA | LegB)[]
const someLegA = legs.find(isLegA)
return someLegA?.foo // Property 'foo' does not exist on type 'LegA | LegB'.
}
Campo de juegos de TS
¿Por qué es necesario agregar :Leg[]? ¿Es posible reescribir el código para que TS entienda por sí mismo cuál es el tipo correcto?
Respuesta #1
Qué asco, veo lo que está pasando y creo que la única respuesta razonable aquí es hacer lo que hiciste: anotar legs como Leg[].
Entonces, la firma de tipo de la biblioteca para el método find() de Array está sobrecargada y la sobrecarga que acepta un callback de guardia de tipo definido por el usuario y devuelve un tipo de elemento más específico es genérica:
find<S extends T>( predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any ): S | undefined; find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
Cuando no anotas legs, obtiene el tipo unión Array<Leg> | Array<LegA>. Este tipo se considera compatible con Array<Leg> pero no lo tratará como tal automáticamente (lo cual probablemente es bueno. Un Array<LegA> no debería permitirte push() un LegB en él, pero un Array<Leg> sí debería).
Eso significa que el tipo de legs.find es en sí mismo una unión de funciones cuyas firmas no son idénticas:
type LegsFind = {
<S extends LegA>(p: (this: void, v: LegA, i: number, o: LegA[]) => v is S, t?: any): S | undefined;
(p: (v: LegA, i: number, o: LegA[]) => unknown, t?: any): LegA | undefined;
} | {
<S extends Leg>(p: (this: void, v: Leg, i: number, o: Leg[]) => v is S, t?: any): S | undefined;
(p: (v: Leg, i: number, o: Leg[]) => unknown, t?: any): Leg | undefined;
};
Antes de TypeScript 3.3, el compilador simplemente se rendía y te decía que no sabía cómo llamar a una unión de funciones de esta manera. Y alguien te diría que cambiaras Array<X> | Array<Y> a Array<X | Y> si estás llamando métodos no mutantes como find(), y usarías Leg[] y seguirías adelante.
TypeScript 3.3 introdujo un comportamiento mejorado para llamar a tipos unión. Hay casos en los que el compilador puede tomar una unión de funciones y representarla como una única función que toma una intersección de los parámetros de cada miembro de la unión. Puedes leer más sobre ello en el pull request relevante, microsoft/TypeScript#29011.
Pero, hay casos en los que no puede hacer esto: específicamente si múltiples ramas de la unión están sobrecargadas o son funciones genéricas. Desafortunadamente para find(), ese es el caso en el que te encuentras. Parece que el compilador intenta encontrar una manera de que esto sea llamable, pero renuncia a las firmas genéricas correspondientes a la reducción de tipos definida por el usuario y termina con solo la "regular", probablemente porque son compatibles entre sí:
type LegsFind33 = (p: (v: LegA, i: number, o: Leg[]) => unknown, thisArg?: any) => Leg | undefined;
Así que lo llamas con éxito, no ocurre ninguna restricción, y te encuentras con el problema aquí. El problema de llamar a uniones de funciones no está completamente resuelto; su incidencia en GitHub microsoft/TypeScript#7294 está cerrada y etiquetada como "Revisit".
Así que hasta que eso se revise, el consejo aquí es sigue siendo tratar Array<X> | Array<Y> como Array<X | Y> si estás llamando a métodos que no mutan, como find()... usa Leg[] y sigue adelante.
Está bien, espero que eso ayude; ¡buena suerte!
