Búsqueda de sitios web

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!

Artículos relacionados