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.includes
as a check in theif
it doesn't?How can I make it work with
Array.includes
check 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.