pfgray / ts-adt

Generate Algebraic Data Types and pattern matchers
MIT License
316 stars 13 forks source link

Better type inference on result type. #7

Closed pfgray closed 3 years ago

pfgray commented 3 years ago

The issue

Currently the inference is not quite great in pipes:

import { ADT, match } from 'ts-adt'
import { pipe } from 'fp-ts/function'

type Option<A> = ADT<{
  some: {value: A},
  none: {}
}>

declare const foo: Option<number>

const result = pipe(
  foo,
  match({
    some: () => 'foo',
    none: () => 'baz'
  })
)

Here, result is inferred as unknown, where is should be at least string.

The solution

A potential solution is to compute the return type with a mapped type:

/**
 * Unions all the return types of matcher functions
 */
type Returns<
  ADT extends { _type: string },
  M extends MatchObj<ADT, unknown>
> = {
  [K in keyof M]: ReturnType<M[K]>;
}[keyof M];

export function match<
  ADT extends { _type: string },
  M extends MatchObj<ADT, unknown>
>(matchObj: M): (v: ADT) => Returns<ADT, M> {
  return (v) => (matchObj as any)[v._type](v);
}

and now the match infers as you would expect:

const result = pipe(
  foo,
  match({
    some: () => 'foo',
    none: () => 'baz'
  })
)

Here, result infers as string.

Another consequence of this approach is that it's easy to sneak in different return types:

const result = pipe(
  foo,
  match({
    some: () => 'foo',
    none: () => 42
  })
)

Here, result is inferred as string | number (previously if would fail to typecheck, with the error "number" is not assignable to "string". I could see an argument either way, but I think it provides the most convenience, while not suffering any type-safety.