Búsqueda de sitios web

typescript - ¿Por qué Object.values para un Partial Record tiene tipos indefinidos?


Al extraer Object.values de un Registro Parcial, los valores son una unión entre lo que esperaría y undefined.

const example: Partial<Record<string, number>> = {}
const values = Object.values(example)

// strangely, the typing for values is as follows:
// const values: (number | undefined)[]

En lugar de (number | undefined)[], esperaría que fuera number[]. En la práctica, cada valor está definido y, a menos que me esté perdiendo de algo, siempre lo estará.

El sandbox de código que muestra el problema (con la estructura de React) se puede encontrar aquí

Esto parece probablemente ser algún artefacto del sistema de tipos, pero me gustaría entender qué está pasando y si hay alguna manera de evitar este comportamiento.

Respuesta #1

En todo lo que sigue asumiré que todos estamos usando el conjunto de opciones de compilador --strict, en particular la opción de compilador --strictNullChecks. Si no tienes habilitado --strictNullChecks, la mayor parte de la discusión siguiente que involucra el tipo undefined no aplicará, pero probablemente no tendrías el problema planteado en esta pregunta en primer lugar.


El tipo de utilidad Partial<T> se implementa como type Partial<T>={[K in keyof T]?: T[K]} y aplica el modificador de tipo mapeado opcional para hacer que todas las propiedades resultantes sean opcionales.

El tipo de utilidad Record<K, V> es un tipo mapeado implementado como type Record<K, V>={[P in K]: V}; y cuando haces eso con string como clave, resulta en una firma de índice de cadena como type Dictionary<V>={[k: string]: V}; donde cualquier propiedad cuya clave sea del tipo string debe tener un valor del tipo V.


Realmente no recomendaría mezclar modificadores opcionales de tipo mapeado (Partial<T>) con firmas de índice (Record<string, V>). Ambos hacen, eh, cosas interesantes con el tipo undefined por defecto, y existen opciones del compilador para cambiar este comportamiento por defecto y producir otros, eh, comportamientos interesantes que a algunas personas les gustan y a otras no. Y sin importar qué configuraciones uses, la combinación de ambos probablemente te hará sentir incómodo, al menos hasta TypeScript 4.8.


Primero, propiedades opcionales: en el modo --strict, TypeScript realmente no distingue entre propiedades ausentes y aquellas que están presentes-pero-undefined. A menudo es razonable tratarlas como lo mismo, ya que leer cualquiera de ellas te dará undefined. Pero, ¿se debería permitir escribir undefined en el valor? ¿O deberías poder leer undefined después de haber usado el operador in para verificar la presencia de la propiedad? Esto depende de lo que pienses que significa "opcional". Por defecto, TypeScript dice que sí, si una propiedad es opcional entonces deberías poder escribir undefined allí. Y por lo tanto, {x?: string} y {x?: string | undefined} son tipos idénticos.

¿No te gusta eso? Bueno, a muchas personas tampoco les gustó... microsoft/TypeScript#13195, "Distinguir entre missing y undefined", estuvo abierto durante mucho tiempo y recibió cientos de votos a favor. Y así, con TypeScript 4.4 se introdujo la opción del compilador --exactOptionalPropertyTypes. Si la activas, ya no podrás asignar undefined a una propiedad opcional a menos que undefined esté incluido explícitamente en el tipo de la propiedad. ¡Hurra!

Pero no es parte del conjunto de opciones del compilador --strict. Quizás si esta función se hubiera introducido con --strictNullChecks en TypeScript 2.0, lo habría sido. Pero mucho código del mundo real actualmente espera que las propiedades opcionales acepten undefined, y se rompería si se introdujera esto. Y podría ser molesto. La simple copia de una propiedad de un objeto a otro del mismo tipo, como a.x=b.x, de repente ya no es aceptable si la propiedad es opcional. Ahora necesitas if ("x" in b) {a.x=b.x} else {delete a.x}. Eso es un obstáculo extra que superar, y si realmente no te importa la distinción entre ausente/undefined, no vas a estar contento con esto.

Así que esas son propiedades opcionales.


Ahora, sobre las firmas de índice: en modo --strict, TypeScript realmente no distingue entre propiedades faltantes y las que están presentes. Esto realmente no importa al escribir, pero al leer es notable. El compilador presume que si estás leyendo desde una clave que coincide con la firma de índice, entonces sabes que un valor realmente está ahí. No toma en cuenta la posibilidad de que no haya un valor de propiedad y que obtendrás undefined. Si quisieras hacer lecturas más seguras, podrías agregar manualmente | undefined al tipo, convirtiendo {[x: string]: number} en {[x: string]: number | undefined}, pero ahora estarías cambiando el problema de lectura insegura por un problema de escritura indeseable, muy parecido al de las propiedades opcionales. Por defecto, TypeScript dice que si lees desde una firma de índice, obtendrás un valor definido (desreferencia bajo tu propio riesgo).

¿No te gusta eso? Bueno, a muchas otras personas tampoco les gustó... microsoft/TypeScript#13778, "Opción para incluir undefined en las firmas de índice", estuvo abierta durante mucho tiempo y recibió cientos de votos positivos. Y así, con TypeScript 4.1 se introdujo la opción del compilador --noUncheckedIndexedAccess. Si la activas, ahora recibirás un valor posiblemente undefined cuando leas desde una firma de índice, pero no puedes escribir uno allí. ¡Hurra!

Pero no forma parte del conjunto de opciones del compilador --strict. Tal vez si esta característica se hubiera introducido con --strictNullChecks en TypeScript 2.0, lo habría sido. Pero mucho del código del mundo real actualmente espera que las propiedades de la firma de índice estén presentes, y se rompería si esto se introdujera. Y podría ser molesto. La simple iteración a través de un arreglo por índice, como for (let i=0; i < arr.length; i++) { sum += arr[i]*arr[i] }, de repente no es aceptable. Ahora necesitas for (let i=0; i < arr.length; i++) { const v=arr[i]; if (v !== undefined) { sum += v * v } }. Eso es un obstáculo adicional que superar y si no usas firmas de índice con claves arbitrarias, no te va a gustar.

Así que esas son las firmas de índice.


Y tú estás combinándolos. Bueno, incluso con --exactOptionalPropertyTypes y --noUncheckedIndexedAccess habilitados, Partial<Record<string, number>> va a incluir explícitamente undefined y, por lo tanto, te permitirá asignar undefined a sus valores de propiedad:

// @exactOptionalPropertyTypes: true
// @noUncheckedIndexedAccess
type X = Partial<Record<string, number>>;
// type X = { [x: string]: number | undefined; }

const x: X = {};
x.hello = undefined; // no error!

Esto es... un error, supongo? ¿O una limitación de diseño? Está registrado en microsoft/TypeScript#46969 "exactOptionalPropertyTypes: Partial de la firma de índice agrega undefined" y marcado como "Necesita investigación" desde TypeScript 4.8.

Así que no importa lo que estés buscando, probablemente no quieras usar Partial<{[k: string]: XXX}> en absoluto.

En su lugar, deberías pensar en qué comportamiento deseas dado las opciones disponibles mencionadas arriba y sus compensaciones. Si planeas solo iterar sobre las propiedades de tu objeto, entonces yo diría que deberías usar simplemente Record<string, number> (sin Partial) y dejar las opciones predeterminadas de --strict. Si planeas acceder a las propiedades de tu objeto con claves arbitrarias, entonces deberías decidir entre las configuraciones predeterminadas o agregar | undefined manualmente (aceptando que los valores undefined pueden aparecer cuando iteres) o habilitar --noUncheckedIndexedAccess y hacer que cada firma de índice en tu base de código comience a preocuparse mucho por undefined. Depende de ti.

Enlace de Playground al código

Artículos relacionados