Issue
I have prepared the demo (reproduction link)
Here is my question:
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");
}
}
How come when I use
===checks against strings in theif, typescript is able to distinguish the types, and knows exactly what variants take under consideration? But when I useArray.includesas a check in theifit doesn't?How can I make it work with
Array.includescheck in theif?
Solution
The reason why the series of equality checks like target === "address_line" works for you is because equality checks are one of the constructs that TypeScript uses to narrow the types of the expressions being checked. That is, x === y can act as a type guard on x and y.
In general it is possible for function calls or method calls to act as type guards, but to do so they need to be annotated so their return type is a type predicate of the form arg is Type where arg is the name of a function parameter (or this), and where Type is some type narrower than typeof arg. But the standard TypeScript library typings for the Array.prototype.includes() method is not annotated this way. It's not a type guard function.
Why not? It has been suggested, at microsoft/TypeScript#36275, but this suggestion was declined because it would be too complicated to do so correctly. Quite often, people call array.includes(value) without trying to narrow the type of value. And if they do want that, they wouldn't necessarily be happy with the inverse, where !array.includes(value) implies that value is not the type of an array element. If you have an array of strings like ["a", "b", "c"], the fact that "z" isn't in the array doesn't mean that "z" isn't a string (!). So care would need to be taken to implement a type guard that doesn't have very strange effects.
Your options here are:
You could write your own standalone type guard function which calls Array.prototype.includes() inside of it, but doesn't try to affect how the compiler sees Array.prototype.includes() in general. That's the safest thing to do, because you can control when your type guard function gets called:
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
}
}
So includes() is a standalone function which accepts an array of strings arr and a target string val, and returns val is T, where T is intended to be a union of string literal types inferred from the contents of arr.
You can see that it works as desired.
Or, you could decide to define a call signature for Array.prototype.includes() that makes it act as a type guard, and merge it into your code base:
// 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;
}
// }
That will make it so that any time you call array.includes(value) where array whose elements are a subtype U of string (but crucially not string itself), and where value is a string, then a true result means that value is of type U, and a false result means that value is not of type U. This is fairly complicated (which is why they didn't do it in microsoft/TypeScript#36275) and possibly fragile, so you might need to make sure it doesn't causes problems for your code base elsewhere.
Anyway, this will make your code work more or less as-is:
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
}
}
Except that the array literal needs that const assertion (["a", "b", "c"] as const) in order for the compiler to realize that the type of that array should be narrower than just string[]. A string[] array wouldn't be helpful with narrowing.
Merging that in is the closest you can probably get to your desired state where your original code "just works", but I'd stay away from it and prefer the standalone type guard function. There's little chance someone will call includes(arr, val) in some other part of your code, whereas Array.prototype.includes() is much more likely to be employed elsewhere.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.