Búsqueda de sitios web

typescript - Propiedades opcionales mutuamente exclusivas


Para hacer que dos propiedades de un tipo sean mutuamente exclusivas, el modismo común que encontré es el siguiente:

type T = { x: number, y?: never } | { y: number, x?: never }

En este caso, solo se puede establecer x o y, pero no ambos. (El otro enfoque son las uniones discriminadas, pero esto requiere una propiedad adicional que realmente no encaja en mi caso). El problema es cuando quiero hacer que las propiedades x e y sean opcionales. Si uso:

type T = { x?: number, y?: never } | { y?: number, x?: never }

Esto funciona bien mientras configure x o y durante la inicialización. Por ejemplo:

let a: T = { x: 1 };
a.y = 1; // Error

Sin embargo, si se empieza sin x o y, no funciona como esperaba:

let a: T = {};
a.x = 1; // OK
a.y = 1; // OK

Supuse que en la segunda línea el compilador reduciría el tipo y que la tercera línea generaría un error, sin embargo, este no es el caso.

Mi pregunta es por qué no se genera ningún error arriba, ya que después de la asignación la forma de "a" no se ajustará a ninguna forma implícita por el tipo T?

Respuesta #1

Para bien o para mal, el sistema de tipos de TypeScript no es completamente sólido. La seguridad total de tipos no es un objetivo de diseño del lenguaje TypeScript (ver #3). A menudo hay un compromiso entre la seguridad de tipos y la conveniencia, y hay algunas situaciones en las que TypeScript opta por la conveniencia.


Un lugar donde esto tiende a aparecer es con escrituras de propiedades. TypeScript hace la suposición conveniente pero insegura de que los tipos de objetos son covariantes en sus tipos de propiedades. (Véase Diferencia entre varianza, covarianza, contravarianza y bivariancia en TypeScript). Así que se te permite restringir las propiedades en subtipos, como:

interface Bar { 
  a: unknown; 
}

interface Baz extends Bar { 
  a: string; 
}

const baz: Baz = { a: "hello" };

const bar: Bar = baz; // okay

Aquí Baz se considera un subtipo de Bar, y puedes asignar un valor de tipo Baz a una variable de tipo Bar. Entonces, la propiedad a de un Bar puede ser cualquier cosa, pero la propiedad a de un Baz tiene que ser un string. Para la lectura de propiedades esto es completamente seguro. Pero tan pronto como escribas en una propiedad del supertipo, puedes fácilmente hacer cosas inseguras:

bar.a = 123; // okay?
baz.a.toUpperCase(); // RUNTIME ERROR!

Eso es desafortunado, pero es intencional.


Esto también aparece con tipos unión. Para cualquier tipo X y Y, X es un subtipo de X | Y. Eso significa que si tienes un valor del tipo X | Y y quieres asignarle una propiedad, aceptará cosas que podrían no ser seguras ni para X ni para Y, como mostraste en tu ejemplo. Aquí hay otro ejemplo:

const foo = Math.random() < 0.5 ?
    { a: "abc", b: 123 } :
    { a: 123, b: "abc" };
/* const foo: 
   { a: string; b: number; } | 
   { a: number; b: string; } */
foo.a = 10; 
foo.b = 20; // no error, oops!

Este es más o menos el mismo problema. Hay una sugerencia reciente en microsoft/TypeScript#54051 para cambiar este comportamiento específicamente para las uniones, pero no está claro si alguna vez sucederá, especialmente dado lo generalizados que son los problemas con la mutabilidad y la subtipificación en todo el lenguaje.


Así que esa es principalmente la respuesta a por qué no obtienes un error. Pero podrías preguntarte por qué sí obtienes un error cuando inicializas la variable de tipo unión. El mismo error puede demostrarse aquí:

const qux: { a: string, b: number } | { a: number, b: string } = 
  { a: 123, b: "abc" };

/* const qux: { a: number; b: string; } */
qux.a = 10;
qux.b = 20; // error!

La razón es que las variables de tipos unión sufren restringimiento por asignación. Si asignas un valor a una variable de un tipo unión, el compilador temporalmente restringirá el tipo aparente de la variable solo a aquellos miembros de la unión compatibles con el valor asignado. En el caso de qux que se muestra arriba, eso es {a: number; b: string}, y por lo tanto la asignación de la propiedad b es un error. En esencia, el tipo aparente de qux no es una unión, y por eso no te encuentras con el mismo problema.

Y no, la reducción de tipo por asignación no ocurre en los objetos padre cuando estableces una propiedad. Tal vez eso podría suceder en el futuro (y posiblemente sería una forma de implementar microsoft/TypeScript#54051), pero por ahora no es parte del lenguaje. La única manera en que la asignación puede reducir el tipo de una variable es asignando un nuevo valor a la variable, no mutando el valor existente.

Enlace de Playground al código