Búsqueda de sitios web

typescript - Validar claves excedentes en el valor devuelto por una función


Digamos que estoy haciendo esto:

type Keys = 'a' | 'b' | 'c'
type Rec = { [K in Keys]?: number }
let rec: Rec = { a: 1, d: 4 }

Esto resulta en:

Type '{ a: number; d: number; }' is not assignable to type 'Rec'.
  Object literal may only specify known properties, and 'd' does not exist in type 'Rec'

Así que no permite ninguna clave extra en un objeto.

Pero si hago lo siguiente:

type Func = () => Rec
const fn: Func = () => ({ a: 1, d: 4 })

TS está totalmente de acuerdo con ello, a pesar de que la función dada definitivamente no devuelve un tipo Rec.

Y, al mismo tiempo, usar const fn: Func =() => ({ a: false, d: 4 }) da

Type 'boolean' is not assignable to type 'number | undefined'.(2322)
input.tsx(63, 12): The expected type comes from property 'a' which is declared here on type 'Rec'

Así que en realidad sí valida un valor devuelto. Pero de alguna manera no le importan las claves adicionales.

Demostración

¿Por qué ocurre esto, y hay alguna manera de no permitir claves extra en el valor devuelto en este caso?

Respuesta #1

Tenga en cuenta que {a: 1, d: 4} es del tipo Rec. Los tipos de objetos en TypeScript generalmente permiten propiedades excedentes y no son "exactos" como se solicita en microsoft/TypeScript#12936. Hay buenas razones para esto relacionadas con la subtipificación y la asignabilidad. Por ejemplo:

class Foo {a: string = ""}
class Bar extends Foo {b: number = 123}
console.log(new Bar() instanceof Foo); // true

Tenga en cuenta que cada Bar es un Foo, lo que significa que no se puede decir "todos los objetos Foo solo tienen una propiedad a" sin impedir la herencia y extensión de clases o interfaces. Y dado que interface funciona de la misma manera, y dado que el sistema de tipos de TypeScript es estructural y no nominal, ni siquiera tienes que declarar un tipo Bar para que exista:

interface Foo2 {a: string};
// interface Bar2 extends Foo2 {b: number};
const bar2 = {a: "", b: 123 };
const foo2: Foo2 = bar2; // okay

Así que, para bien o para mal, estamos atrapados con un sistema de tipos en el que las propiedades adicionales no rompen la compatibilidad de tipos.


Por supuesto, esto puede ser una fuente de errores. Así que, en el caso en que esté asignando explícitamente un nuevo literal de objeto a un lugar que espera un tipo de objeto particular, hay comprobaciones de propiedades excesivas que se comportan como si el tipo fuera exacto. Estas comprobaciones solo ocurren en circunstancias particulares, como en su primer ejemplo:

let rec: Rec = { a: 1, d: 4 }; // excess property warning

Pero los valores de retorno de las funciones actualmente no son una de estas circunstancias. El tipo del valor de retorno se amplía antes de que puedan ocurrir verificaciones de propiedades excesivas. Hay un problema abierto bastante antiguo en GitHub, microsoft/TypeScript#241, que sugiere que esto debería cambiarse para que los valores de retorno de las funciones no se amplíen de esta manera, e incluso hay una implementación de una posible solución en microsoft/TypeScript#40311, pero fue cerrada, así que podría nunca llegar al lenguaje.


No existen formas perfectas de suprimir propiedades en exceso en general. Mi consejo es simplemente aceptar que los objetos pueden tener claves en exceso y asegurarse de que cualquier código que escriba no se rompa si este es el caso. Puede hacer cosas que desalienten las propiedades en exceso, como estas:

// annotate return type explicitly
const fn2: Func = (): Rec => ({ a: 1, d: 4 }) // excess property warning

// use a generic type that gets mad about excess properties
const asFunc = <T extends Rec & Record<Exclude<keyof T, keyof Rec>, never>>(
    cb: () => T
): Func => cb;
const fn3 = asFunc(() => ({ a: 1, d: 4 })); // error! number is not never

Pero son más complicados y se rompen fácilmente, ya que nada en absoluto te impedirá hacer esto sin importar cuánto intentes proteger tu tipo Func:

const someBadFunc = () => ({ a: 1, d: 4 });
const cannotPreventThis: Rec = someBadFunc();

Escribir código que anticipa propiedades adicionales generalmente implica mantener un arreglo de claves conocidas. Así que no hagas esto:

function extraKeysBad(rec: Rec) {
    for (const k in rec) { 
        const v = rec[k as keyof Rec];  // bad assumption that k is keyof Rec 
        console.log(k + ": " + v?.toFixed(2))
    }
}

const extraKeys = {a: 1, b: 2, d: "four"};
extraKeysBad(extraKeys); // a: 1.00, b: 2.00, RUNTIME ERROR! v.toFixed not a function

Haz esto en su lugar:

function extraKeysOkay(rec: Rec) {
    for (const k of ["a", "b", "c"] as const) {
        const v = rec[k];
        console.log(k + ": " + v?.toFixed(2))
    }
}

extraKeysOkay(extraKeys); // a: 1.00, b: 2.00, c: undefined

Enlace de Playground al código

Artículos relacionados