Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)

like image 210
Mateja Petrovic Avatar asked Dec 13 '22 10:12

Mateja Petrovic


2 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

like image 125
jcalz Avatar answered Feb 02 '23 04:02

jcalz


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>>;
like image 30
Charles-Lévi BRIANI Avatar answered Feb 02 '23 04:02

Charles-Lévi BRIANI