Home > OS >  Defining TypeScript type by value of nested property
Defining TypeScript type by value of nested property

Time:04-06

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";
};
*/

TypeScript Playground Link

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;
    };
};
*/

TypeScript Playground Link

  • Related