¿Por qué ciertos tipos básicos no son asignables de forma transitiva en TypeScript - están funcionando como se espera?
(Originalmente, queriendo abrir un informe de errores en el rastreador de problemas del repositorio de TypeScript, me di cuenta de que había hecho demasiadas preguntas, así que estoy abriendo una pregunta aquí antes del informe de errores.)
Enlace del playground con el código relevante
type Foo0 = {} extends Record<string, unknown> ? true : false;
// ^? [type Foo0 = true]
type Foo1 = Record<string, unknown> extends {} ? true : false;
// ^? [type Foo1 = true]
type Bar0 = {} extends object ? true : false;
// ^? [type Bar0 = true]
type Bar1 = object extends {} ? true : false;
// ^? [type Bar1 = true]
type Baz0 = Record<string, unknown> extends object ? true : false;
// ^? [type Baz0 = true]
type Baz1 = object extends Record<string, unknown> ? true : false;
// ^? [type Baz1 = false]
Pregunta editada: Supongo que lo he entendido. En Foo0 y Bar0, {} representa “un objeto vacío (un registro sin ningún valor)”, por lo que es asignable tanto a object (que representa “cualquier no primitivo”) como a Record<string, unknown> (que es equivalente a { [key: string]: unknown }). Por otro lado, {} en Foo1 y Bar1 representa “cualquier valor no nulo o indefinido”, por lo que tanto object como Record<string, unknown> son asignables a él. Así que supongo que mi pregunta sería:
¿Por qué
{}representa dos cosas diferentes? ¿Es intencional? Si es por razones históricas, ¿hay alguna posibilidad de que estas dos cosas se separen?
Señálalos si me equivoco en algo.
Respuesta #1
No entiendo TypeScript lo suficiente como para resolver esto, pero ofreceré algunas observaciones a partir de las cosas que logré investigar.
Primero que nada: object es cualquier cosa no primitiva. Así que, por ejemplo, las funciones son objects y las cadenas no lo son:
let bar1: object = alert // typechecks
let bar2: object = 'a' // doesn't
La propiedad especial de {}, por otro lado, es "asignable desde cualquier valor que no sea nulo/indefinido": https://gist.github.com/OliverJAsh/381cd397008309c4a95d8f9bd31adcd7?permalink_comment_id=2890808#gistcomment-2890808
{} es el tipo vacío y se le puede asignar cualquier tipo que no sea nulo ni indefinido. Recuerda la tipificación estructural aquí: Reducir el número de propiedades en un tipo (debería) nunca disminuir el número de tipos que se le pueden asignar (n.b. la verificación de propiedades excesivas no es un factor en la asignabilidad; es una verificación aparte).
Observe alguna simetría problemática aquí: una cadena debería poder asignarse a { length: number }, y para cualquier tipo { r0; r1; r2...}, un valor asignable a ese tipo también debería poder asignarse a { r1; r2...}. Así que si una cadena puede asignarse a { length: number } (lo cual absolutamente debería), entonces la cadena también debe poder asignarse a { }, aunque no sea un objeto.
Así que aquí está la diferencia entre {} y object, ilustrada:
let bar0: {} = 'a' // typechecks
let bar1: object = 'a' // doesn't
Esto significa que una parte demostrablemente sospechosa es {} extends object:
let bar0: {} = 'a'
let bar1: object = {}
bar1 = bar0 // shouldn't work but it does
https://github.com/microsoft/TypeScript/pull/49119 trata sobre la otra dirección: la asignabilidad de cosas a {}. Busqué pero no he encontrado nada sobre asignar {} a cosas.
No estoy seguro de si TS realmente se preocupa por mantener el contrato de que un primitivo (como string) no pueda terminar en un object. Si le importa, entonces, como mencionaste, debería haber un false en cada uno de los dos primeros pares.
Pero está bien. Digamos que no le importa.
La segunda parte dudosa es la disparidad entre:
{} extends Record<string, unknown> = true
object extends Record<string, unknown> = false
¿Qué significa realmente una firma de índice en TypeScript? podría dar una pista sobre por qué el segundo es false.
He pasado la última hora tratando de entender las firmas de índice (que son usadas por Record) y no puedo idear una semántica clara para ellas que tenga sentido.
Quizás la única buena explicación sea "TS quiere que te quejes si es muy obvio que estás a punto de inyectar claves/valores de tipo incorrecto, pero de lo contrario no protestará demasiado". Así que object es una especie de montón de basura que no debería terminar en un {[key: string]: unknown}, pero {} está bien aunque potencialmente también sea absolutamente un montón de basura.
Ojalá las reglas de verificación de tipos de TS estuvieran realmente escritas en algún lugar.
