Let's say I have a type definition like this:
type Animal =
| {
type: "cat";
owner:
| { type: "human"; name: string }
| { type: "another_cat"; nickname: string };
}
| {
type: "dog";
owner: { type: "human" };
};
I know that I can create a type specific to the cat shape using the Extract
utility:
type Cat = Extract<Animal, { type: "cat" }>;
Am I able to also define a type based on a nested value? Say I wanted to define a type for the shape where type
is cat
and owner.type
is human
. I've tried this:
type CatOwnedByHuman = Extract<Cat, { owner: { type: "human" } }>;
But that leads to the type of never
.
CodePudding user response:
You are able to use the Extract
utility type to pick out branches from discriminated unions. However, as you have seen, it can be troublesome in some cases such as when you want nested objects. The reason for this is because the Extract
type relies on the distributive behavior of conditional clauses so it therefore distributes through each branch of T
and checks if it extends F
. If it does, then it returns T
. However, this would only work for the top-level unions. Therefore, you would need to have a recursive way to check for the nested unions. Here's an example showing how Extract
works:
// type Extract<T, U> = T extends U ? T : never; // iterates through each branch `T`
type Foo = Extract<string | number, number>;
// ^? (string extends number ? string : never) | (number extends number ? number : never)
// ^? never | number
// ^? number
A possible solution is not using the Extract
utility type and instead using an intersection on the property values that you want which will pull out the branches of a union that match it because the other branches would result in never
and therefore be omitted:
type Animal =
| {
type: "cat";
owner:
| { type: "human"; name: string }
| { type: "another_cat"; nickname: string };
}
| {
type: "dog";
owner: { type: "human" };
};
type Cat = Animal & { type: "cat" };
/*
type Cat = {
type: "cat";
owner: {
type: "human";
name: string;
} | {
type: "another_cat";
nickname: string;
};
} & {
type: "cat";
};
*/
Now, when you want to match a nested object's properties, you can do using the same simple syntax:
type Animal =
| {
type: "cat";
owner:
| { type: "human"; name: string }
| { type: "another_cat"; nickname: string };
}
| {
type: "dog";
owner: { type: "human" };
};
type Cat = Animal & { type: "cat"; owner: { type: "human" } };
/*
type Cat = {
type: "cat";
owner: {
type: "human";
name: string;
} | {
type: "another_cat";
nickname: string;
};
} & {
type: "cat";
owner: {
type: "human";
};
};
*/
/* COMPUTES TO:
type Cat = {
type: "cat";
owner: {
type: "human";
name: string;
};
};
*/