Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ts 3.4 inference improvements #348

Closed
wants to merge 9 commits into from

Conversation

mweststrate
Copy link
Collaborator

Improved type inference a bit:

  • Required version is upped to 3.4
  • Draft and Immutable have become simpler, as suggest in TypeScript 3.4.0 support #325, which is possible since 3.4
  • Type inference for curried produce has improved. Especially the rest parameters are now better typed and has less edge cases (before at random places | boolean was added to the type inference, and rest arguments remained optional`
    • As a result produce<State>(fn) no longer works, however, produce((x: Draft<State>)) will lead to the correct inference

@aleclarson feel free to shoot at it! I think especially for currying type inference is now better and a bit more predictable, but this change is also kinda breaking on how to type producers

@@ -70,27 +71,47 @@ it("can update readonly state via standard api", () => {
// NOTE: only when the function type is inferred
it("can infer state type from default state", () => {
type State = {readonly a:number} | boolean
type Recipe = (base?: State | undefined) => State
type Recipe = (base?: State | boolean) => State
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doh, that should just be State

@coveralls
Copy link

coveralls commented Apr 10, 2019

Coverage Status

Coverage remained the same at 99.746% when pulling 3fdfcc6 on ts-3.4-inference-improvements into d8d7a1d on ts-3.4.

__tests__/produce.ts Outdated Show resolved Hide resolved
: T extends AtomicObject
: T extends object
? {-readonly [K in keyof T]: Draft<T[K]>}
: T // mostly: unknown & any
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And also primitives like number | string | boolean 😛 Or am I misunderstanding this comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh yes, had a never first there, and AtomicObject at the start, will remove the comment

@aleclarson
Copy link
Member

I'll give it a thorough look sometime soon. Is there a reason you didn't just commit to #325?

Did you try out the curried producer changes I made in #325? If so, what didn't work about them?

): (state?: Immutable<T>, ...rest: Tail<Params>) => Produced<Immutable<T>, ReturnType<Recipe>>

/** Normal producer */
<Base, D = Draft<Base>, Return = void>(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separating Base and D allows things like: produce<readonly string[]>([], (draft: string[]) => ...

@mweststrate
Copy link
Collaborator Author

yes, this was based on that branch. Main problem there was that the rest arguments became optional in the returned producer function. Beyond that not too much changed, but I think the inference flow is slightly clearer this way, in which case we can better document certain best practices.

: (
// The `base` argument is required when `Rest` is required.
base: Immutable<Base> | undefined,
...rest: Rest
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice, this ...rest: Rest always resolved into ...any[], as soon as Base is set (not inferred) by using produce<X>((draft, x: number) => {})

@@ -274,3 +297,68 @@ it("works with generic parameters", () => {
let arr: ReadonlyArray<typeof val> = 0 as any
insert(arr, 0, val)
})

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In these two test cases I tried to capture two real-life scenarios:

  1. Your generic state shape is defined as mutable
  2. The generic state shape is defined as immutable

Given those two scenarios (tests), the tests below verify a few things:

  1. draft is always mutable, regardless the base state
  2. if state: State, then ideally produce(state, fn) and produce(fn)(state) should result in State again

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think in the latest push I've found the solution that satisfies all those constraints! In the latest, the curried producer has it's own generic argument, and basically, what goes in, goes out. That means: immutable types in, immutable types out. Mutable types in, mutable types out. However, the generic argument has to at least satisfy that it is at least the Immutable<T> where T is the first argument of the recipe.

I think this solves all cases quite neatly. The only downside is that the tests that use exactType on the curried function now fails, because the curried producer has a wider type than the recipe. But technically that is actually valid, as the following statement is correct:

produce((draft: { count: 0 }) => { draft.count++ }) ({ count: 7, messages: [] })

The above now neatly does produce a new state of type { count, messages } rather than just { count } which it would be, based on the recipe

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mweststrate mweststrate force-pushed the ts-3.4-inference-improvements branch from d762fc0 to 3644aa8 Compare April 11, 2019 22:52
@mweststrate
Copy link
Collaborator Author

@aleclarson I think this PR is done. I think this should be released as minor, although typescript types change (and TS 3.4 is probably required), there are no semantic or api changes to the library itself.

@mweststrate
Copy link
Collaborator Author

...or I just finish 3.0 and merge it in there :)

@jineshshah36
Copy link

Just my two cents:

You could use the types versions system in package.json to make this a non-breaking change. (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html#version-selection-with-typesversions)

Otherwise, this would break a lot of people’s code.

@aleclarson
Copy link
Member

aleclarson commented Apr 15, 2019

@jineshshah36 I mentioned that here. If we do that, we need to use git apply in the CI build in order to test both versions, because the tests need changes to support 3.4.x.

@aleclarson
Copy link
Member

@mweststrate I'll see if I can review this today!

@jineshshah36
Copy link

jineshshah36 commented Apr 15, 2019

Would it be too much to keep two separate test files within versions in them like the types would be, and then, you only run the file with the matching version?

@aleclarson
Copy link
Member

@jineshshah36 That would require more maintenance than going with git apply and maintaining only the differences between 3.4 and 3.3. Honestly, I like the idea of supporting only TypeScript 3.4+ in Immer 3, like Michel mentioned.

@mweststrate
Copy link
Collaborator Author

Closing this branch as it it is merged to immer3 branch :)

mweststrate added a commit that referenced this pull request Apr 15, 2019
aleclarson pushed a commit that referenced this pull request Apr 17, 2019
@aleclarson aleclarson deleted the ts-3.4-inference-improvements branch April 18, 2019 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants