Búsqueda de sitios web

typescript - la propiedad posiblemente sea 'undefined' aunque ha sido definida explícitamente


Considerando este código muy sencillo:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = { someProperty: [] };

someVar.someProperty.push('test'); // => 'someVar.someProperty' is possibly 'undefined'.ts(18048)

No entiendo por qué TypeScript devuelve el error en esta última línea. someProperty no puede ser undefined ya que fue definido explícitamente.

Si actualizo ligeramente el código y defino someProperty justo después de la instanciación de la variable, funciona:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = {};
someVar.someProperty = [];

someVar.someProperty.push('test'); // => No error!

Esto me parece un error en Typescript... Pero por otro lado, esperaría que un error así se detectara (y corrigiera) muy rápido, así que probablemente no sea un error.

¿Entonces me perdí algo que podría explicar este error?

Respuesta #1

Si anotas una variable con un tipo (no unión) como ExempleType, entonces eso es todo lo que TypeScript sabe sobre el tipo de esa variable. Cualquier información más específica sobre el tipo de un valor que le asignes simplemente no se rastrea. Existe algo llamado estrechamiento por asignación, pero eso solo funciona para tipos unión. Así que si escribes let x: string[] | undefined=[], entonces x se estrecha a string[] después de esa asignación. Pero el similar let y: { x: string[] | undefined }={ x: [] } no funciona de la misma manera porque estás asignando a un tipo de objeto no unión cuya propiedad resulta ser una unión. Sería bueno si el estrechamiento por asignación funcionara también en tipos no unión, pero no sucede (ver este comentario en microsoft/TypeScript#8513 para más información).

Esto explica la diferencia entre const someVar: ExempleType={ someProperty: [] }; y const someVar: ExempleType={}; someVar.someProperty=[];. En el último caso, has asignado la propiedad, que es efectivamente un tipo unión (las propiedades opcionales se comportan de manera similar a las uniones con undefined), por lo que se reduce a string[]. Los accesos directos posteriores a la propiedad, como someVar.someProperty, verán entonces string[], pero someVar en sí mismo no se reduce:

const someVar: ExempleType = {};
someVar.someProperty = [];
someVar.someProperty.push('test'); // no error
function foo(r: Required<ExempleType>) { }
foo(someVar); // error!  Types of property 'someProperty' are incompatible.

En términos generales, si quieres que TypeScript haga un seguimiento de información específica de los inicializadores (y no planeas hacer muchas mutaciones), entonces quizá quieras evitar anotar por completo y usar el operador satisfies para comprobar que el inicializador es compatible con ese tipo.

Esto es un poco desordenado con arreglos vacíos, porque [] terminará siendo inferido como never[], incluso con satisfies. Hay un problema abierto en microsoft/TypeScript#51853. Por ahora, la forma "segura" que utiliza satisfies es extensa:

const someVar = {
    someProperty: [] satisfies string[] as string[]
} satisfies ExempleType;

/* const someVar: { someProperty: string[]; } */

La construcción expr satisfies T as T efectivamente primero verifica que expr sea asignable a T y luego lo amplía a T, por lo que [] satisfies string[] as string[] termina con [] como tipo string[] después de comprobar que funciona. Es más fácil simplemente escribir [] as string[], pero eso es un poco menos seguro. Y luego el satisfies al final se asegura de que { someProperty: string[] } sea compatible con ExempleType, lo cual lo es... pero someVar ahora tiene un tipo más específico que ExempleType; se sabe que someProperty es string[], y el resto de tu código funciona como se desea:

someVar.someProperty.push('test'); // okay
function foo(r: Required<ExempleType>) { }
foo(someVar); // okay

Enlace de Playground al código

Respuesta #2

La razón por la que TypeScript te dice que la propiedad podría ser undefined es porque eso es lo que le indicas. Cuando escribes const x: T=y, TypeScript trata a x como T, independientemente del valor real de y (aunque y debe ser asignable a T).

Si realmente quieres que Typescript use el tipo de y para x, puedes usar const x=y satisfies T, lo cual le indica a Typescript que verifique que y es asignable a T sin convertir y a T.