typescript - ¿Cómo declarar un objeto tipado con claves de cadena arbitrarias? - ¿Falta la firma de índice?
Necesito declarar un objeto que tenga claves de tipo string arbitrarias y valores de tipo number o string, es decir, algo como en esta pregunta.
Así que escribí
interface AnyObjectWithNumberAndStringValues {
[s: string]: number | string;
}
y una función de prueba
function f(x: AnyObjectWithNumberAndStringValues) {
console.log(x);
}
y funciona más o menos. Puedo llamar
f({ id: 123, name: "noname" })
Pero ahora, tengo una interfaz y un objeto como
interface ISupplier {
id: number;
name: string;
}
const o: ISupplier = { id: 123, name: "noname" };
y llamar a f(o) produce el siguiente error:
El argumento de tipo 'ISupplier' no se puede asignar al parámetro de tipo 'AnyObjectWithNumberAndStringValues'. Falta la firma de índice en el tipo 'ISupplier'.ts(2345)
- ¿Alguien puede explicar por qué?
- ¿Cómo puedo solucionar el problema?
Más precisamente: ¿Cómo puedo declarar que f acepta cualquier objeto siempre que sus valores sean solo números y cadenas?
Consulta este sandbox para un ejemplo completo. Ten en cuenta que parece ser no determinista: ¡El error puede o no aparecer!
Respuesta #1
Definitivamente es confuso averiguar qué tipos se pueden asignar a y desde un tipo indexable. La razón por la que
f({ id: 123, name: "noname" }); // okay
funciona porque el argumento es un literal de objeto, al cual se le da una firma de índice implícita. En el momento en que esto ocurre, el compilador sabe que el parámetro es un literal de objeto y, por lo tanto, no tiene ninguna propiedad desconocida. Por lo tanto, es seguro tratarlo como algo asignable a AnyObjectWithNumberAndStringValues.
La razón por la cual
const o: ISupplier = { id: 123, name: "noname" };
f(o); // error
no funciona porque a o se le da una anotación de tipo de ISupplier. Para cuando se llama a f(o), el compilador ha olvidado las propiedades específicas dentro de o. Para todo lo que sabe el compilador, puede haber propiedades desconocidas que no sean string ni number allí.
Considera el siguiente código:
interface ISupriser extends ISupplier {
surprise: boolean;
}
const s: ISupriser = { id: 123, name: "noname", surprise: true };
const uhOh: ISupplier = s;
f(uhOh); // error makes sense now
Un ISurpriser contiene una propiedad boolean, por lo que no querrías llamar a f() con él. Pero cada ISurpriser también es un ISupplier. La variable uhOh es tan válida como ISupplier como lo era tu o. Y todo lo que el compilador recuerda sobre ambos, o y uhOh, es que son instancias de ISupplier. Por eso f(o) falla... es para evitar que llames accidentalmente a f(uhOh).
Tenga en cuenta que cualquier solución alternativa que haga que pueda llamar a f() o a una función similar sobre algo que no sea un literal de objeto tiene el potencial de permitir argumentos inesperados, con propiedades incorrectas que el compilador no conoce (o no recuerda). Pero veamos estas soluciones alternativas de todos modos:
Se ha observado que una solución temporal es usar un alias de tipo para ISupplier en lugar de una interfaz. Aparentemente, la desventaja aquí es que no es tan fácil extender alias de tipo (puedes obtener el mismo efecto con intersecciones), por lo que es "lo suficientemente seguro" considerarlo como si tuviera una firma de índice implícita:
type TSupplier = {
id: number;
name: string;
};
const p: TSupplier = { id: 123, name: "noname" };
f(p); // okay
Podría no ser "tan fácil" agregar propiedades indeseables como lo es con una interfaz, pero todavía es lo suficientemente fácil:
const ohNo: TSupplier = uhOh; // okay
f(ohNo); // no error, but there probably should be one!
Otra solución alternativa, especialmente si no puedes cambiar ISupplier, es usar un tipo genérico restringido en tu función en lugar de una firma de índice, así:
function g<T extends Record<keyof T, number | string>>(x: T) {
console.log(x);
}
La función g() básicamente solo acepta argumentos cuyos propiedades conocidas sean asignables a number | string:
g({ id: 123, name: "noname" }); // okay
g(o); // okay
g(s); // error, "surprise" is incompatible
Sin embargo, nuevamente, si el compilador no conoce una propiedad, no puede verificarla, por lo que estos tampoco tienen error:
g(uhOh); // no error, but there probably should be one
g(ohNo); // no error, but there probably should be one
De todos modos, espero que eso te ayude. ¡Buena suerte!
Enlace al código
