pfgray / ts-adt

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

Added support for using underscore for partial matchers. #5

Closed rjdestigter closed 3 years ago

rjdestigter commented 4 years ago
// example.ts

type PrettyPlease<A> = ADT<{
  idle: {};
  workingOnIt: {};
  success: { value: A };
  failure: { error: Error };
}>;

declare const promise: PrettyPlease<"Chocolate">;

matchI(promise)({
  success: (_) => _.value,
  _: () => "Wait or come back later.",
});

I've also added a test file to ensure the type safety persisted. type ADT will also remove underscore types to prevent conflicts. I upgraded TypeScript so that I could make use of @ts-expect-error in the test file.

ryanleecode commented 3 years ago

underscore matcher should include the adt as its argument. useful when using match and you're not using a named argument for the adt.

pfgray commented 3 years ago

@rjdestigter , many, many apologies... I have not seen this until just now! But this idea is awesome 💯

I may change it have specific functions for partiality:

matchP({
})
matchPI(promise)({
})

just to keep backwards compatibility for people. I also wonder how hard it would be to have the argument be the union of all the types not accounted for, i.e.:

matchI(promise)({
  success: s => ...,
  _: (rest: idle | workingOnIt | failure) => 
})

cheers 👍

pfgray commented 3 years ago

@ryanleecode I'm not sure what you mean here, could you clarify? @rjdestigter 's changes affect both match and matchI

matchI allows you to supply the adt instance as the first parameter, whereas match has it come last (which allows you to use it in a point-free context, which I think is what you're talking about?)

ryanleecode commented 3 years ago

U covered what I meant here.

"I also wonder how hard it would be to have the argument be the union of all the types not accounted for, i.e.:"

rjdestigter commented 3 years ago

@pfgray I added matchP and matchPI. I can't figure out how to pass the union of all unaccounted cases.

rjdestigter commented 3 years ago

I think the only way to identify the union of all the types not accounted for is to curry the solution maybe?:

export function matchPI2<ADT extends { _type: string }>(
  v: ADT
): <Z, TPartial extends Partial<MatchObj<ADT, Z>>>(
  matchObj: TPartial
) => (onElse: (rest: Exclude<ADT, { _type: keyof TPartial }>) => Z) => Z {
  return (matchObj) => (onElse) =>
    (matchObj as any)[v._type] != null
      ? (matchObj as any)[v._type](v)
      : onElse(v as any);
}

This would allow adding entries to matchObj though that aren't part of the ADT

pfgray commented 3 years ago

Yeah, I'm playing with this now...

Perhaps your second solution is a bit better:

type Foo = ADT<{
  foo: { value: string };
  bar: { value: string };
  baz: { baz: number };
}>;

const result = matchPI2(foo)({
  baz: (b) => b.baz,
})((hmm) => hmm.value.length);

since we could add it with zero breaking changes...

Here, result is inferring as unknown (TS 3.9.4), does it do the same for you, @rjdestigter ?

rjdestigter commented 3 years ago

~I'm not seeing that. I tried on 3.9 and 3.8 as well with no issue~

Edit: I am seeing it now

rjdestigter commented 3 years ago

Actually, I was only seeing it for tests that were using @ts-expect-error. Everything seems to working fine here using 3.8

pfgray commented 3 years ago

I've actually been playing around with using mapped types to calculate the Z type, instead of letting TS try to infer it:

export function match<
  ADT extends { _type: string },
  M extends MatchObj<ADT, any>
>(matchObj: M): (v: ADT) => Returns<ADT, M> {
  ...
}

type Returns<ADT extends { _type: string }, M extends MatchObj<ADT, any>> = {
  [K in keyof M]: ReturnType<M[K]>;
}[keyof M];

This let you return different types, but just sums them together, i.e.:

const result = pipe(
  foo,
  match({
    bar: (b) => b.value.length,
    foo: (f) => "asdf",
    baz: (b) => b.baz,
  })
);

here, result is inferred as string | number.

I'm gonna merge this as is, and I will probably roll this up with some other things in a v2, thanks for the great idea & work, @rjdestigter !