Generic match function for discriminated union types in Typescript
Asked Answered
F

1

9

Is it possible to define something like a generic match function over discriminated union type? Let's say we have the following type definitions:

const Kinds = {
  A: 'A',
  B: 'B',
};
type Kind = typeof Kinds.A | typeof Kinds.B;
type Value = A | B;
interface A {
  kind: Kinds.A
}
interface B {
  kind: Kinds.B
}

With switch statement it is possible to define match function like:

interface Matcher<T> {
  Kinds.A: (value: A) => T
  Kinds.B: (value: B) => T
}
function match<T>(matcher: Matcher<T>) {
  return function(value: Value) {
    switch (value.kind) {
      case Kinds.A: return matcher[Kinds.A](value);
      case Kinds.B: return matcher[Kinds.B](value);
    }
  }
}

It does the job, but it is very tedious to define such functions especially when one have many union members.

Is it possible to simplify this definition somehow, maybe with Mapped Types or other existing means from the latest 2.1 branch.

I was playing around with "Mapped Types", but I'm not sure that it is actually possible to get concrete Value even when I know Kind, e.g something like:

type Matcher<T> = {[P in Kind]: (value: P) => T};
function match<T>(matcher: Matcher<T>) {
  return function(value: Value) {
    return matcher[value.kind](value);
  }
}

but where one can actually translate P to corresponding Value type.

Frailty answered 26/11, 2016 at 21:34 Comment(0)
L
1

The key to this is being able to get a member of a union based on its kind. That can be done with a type like this:

type UnionMemberByKind<K> = Extract<Union, { kind: K }>

Extract<T, U> returns the members of T that match U: in this case it's going to return the single member of the union with the specified kind.

With that type, you can properly build your matcher object:

type Matcher<Res> = {
    [P in Union["kind"]]: (value: UnionMemberByKind<P>) => Res
}

And then define your match function basically as before:

function match<T>(matcher: Matcher<T>) {
    return function(value: Union) {
        return matcher[value.kind](value as any);
    }
}

(The as any cast is unfortunate, but I can't find a way to avoid it)

Longdrawn answered 24/2, 2019 at 5:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.