How to filter union of object type by property in Typescript?

Viewed 2995

Imagine the following simplified setup:

import { Action, AnyAction } from 'redux'; // interface Action<Type> { type: Type } and type AnyAction = Action<any>

export type FilterActionByType<
  A extends AnyAction,
  ActionType extends string
> = A['type'] extends ActionType ? A : never;

type ActionUnion = Action<'count/get'> | Action<'count/set'>;

type CountGetAction = FilterActionByType<ActionUnion, 'count/get'>;
// expected: Action<'count/get'>
// received: never

Is there a way to accomplish this? (typescript 3.7 is an option)

3 Answers

You want FilterActionByType<A, T> to take a union type A and act on each member separately, and then take all the results and unite them in a new union... that means you want FilterActionByType<A, T> to distribute over unions (at least in A). You can use distributive conditional types to do this, by making sure that your conditional type is of the form type FilterActionByType<A, T> = A extends .... Having the bare A as the checked type triggers the distribution you want.

But: your conditional type is of the form type FilterActionByType<A, T> = A["type"] extends ..., in which A is "clothed" by the property lookup, and so is not distributive. That means the A["type"] extends ActionType takes the whole union value for A, namely (in your case) ActionUnion. And ActionUnion["type"] extends "count/get" becomes ("count/get" | "count/set") extends "count/get", which is false. (X extends (X | Y) is always true, but (X | Y) extends X is not true in general.) So you get never.


The simplest way to change what you have into a distributive conditional type is just to wrap your definition in A extends any ? ... : never:

export type FilterActionByType<
  A extends AnyAction,
  ActionType extends string
> = A extends any ? A['type'] extends ActionType ? A : never : never;

type ActionUnion = Action<'count/get'> | Action<'count/set'>;

type CountGetAction = FilterActionByType<ActionUnion, 'count/get'>;
// type CountGetAction = Action<"count/get">

Or you could refactor your original conditional type to be distributive without wrapping it:

export type FilterActionByType<
  A extends AnyAction,
  ActionType extends string
> = A extends { type: ActionType } ? A : never;

type CountGetAction = FilterActionByType<ActionUnion, 'count/get'>;
// type CountGetAction = Action<"count/get">

The check A extends {type: ActionType} is the distributive version of A["type"] extends ActionType.

Either way should work for you, but the latter is probably cleaner.


Okay, hope that helps; good luck!

Link to code

The best solution from Piotr Lewandowski :

type FilterFlags<Base, Condition> = {
    [Key in keyof Base]: 
        Base[Key] extends Condition ? Key : never
};
type AllowedNames<Base, Condition> = 
        FilterFlags<Base, Condition>[keyof Base];
type SubType<Base, Condition> = 
        Pick<Base, AllowedNames<Base, Condition>>;

Extract<T, U> can be very useful for this and can be a lot simpler than a conditional type for many cases. You can think of it as Filter for this purpose:

// cat meows but doesn't bark
type Cat = { meow: true, bark: false };

// dog and coyote bark
type Coyote = { bark: true };
type Dog = { bark: true };

// all the animals
type Animal = Cat | Dog | Coyote;

// Filter / Extract Animal types that are assignable to { bark: true }
type Barkers = Extract<Animal, { bark: true }>;


// Result: 
// type Barkers == Dog | Coyote

Probably less likely to be useful for OP question, but there's some cool functionality for filtering in mapped types too (from TS 4.1).

Related