Búsqueda de sitios web

¿TypeScript no valida los tipos genéricos?


Me gustaría usar una función auxiliar asíncrona en mi código TypeScript que devuelva un generador de consultas de base de datos instanciado para un tipo de modelo de base de datos específico. Sin embargo, noté que TypeScript no verifica el tipo genérico especificado, por lo que puedo asignar el valor devuelto a una variable donde el tipo genérico es incorrecto.

Como ejemplo simplificado, esto es válido:

class Foobar<T> { }

const asyncFunc = async (): Promise<Foobar<string>> => {
    ...
}    
const caller = async (): Promise<void> => {
  const result: Foobar<number> = await asyncFunc();    
}

El tipo de Foobar es string en el tipo de retorno de asyncFunc y es number en el tipo de la variable "result".

¿Es este un comportamiento esperado y hay una razón detrás de esto? ¿Hay alguna manera de evitarlo?

Esto es bastante molesto para mi código de base de datos ya que TypeScript ahora no verifica lo que estoy intentando insertar con mi generador de consultas.

Respuesta #1

El sistema de tipos de TypeScript es (en gran medida) estructural y no nominal; dos tipos con la misma forma o estructura se consideran del mismo tipo, incluso si tienen diferentes nombres o declaraciones. Dado que la siguiente declaración de clase genérica

class Foobar<T> { }

no utiliza el parámetro de tipo T en ninguna parte de manera estructural dentro del cuerpo de la clase, entonces Foobar<T> es estructuralmente independiente de T. Por lo tanto, los tipos Foobar<string> y Foobar<number> son iguales. De hecho, el cuerpo de la clase es (al menos en tu ejemplo) completamente vacío, y por lo tanto Foobar<T> es equivalente al tipo de objeto vacío {} sin importar qué T sea.

Las clases vacías y los parámetros de tipo no utilizados a menudo conducen a resultados sorprendentes para aquellos acostumbrados a sistemas de tipos nominales (como los de Java) y, por lo tanto, es mejor evitarlos. Consulte las entradas de la FAQ de TypeScript ¿Por qué se comportan de manera extraña estas clases vacías? y ¿Por qué A<string> se puede asignar a A<number> para interface A<T> {}? para obtener más información.

Suponiendo que quisieras que Foobar<T> dependiera de T, puedes lograr esto agregando alguna propiedad o método a Foobar<T> que realmente dependa de T. Por ejemplo:

class Foobar<T> {
  prop: T;
  constructor(prop: T) {
    this.prop = prop;
  }
}

declare const asyncFunc: () => Promise<Foobar<string>>;

const caller = async (): Promise<void> => {
  const error: Foobar<number> = await asyncFunc();
  // Type 'Foobar<string>' is not assignable to type 'Foobar<number>'.
  const okay: Foobar<string | number> = await asyncFunc(); // okay
}

Ahora Foobar<string> no se puede asignar a Foobar<number>, porque la propiedad prop de tipo string no se puede asignar a number. Ten en cuenta que los tipos de objetos son covariantes en sus tipos de propiedades (ver Diferencia entre varianza, covarianza, contravarianza y bivariancia en TypeScript), lo que significa que Foobar<string> todavía se puede asignar a Foobar<string | number>, porque string se puede asignar al tipo unión string | number. Si esto no es deseable, podrías necesitar modificar la forma en que Foobar<T> depende de T. Pero estamos saliéndonos del alcance de la pregunta tal como se formuló, así que me detendré aquí.

Enlace de Playground al código