javascript - Operador de igualdad en Typescript vs comprobaciones con Array.includes
He preparado la demostración (enlace de reproducción)
Aquí está mi pregunta:
interface Person {
target:
| "first_name"
| "last_name"
| "date_of_birth"
| "address_line"
| "address_line2"
| "city"
| "state"
| "zip";
}
interface Address {
address_line?: string | null;
address_line2?: string | null;
state?: string | null;
city?: string | null;
zip?: string | null;
}
interface Owner {
id: number;
first_name: string;
last_name: string;
date_of_birth: string | null;
address: Address
}
function createNewOnwer(target: Person["target"]) {
const newData = {} as Partial<Owner>;
if (
target === "address_line" ||
target === "address_line2" ||
target === "zip" ||
target === "state" ||
target === "city"
) {
// hover on target
newData.address = {
[target]: "test address" // typescript knows exactelly that target here is one of "address_line" | "address_line2" | "zip" | "state" | "city"
};
} else {
newData[target] = String("test address"); // and here one of "first_name" | "last_name" | "date_of_birth"
}
}
function createNewOnwerWithIncludeCheck(target: Person["target"]) {
const newData = {} as Partial<Owner>;
if (
["address_line", "address_line2", "zip", "state", "city"].includes(target)
) {
// hover on target
newData.address = {
[target]: "test address" // ⬅️ why it doesn't know here that it is still one of "address_line" | "address_line2" | "zip" | "state" | "city" ??? 🤔
};
} else {
newData[target] = String("test address"); // Why typescript throws error here?
}
}
function createNewOnwerWithIncludeCheckWithHardcodedTypes(target: Person["target"]) {
const newData = {} as Partial<Owner>;
if (
["address_line", "address_line2", "zip", "state", "city"].includes(target)
) {
// hover on target
newData.address = {
[target as "address_line" | "address_line2" | "zip" | "state" | "city"]: "test address" // still thinks it is one of all Person["target"]
};
} else {
newData[target as "first_name" | "last_name" | "date_of_birth"] = String("test address");
}
}
¿Por qué cuando uso comprobaciones
===contra cadenas en elif, TypeScript puede distinguir los tipos y sabe exactamente qué variantes se están considerando? Pero cuando usoArray.includescomo comprobación en elif, ¿no lo hace?¿Cómo puedo hacer que funcione con la comprobación
Array.includesen elif?
Respuesta #1
La razón por la que la serie de verificaciones de igualdad como target === "address_line" funciona para ti es porque las verificaciones de igualdad son uno de los constructos que TypeScript usa para reducir los tipos de las expresiones que se están verificando. Es decir, x === y puede actuar como un guardián de tipo sobre x y y.
En general, es posible que las llamadas a funciones o métodos actúen como guardianes de tipo, pero para hacerlo deben estar anotadas de manera que su tipo de retorno sea un predicado de tipo de la forma arg is Type, donde arg es el nombre de un parámetro de la función (o this), y donde Type es algún tipo más específico que typeof arg. Pero las definiciones de la biblioteca estándar de TypeScript para el método Array.prototype.includes() no están anotadas de esta manera. No es una función guardián de tipo.
¿Por qué no? Se ha sugerido en microsoft/TypeScript#36275, pero esta sugerencia fue rechazada porque sería demasiado complicado hacerlo correctamente. Muy a menudo, las personas llaman a array.includes(value) sin intentar limitar el tipo de value. Y si de hecho quisieran eso, no necesariamente estarían contentos con lo inverso, donde !array.includes(value) implica que value no es del tipo de un elemento del array. Si tienes un array de cadenas como ["a", "b", "c"], el hecho de que "z" no esté en el array no significa que "z" no sea un string (!). Por lo tanto, habría que tener cuidado al implementar un guardia de tipo que no tenga efectos muy extraños.
Tus opciones aquí son:
Podrías escribir tu propia función de protección de tipos independiente que llame a Array.prototype.includes() dentro de ella, pero que no intente afectar cómo el compilador ve Array.prototype.includes() en general. Eso es lo más seguro, porque puedes controlar cuándo se llama a tu función de protección de tipos:
function includes<T extends string>(arr: readonly T[], val: string): val is T {
return (arr as readonly string[]).includes(val);
}
function createNewOnwerWithIncludeCheck(target: Person["target"]) {
const newData: Partial<Owner> = {};
if (includes(["address_line", "address_line2", "zip", "state", "city"], target)) {
newData.address = { [target]: "test address" };
} else {
newData[target] = String("test address"); // okay
}
}
Entonces includes() es una función independiente que acepta un arreglo de cadenas arr y una cadena objetivo val, y devuelve val es T, donde T está destinado a ser una unión de tipos literales de cadena inferidos a partir del contenido de arr.
Puedes ver que funciona como se desea.
O, podrías decidir definir una firma de llamada para Array.prototype.includes() que haga que actúe como un guardia de tipos, y fusionarla en tu base de código:
// declare global { // uncomment this if your code is in a module
interface ReadonlyArray<T> {
includes<U extends (string extends U ? never : string)>(
this: ReadonlyArray<U>, val: string): val is U;
}
interface Array<T> {
includes<U extends (string extends U ? never : string)>(
this: Array<U>, val: string): val is U;
}
// }
Eso hará que cada vez que llames a array.includes(value) donde array cuyos elementos son un subtipo U de string (pero crucialmente no string en sí), y donde value es un string, entonces un resultado true significa que value es del tipo U, y un resultado false significa que value no es del tipo U. Esto es bastante complicado (por lo cual no lo hicieron en microsoft/TypeScript#36275) y posiblemente frágil, por lo que podrías necesitar asegurarte de que no cause problemas en tu base de código en otros lugares.
De todos modos, esto hará que tu código funcione más o menos tal como está:
function createNewOnwerWithIncludeCheck2(target: Person["target"]) {
const newData: Partial<Owner> = {};
if ((["address_line", "address_line2", "zip", "state", "city"] as const).includes(target)) {
newData.address = { [target]: "test address" };
} else {
newData[target] = String("test address"); // okay
}
}
Excepto que el literal de matriz necesita esa afirmación de const (["a", "b", "c"] as const) para que el compilador se dé cuenta de que el tipo de esa matriz debería ser más específico que solo string[]. Una matriz string[] no sería útil para estrechar el tipo.
Fusionar eso es probablemente lo más cercano que puedes llegar a tu estado deseado donde tu código original "simplemente funciona", pero yo me mantendría alejado de eso y preferiría la función de guardia de tipo independiente. Hay pocas posibilidades de que alguien llame a includes(arr, val) en alguna otra parte de tu código, mientras que Array.prototype.includes() es mucho más probable que se utilice en otros lugares.
Enlace de Playground al código
