Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@twop
Copy link
Contributor

@twop twop commented Mar 24, 2018

This is a step 1 and 1.1 of #18

Concerns/comments:

  1. It is a breaking change because of "default".
  2. I used match(...args) {} for checking number of arguments. This is being transpiled to args[0] = arguments[0]. Which is an extra allocation of a new array for args every time match is called. What is the best practice here? Use (...args[]) vs arguments object? There is no arguments object in arrow functions as far as I know, which is a downside of arguments I suppose?
  3. I removed the fallback function which was initialized as v => undefined. It means that if you cheat on types and pass incomplete match cases you will have undefined as a result with no exception. I do believe that it is better to throw an exception if somebody hacked ts. Not 100% of a proper way to handle that.
  4. I didn't change readme.md. I think it makes sense to do that right before publishing to npm?

@coveralls
Copy link

coveralls commented Mar 24, 2018

Coverage Status

Coverage remained the same at 100.0% when pulling 00218c3 on twop:master into ce6abab on pelotom:master.

@pelotom
Copy link
Owner

pelotom commented Mar 27, 2018

  1. It is a breaking change because of "default".

Yep.

  1. I used match(...args) {} for checking number of arguments. This is being transpiled to args[0] = arguments[0]. Which is an extra allocation of a new array for args every time match is called. What is the best practice here? Use (...args[]) vs arguments object? There is no arguments object in arrow functions as far as I know, which is a downside of arguments I suppose?

I think using ...args is fine and is definitely the most idiomatic typescript solution, but it would also be fine to optimize and use arguments. shrug

  1. I removed the fallback function which was initialized as v => undefined. It means that if you cheat on types and pass incomplete match cases you will have undefined as a result with no exception. I do believe that it is better to throw an exception if somebody hacked ts. Not 100% of a proper way to handle that.

My philosophy is that it's not worth putting in place runtime checks for things that should be caught by the type system. If someone is subverting the type system, they have accepted the risk and responsibility for keeping themselves safe. However, there is a problem in your current implementation, which is that it's no longer type safe. For example, this type checks, but it shouldn't, and will throw TypeError: cases[k] is not a function at runtime:

unionize({ x: {} }).match({
  x: undefined,
  default: () => 42
})({ tag: 'x' })
  1. I didn't change readme.md. I think it makes sense to do that right before publishing to npm?

Either way is fine with me, but we'll definitely want to update the readme before releasing.

@pelotom
Copy link
Owner

pelotom commented Mar 27, 2018

I would rather this was 2 separate PRs, one for the default case and one for the overloading of match.

@twop
Copy link
Contributor Author

twop commented Mar 28, 2018

I was trying rly hard to improve typings but I got stuck :(

But as It turned out with the current typings you can do exactly the same

    expect(Foo.match({
      x: undefined, // AHA!
      y: s => s.length,
    })(foo)).toBe(12)

So I guess we either try to fix it or keep it as is with Partial<>. But check for undefined before calling.
So essentially ignore x:undefined

Thoughts?

@twop
Copy link
Contributor Author

twop commented Mar 28, 2018

It is possible for a function to return undefined as well

  const Foo = unionize({
    x: ofType<number>(),
    y: ofType<string>()
  }, 'tag', 'data')

  const foo = Foo.x(3);

// i is inferred as any
  const i = Foo.match({
    x: n => n + 1,
    y: s => undefined, // no errors here
  })(foo);

Am I missing something?

It seems that it is related to tsconfig settings. Works very differently in another project

@twop
Copy link
Contributor Author

twop commented Mar 28, 2018

So the closest typings that I came up with is

export type MatchCases<Record, Union, A, K extends keyof Record> =
  | Cases<Record, A>
  | Cases<Record, A, K> & { default: (variant: Union) => A };

export type Match<Record, Union> = {
  <A, K extends keyof Record>(cases: MatchCases<Record, Union, A, K>): (
    variant: Union
  ) => A;
};

Which is conceptually either: full keys of Record or any subset + default case.

Examples:

const Foo = unionize(
  {
    x: ofType<number>(),
    y: ofType<string>()
  },
  'tag',
  'data'
);

const foo = Foo.x(3);

// full set of keys
const a = Foo.match({
  x: n => n + 1,
  y: s => s.length
})(foo);

// still can put default in here
const a_ = Foo.match({
  x: n => n + 1,
  y: s => s.length,
  default: ({ tag }) => tag.length
})(foo);

// should work but gives an error
const c = Foo.match({ x: n => n + 1, default: _ => 42 })(foo);

Argument of type '{ x: (n: number) => number; default: (: ({ tag: "x"; } & { data: number; }) | ({ tag: "y"; } & {...' is not assignable to parameter of type 'MatchCases<{ x: number; y: string; }, ({ tag: "x"; } & { data: number; }) | ({ tag: "y"; } & { da...'.
Type '{ x: (n: number) => number; default: (
: ({ tag: "x"; } & { data: number; }) | ({ tag: "y"; } & {...' is not assignable to type 'Cases<{ x: number; y: string; }, {}, "x" | "y"> & { default: (variant: ({ tag: "x"; } & { data: n...'.
Type '{ x: (n: number) => number; default: (: ({ tag: "x"; } & { data: number; }) | ({ tag: "y"; } & {...' is not assignable to type 'Cases<{ x: number; y: string; }, {}, "x" | "y">'.
Property 'y' is missing in type '{ x: (n: number) => number; default: (
: ({ tag: "x"; } & { data: number; }) | ({ tag: "y"; } & {...'.

// It seems that ts cannot infer 'x' as a subset. That error goes away with explicit type of subset 'x'
const b = Foo.match<number, 'x'>({ x: n => n + 1, default: _ => 42 })(foo);

So it is easy to have possible x: undefined using Partial<> and just ignore there values at runtime. I tried a couple of options using ts 2.8 but none seemed to work.

This is my 3rd day trying to provide typings and I feel a bit defeated :(

@pelotom
Copy link
Owner

pelotom commented Mar 28, 2018

The test file was previously not compiled according to the tsconfig.json, so strictNullChecks wasn’t being applied to the tests. I’ve fixed that on master, if you try it there you’ll see that undefined can’t be used in place of a case function.

@twop
Copy link
Contributor Author

twop commented Mar 28, 2018

@twop
Copy link
Contributor Author

twop commented Mar 29, 2018

out of all solutions that I tried

type EvalCases<Record, Res> =
  | Cases<Record, Res> & { else?: never }
  | (Partial<Cases<Record, Res>> & { else: (r: Record) => Res });

This seems to work the best. Yes you can type {b: undefined} (from stackoverflow)

evalMyRecord(rec, {
  a: s => s.length,
  b: undefined,
  else: _ => 2
});

But on the other hand all other corner cases seem to be solved. It is also easy to check against undefined values (ts will allow only undefined for 'b')

function match(cases: any): (variant: any) => any {
    return (variant: any) => {
      const k = variant[tagProp]
      const handler = cases[k]; 
      // also isn't it faster this way? Try to always extract the prop
      return handler !== undefined
        ? handler(valProp ? variant[valProp] : variant)
        : cases.default(variant)
    }
  }

@twop twop changed the title Added "default" case + inline usage for matching "default" case for matching Mar 29, 2018
@twop
Copy link
Contributor Author

twop commented Mar 29, 2018

The latest version uses Partial + check for undefined. Pls take a look

@pelotom
Copy link
Owner

pelotom commented Apr 2, 2018

Hey @twop, sorry for the delay, been on vacation. I'll take a look at this tonight.

@pelotom pelotom merged commit 00218c3 into pelotom:master Apr 7, 2018
@pelotom
Copy link
Owner

pelotom commented Apr 7, 2018

Thanks, this looks really good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants