DefinitelyTyped / DefinitelyTyped

The repository for high quality TypeScript type definitions.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

On the use of Pick for @types/react's setState

Jessidhia opened this issue · comments

I understand that Pick was used for the type of setState because returning undefined for a key that should not be undefined would result in the key being set to undefined by React.

However, using Pick causes other problems. For one, the compiler service's autocomplete becomes useless, as it uses the result of the Pick for autocompletion, and when you request completions the Pick's result doesn't yet contain the key you may want to autocomplete. But the problems are particularly bad when writing setState with a callback argument:

  1. The list of keys is derived from your first return statement; if you don't return a particular key in your return statement you also can't read it in the argument without forcing the list of keys to reset to never. Multiple return statements can be hard to write if they return different keys, especially if you have an undefined return somewhere (e.g. if (state.busy) { return }).
    • This can be worked around by always using the input in a spread (e.g. this.setState(input => ({ ...input, count: +input.count + 1 }))) but this is redundant and a deoptimization, particularly for larger states, as setState will pass the return value of the callback to Object.assign.
  2. If, for some reason, the type you are returning is not compatible with the input type, the Pick will choose never for its keys, and the function will be allowed to return anything. Even keys that coincide with an existing key effectively allow any as a value -- if it doesn't fit, it's just not Picked, and is treated as an excess property for {}, which is not checked.
  3. If never is picked as the generic argument, for any of the reasons listed above, a callback argument may actually be treated as an argument to the object form of setState; this causes the callback's arguments to be typed any instead of {}. I am not sure why this is not an implicit any error.
interface State {
  count: string // (for demonstration purposes)
}

class Counter extends React.Component<{}, State> {
  readonly state: Readonly<State> = {
    count: '0'
  }

  render () {
    return React.createElement('span', { onClick: this.clicked }, this.state.count)
  }

  private readonly clicked = () => {
    this.setState(input => ({
      count: +input.count + 1 // not a type error
      // the setState<never>(input: Pick<State, never>) overload is being used
    }))
  }
}

To sum it up, while the use of Pick, despite some inconvenience, help catch type errors in the non-callback form of setState, it is completely counter-productive in the callback form; where it not only does not do the intended task of forbidding undefined but also disables any type checking at all on the callback's inputs or outputs.

Perhaps it should be changed, at least for the callback form, to Partial and hope users know to not return undefined values, as was done in the older definitions.

Thanks for your feedback. This is a very interesting case you bring up.

I need to think about the implications a bit before I commit to an opinion.

At one point @ahejlsberg wanted to treat optionality differently than | undefined. Thus foo?: string would mean foo is either unset or it's a string.

When reading a property value, the difference is irrelevant 99.9% of the time, but for writes, especially in the case of Partial<>, the distinction is wildly important.

Unfortunately, this is a breaking language change so we must wait for 3.0 or have it behind a flag.

If we had said change, Partial<> becomes extremely useful to many instead of my current dogma which is to reject its use on sight.

@ahejlsberg I know you're a busy man, how hard would it be to implement ? such that undefined is not an implicit assignable value?

Alright, after spending some time this morning thinking about the problem, I see a few "solutions" to the problem you propose, each with some pretty heavy side effects.

1. Change optionality (interface State { foo?: string }) to mean string or not set.

Example:

interface State {
  foo: string;
  bar: string;
}

const bleh: Partial<State> = { foo: "hi" }; // OKAY
const bleh: Partial<State> = { foo: undefined; } // BREAK

This would technically solve the problem and allow us to use Partial<>, at the expense of the 98% case being more difficult. This breaks the world so wouldn't really be possible to do until 3.x and generally, very few libraries/use cases actually care about the distinction between unset vs undefined.

2. Just switch to partial

Example:

interface State {
  foo: string;
  bar: string;
}

setState(prevState => {foo: "hi"}); // OKAY
setState(prevState => {foo: undefined}); // OKAY BUT BAD!

3. Do Nothing

Example:

interface State {
  foo: string;
  bar: string;
}

// The following errors out because {foo: ""} can't be assigned to Pick<State, "foo" | "bar">
// Same with {bar: "" }
setState((prevState) => {
  if (randomThing) {
    return { foo: "" };
  }
  return { bar: "" };
});

// A work around for the few places where people need this type of thing, which works
setState((prevState) => {
  if (randomThing) {
    return { foo: "" } as State
  }
  return { bar: "" } as State
});

// This is fine because the following is still an error
const a = {oops: ""} as State

4. Add nested logic to literal types that are joined

Right now, when we have multiple return paths, the compiler just all the potential keys into one giant Pick<State, "foo" | "bar">.

However, a backwards compatible change would be to allow the literals to be grouped, like Pick<State, ("foo") | ("bar")> or in a more complex case: Pick<State, ("foo" | "bar") | ("baz")>

The lack of parens suggests its just a single set of values which is the existing functionality.

Now when we try to cast {foo: number} to Pick<State, ("foo") | ("bar")>, we can succeed. Same with {bar: number}.

Pick<State, ("foo") | ("bar")> is also castable to Partial<State> and to {foo: number} | {bar: number}.

My personal conclusion

(1) and (2) are just not doable. One introduces complexity for nearly everyone, no matter what, and the other helps people produce code that compiles but is clearly incorrect.

(3) is completely usable today and while I don't recall why I added | S as a potential return value to the function in setState, I cannot fathom any other reason to do so.

(4) could obviate the workaround in (3) but is up to the TypeScript developers and maybe if we get @ahejlsberg interested enough we could see it sooner than later.

This leaves me thinking the types are more correct today than if we changed them.

The problem with the "do nothing" approach is that the compiler is not behaving the way you are describing in the case you actually make a real type error. If I modify your example to, say, set the value to 0 instead of empty string:

interface State {
  foo: string;
  bar: string;
}

// The following does not error at all because the compiler picks `Pick<State, never>`
// The function overload of `setState` is not used -- the object overload is, and it
// accepts the function as it is accepting anything (`{}`).
setState((prevState) => {
  if (randomThing) {
    return { foo: 0 };
  }
  return { bar: 0 };
});

I see now. Back to the drawing board. I'm going to see if we can create a clever solution to this. If we cannot come up with one, then we need to evaluate the Partial<> solution you propose. The big thing to consider is whether or not an unexpected undefined on a value would be more common/annoying than an unexpected wrong type.

To clarify, we have two overloads for this...

setState<K extends keyof S>(f: (prevState: Readonly<S>, props: P) => Pick<S, K>, callback?: () => any): void;
setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void;

Setting either or both of these to Partial<S> instead of Pick<S, K> doesnt fix your problem. It still drops through to the object version, which accepts the wrong type for some reason:

interface State {
  bar: string;
  foo: number;
}

class Foo extends React.Component<{}, State> {
  public blah() {
    this.setState((prevState) => ({bar: 1})); // Doesnt error :/
  }
}```

I came here to report a related issue, which is that refactoring of the state type is broken with this change.

For example, see https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx - if you open this in VS Code and F2 to refactor/rename something on line 6 (https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L6), it will update that line and https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L14, but will miss the use of this in the setState call at https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L20

The only way I've found to make it work correctly is to explicitly type my object as IState when calling setState, which is a bit inconvenient and quite easy to forget.

I'm not familiar with the rationale for the change exactly, but it does seem to have broken type-safety in a fairly significant way when working with state so it would be great if there was a way to solve it!

Thanks,
Tom

Yeah, again I believe it would be better to use Partial as it keeps that relation information better.

Arguably, Pick not being correctly refactored is a limitation/bug of the refactoring code that could be improved, but I still don't believe that Pick is providing the type safety that #18365 (comment) mentions. While Partial would allow undefined where undefined is not supposed to be, and the user then needs to be careful to not do that, Pick allows anything because any type that does not conform to the interface would cause, instead of a type error, Pick to generate an empty interface instead, which accepts anything.

As for #18365 (comment), this could this be a bug in the excess property checker? Or was this tested before the excess property checker was made stricter in 2.3 or 2.4 (I forgot)?

Another issue I just noticed here is that if a component has either no state type (i.e. just one type parameter to React.Component), or the state type is empty ({}), Typescript will still allow calls to setState without throwing an error, which seems incorrect to me – see https://github.com/tomduncalf/react-types-issue/blob/master/Test2.tsx

The solution using Partial sounds preferable to me – I might try patching it myself and see how it works.

Thanks,
Tom

Hi all!
I don't know why, but if I join setState declarations to single declaration:

setState<K extends keyof S>(state: ((prevState: Readonly<S>, props: P) => Pick<S, K>) | Pick<S, K>, callback?: () => any): void;

It works as expected for me:

import * as React from 'react';

export class Comp extends React.Component<{}, { foo: boolean, bar: boolean }> {
  public render() {
    this.handleSomething();
    return null;
  }

  private handleSomething = () => {
    this.setState({ foo: '' }); // Type '""' is not assignable to type 'boolean'.
    this.setState({ foo: true }); // ok!
    this.setState({ foo: true, bar: true }); // ok!
    this.setState({}); // ok!
    this.setState({ foo: true, foo2: true }); // Object literal may only specify
    // known properties, and 'foo2' does not exist in type
    this.setState(() => ({ foo: '' })); // Property 'foo' is missing in type '() => { foo: string; }'.
    this.setState(() => ({ foo: true })); // ok!
    this.setState(() => ({ foo: true, bar: true })); // ok!
    this.setState(() => ({ foo: true, foo2: true })); // Property 'foo' is missing in type
    // '() => { foo: true; foo2: boolean; }'
    this.setState(() => ({ foo: '', foo2: true })); // Property 'foo' is missing in
    // type '() => { foo: string; foo2: boolean; }'.
    this.setState(() => ({ })); // ok!
  };
}

May this changes enough for fixing the original issue?

@mctep Nice find.

And if I take what you did and extend it a tiny bit, to be Partial<S> & Pick<S, K> instead of Pick<S, K> in places, intellisense suggests key names for you. Unfortunately, the key names say the property value is "something | undefined" but when you compile it, it knows better:

declare class Component<P, S> {
    setState<K extends keyof S>(state: ((prevState: Readonly<S>, props: P) => (Partial<S> & Pick<S, K>)) | (Partial<S> & Pick<S, K>), callback?: () => any): void;
}

interface State {
    foo: number;
    bar: string;
    baz?: string;
}

class Foo extends Component<{}, State> {
    constructor() {
        super();
        this.setState(() => { // error
            return {
                foo: undefined
            }
        });
        this.setState({ // error
            foo: undefined
        })
        this.setState({
            foo: 5,
            bar: "hi",
            baz: undefined
        })
    }
}

I'll put in this change later today

The recent "fix" is causing problems for me now with multiple return statements within a setState() callback.

Typescript: 2.6.2 with all "strict" options enabled except for "strictFunctionTypes"
types/react: 16.0.30

Code example:

interface TestState {
    a: boolean,
    b: boolean
}

class TestComponent extends React.Component<{}, TestState> {
    private foo(): void {
        this.setState((prevState) => {
            if (prevState.a) {
                return {
                    b: true
                };
            }

            return {
                a: true
            };
        });
    }
}

Compiler error:

error TS2345: Argument of type '(prevState: Readonly<TestState>) => { b: true; } | { a: true; }' is not assignable to parameter of type '((prevState: Readonly<TestState>, props: {}) => Pick<TestState, "b"> & Partial<TestState>) | (Pick<TestState, "b"> & Partial<TestState>)'.
  Type '(prevState: Readonly<TestState>) => { b: true; } | { a: true; }' is not assignable to type 'Pick<TestState, "b"> & Partial<TestState>'.
    Type '(prevState: Readonly<TestState>) => { b: true; } | { a: true; }' is not assignable to type 'Pick<TestState, "b">'.
      Property 'b' is missing in type '(prevState: Readonly<TestState>) => { b: true; } | { a: true; }'.

522         this.setState((prevState) => {
                          ~~~~~~~~~~~~~~~~

Also seeing the issue described by @UselessPickles as a result of this change.

I’m fairly certain that’s always been a problem.

I'm way more than fairly certain that it has not always been a problem. I have a project with multiple return statements in setState() callbacks that has been compiling without issue for 2+ months. I have been keeping up with NPM dependecy upgrades every 2 weeks or so, and this compiler error just started happening for me today after upgrading to the latest version of types/react. The previous version did not produce this compiler error.

It's been a problem since the change from Partial to Pick. The workaround would be to give the list of keys you intend to return to setState's generic parameter, but then you're forced to always return all keys...

What I do in those cases is either I always return all keys, with the non-modified keys being set to key: prevState.key, or by returning a spread with prevState ({ ...prevState, newKey: newValue }).

Maybe I actually have a more specific edge case in my code that was working previously by coincidence? An actual example from my project is more like this, where it either returns an empty object (to not change any state), or returns a non-empty object:

interface TestState {
    a: boolean,
    b: boolean
}

class TestComponent extends React.Component<{}, TestState> {
    private foo(newValue: boolean): void {
        this.setState((prevState) => {
            if (prevState.a === newValue) {
                // do nothing if there's no change
                return { };
            }

            return {
                a: newValue,
                // force "b" to false if we're changing "a"
                b: false
            };
        });
    }
}

return null and return undefined are also perfectly acceptable return values for not changing state (they will go through Object.assign and so do no changes to this.state).

Reminds me, the current signature does not allow either of them. Maybe null should be allowed as a return value, at least, as it's not something that can come out of accidentally forgetting to return at all.


Anyway, in cases where the signature is ambiguous, TypeScript seems to pick only the first return statement in source order and use it to derive the generic parameters. It seems to be able to merge the types for simple generics (e.g. Array or Promise), but it never merges them if the contextual type is a mapped type like Pick.

I am seeing some regressions with the non-function version with the most recent types. In particular, the "state" argument type when passing an argument has changed from:

setState(state: Pick<S, K>, callback?: () => any): void;

to:

state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> & Partial<S>)) | (Pick<S, K> & Partial<S>),

62c2219#diff-96b72df8b13a8a590e4f160cbc51f40c

The addition of & Partial<S> seems to be breaking things that previously worked.

Here's a minimal repro:

export abstract class ComponentBaseClass<P, S = {}> extends React.Component<P, S & { baseProp: string }>
{
	foo()
	{
		this.setState( { baseProp: 'foobar' } );
	}
}

this fails with error:

(429,18): Argument of type '{ baseProp: "foobar"; }' is not assignable to parameter of type '((prevState: Readonly<S & { baseProp: string; }>, props: P) => Pick<S & { baseProp: string; }, "b...'.
Type '{ baseProp: "foobar"; }' is not assignable to type 'Pick<S & { baseProp: string; }, "baseProp"> & Partial<S & { baseProp: string; }>'.
Type '{ baseProp: "foobar"; }' is not assignable to type 'Partial<S & { baseProp: string; }>'.

Changing the state type from S &{ baseProp: string } to just { baseProp: string } will also make the error go away (though that will break actual classes that specify the S type).

That’s interesting. I’m happy to roll back the partial and pick portion of the change

I will say that what you report sounds like a bug in TS, specifically:

Type '{ baseProp: "foobar"; }' is not assignable to type 'Partial<S & { baseProp: string; }>'.

Alright. Maybe TS can’t know what’s up because there is no relationship between S and baseProp.

One could pass an S that is type {baseProp:number} in which case it is correct that you cannot assign a string to baseProp.

Perhaps if S extended the baseProp version?

I had tried something like this before:

interface BaseState_t
{
	baseProp: string
}

export abstract class ComponentBaseClass<P, S extends BaseState_t> extends React.Component<P, S >
{
	foo()
	{
		this.state.baseProp;
		this.setState( { baseProp: 'foobar' } );
	}
}

while the access is ok, the setState call has the same error:

Argument of type '{ baseProp: "foobar"; }' is not assignable to parameter of type '((prevState: Readonly<S>, props: P) => Pick<S, "baseProp"> & Partial<S>) | (Pick<S, "baseProp"> &...'.
Type '{ baseProp: "foobar"; }' is not assignable to type 'Pick<S, "baseProp"> & Partial<S>'.
Type '{ baseProp: "foobar"; }' is not assignable to type 'Partial<S>'.

It probably isn't a good way to structure it - after all, some child class could declare state variables that collided with variables on the base class. So typescript might be right to complain that it can't be sure what's going to happen there. It's odd that it has no problem with accessing those props though.

Hmm. This definitely feels like a bug in TS now.

I will play with this today and possibly take out the & Partial.

I’ll add this as a test case as well and try another idea to make intellisense happy

Hi,
Same problem here...

export interface ISomeComponent {
    field1: string;
    field2: string;
}

interface SomeComponentState {
    field: string;
}

export class SomeComponent<
    TProps extends ISomeComponent = ISomeComponent,
    TState extends SomeComponentState = SomeComponentState> extends React.Component<TProps, TState>{

    doSomething() {
        this.setState({ field: 'test' });
    }

    render() {
        return (
            <div onClick={this.doSomething.bind(this)}>
                {this.state.field}
            </div>
        );
    }
}

Error in setState:
TS2345 (TS) Argument of type '{ field: "test"; }' is not assignable to parameter of type '((prevState: Readonly<TState>, props: TProps) => Pick<TState, "field"> & Partial<TState>) | (Pick...'. Type '{ field: "test"; }' is not assignable to type 'Pick<TState, "field"> & Partial<TState>'. Type '{ field: "test"; }' is not assignable to type 'Partial<TState>'.

Error just started to happen after this change. In version 16.0.10 it worked fine.

Typescript has several related issues, they have closed them as working as designed:

microsoft/TypeScript#19388

I made a gist of some examples here: https://gist.github.com/afarnsworth-valve/93d1096d1410b0f2efb2c94f86de9c84

Though the behavior still seems strange. In particular, you can assign to a variable typed as the base class without cast, and then make the same calls with that without problem. This issue seems to suggest assignment and comparison are two different things.

Haven’t forgotten you guys. Will fix soon

The referenced PR should solve this issue. I also added tests that should protect against breaking this edge case again.

@ericanderson Thanks for the quick response :)

Does the new fix have any limitations/downsides?

None that I currently know of. I was able to keep intellisense (actually better than before, as it solves some edge cases).

To elaborate, the & Partial<S> solution was to trick intellisense to reveal possible params, but it did so by stating they are X | undefined. This would of course fail to compile but it was a bit confusing. Getting the | S fixes the intellisense so that it suggests all the right params and now it doesnt show a false type.

I tried looking into adding null as a possible return value from the setState callback (what you can return to state you don't want to change any state, without making return; also valid), but that instead made the type inference just completely give up and pick never as the keys 😢

For now, in the callbacks where I do actually want to skip updating state, I've been using return null!. The never type of this return makes TypeScript ignores the return for the generic type inference.

Hi guys...

The last commit fixed my issue.
Thanks for the quick response :)

What version has the fix? Is it published to npm? I am hitting the case where the callback version of setState is telling me there is a property mismatch in the return value, just like @UselessPickles found.

Should be good on latest @types/react

I fixed it for the 15 and the 16 series

import produce from 'immer';

interface IComponentState
{
    numberList: number[];
}

export class HomeComponent extends React.Component<ComponentProps, IComponentState>

The React's old type definition..

// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
// Also, the ` | S` allows intellisense to not be dumbisense
setState<K extends keyof S>(
    state: ((prevState: Readonly<S>, props: P) => (Pick<S, K> | S)) | (Pick<S, K> | S),
    callback?: () => void
): void;

..produces this error:

screen shot 2018-05-18 at 2 36 44 pm

I tried this suggestion:

setState<K extends keyof S>(
    state:
        ((prevState: Readonly<S>, props: P) => (Partial<S> & Pick<S, K>))
        | (Partial<S> & Pick<S, K>),
    callback?: () => any
): void;

Intellisense can now sense the property from React's state. However, the sensed property is now perceived as possibly undefined.

screen shot 2018-05-18 at 2 37 32 pm

Would have to use null assertion operator even though numberList is not undefined nor nullable:

screen shot 2018-05-18 at 2 38 51 pm

I'll just stay on old type definition until type sensing is improved. For the meantime, I'll just explicitly state the type on immer produce's generic parameter. produce<IComponentState> is easier to reason about than list!.

screen shot 2018-05-18 at 2 51 43 pm

Your first error is because you aren’t returning anything. That’s not how setState works.

It works even when not returning a variable in produce. I just followed the example here (non-TypeScript):

https://github.com/mweststrate/immer

onBirthDayClick2 = () => {
    this.setState(
        produce(draft => {
            draft.user.age += 1
            // no need to return draft
        })
    )
}

The only thing preventing TypeScript from being able to run that code is it has wrongly-inferred type from React type definition. The type definition reports an error of numberList does not exist on type Pick<IComponentState, never>. Just able to make the compile error gone by explicitly passing the type on produce's generic parameter, i.e., produce<IComponentState>.

I even tried to return the variable in produce and see if it would help React type definition infer the type (a chick-and-egg problem though), but still there's no way for the React type definition to detect the state's correct type. Hence the intellisense for draft is not appearing:

screen shot 2018-05-18 at 10 38 04 pm

Or perhaps I have a wrong expectation from the compiler :) The compiler can't make a type for the draft variable based on setState's type as the compiler process the code from the inside out. However, the suggested type definition somehow made me think that the compiler can process the code from outside in, that it can choose the best type it can pass from outer code (setState) to inner code (produce).

setState<K extends keyof S>(
    state:
        ((prevState: Readonly<S>, props: P) => (Partial<S> & Pick<S, K>))
        | (Partial<S> & Pick<S, K>),
    callback?: () => any
): void;

With the above type definition, the compiler can detect that draft has a numberList property. It detects it as possibly undefined though:

image

Upon further tinkering, I made the compiler able to pass the state's type to produce's draft by adding the S to setState's type definition:

setState<K extends keyof S>(
    state:
        ((prevState: Readonly<S>, props: P) => (Partial<S> & Pick<S, K> & S))
        | (Partial<S> & Pick<S, K>),
    callback?: () => any
): void;

The code is compiling now :)

screen shot 2018-05-18 at 11 21 03 pm

Your error isn't with setState, your error is inside of produce. What is your type definition for produce?

My PR above above has error on DefinitelyTyped's test script, I didn't test locally . So I'm testing it locally now.

Here's the immer/produce type definition.

/**
 * Immer takes a state, and runs a function against it.
 * That function can freely mutate the state, as it will create copies-on-write.
 * This means that the original state will stay unchanged, and once the function finishes, the modified state is returned.
 *
 * If the first argument is a function, this is interpreted as the recipe, and will create a curried function that will execute the recipe
 * any time it is called with the current state.
 *
 * @param currentState - the state to start with
 * @param recipe - function that receives a proxy of the current state as first argument and which can be freely modified
 * @param initialState - if a curried function is created and this argument was given, it will be used as fallback if the curried function is called with a state of undefined
 * @returns The next state: a new state, or the current state if nothing was modified
 */
export default function<S = any>(
    currentState: S,
    recipe: (this: S, draftState: S) => void | S
): S

// curried invocations with default initial state
// 0 additional arguments
export default function<S = any>(
    recipe: (this: S, draftState: S) => void | S,
    initialState: S
): (currentState: S | undefined) => S
// 1 additional argument of type A
export default function<S = any, A = any>(
    recipe: (this: S, draftState: S, a: A) => void | S,
    initialState: S
): (currentState: S | undefined, a: A) => S
// 2 additional arguments of types A and B
export default function<S = any, A = any, B = any>(
    recipe: (this: S, draftState: S, a: A, b: B) => void | S,
    initialState: S
): (currentState: S | undefined, a: A, b: B) => S
// 3 additional arguments of types A, B and C
export default function<S = any, A = any, B = any, C = any>(
    recipe: (this: S, draftState: S, a: A, b: B, c: C) => void | S,
    initialState: S
): (currentState: S | undefined, a: A, b: B, c: C) => S
// any number of additional arguments, but with loss of type safety
// this may be alleviated if "variadic kinds" makes it into Typescript:
// https://github.com/Microsoft/TypeScript/issues/5453
export default function<S = any>(
    recipe: (this: S, draftState: S, ...extraArgs: any[]) => void | S,
    initialState: S
): (currentState: S | undefined, ...extraArgs: any[]) => S

// curried invocations without default initial state
// 0 additional arguments
export default function<S = any>(
    recipe: (this: S, draftState: S) => void | S
): (currentState: S) => S
// 1 additional argument of type A
export default function<S = any, A = any>(
    recipe: (this: S, draftState: S, a: A) => void | S
): (currentState: S, a: A) => S
// 2 additional arguments of types A and B
export default function<S = any, A = any, B = any>(
    recipe: (this: S, draftState: S, a: A, b: B) => void | S
): (currentState: S, a: A, b: B) => S
// 3 additional arguments of types A, B and C
export default function<S = any, A = any, B = any, C = any>(
    recipe: (this: S, draftState: S, a: A, b: B, c: C) => void | S
): (currentState: S, a: A, b: B, c: C) => S
// any number of additional arguments, but with loss of type safety
// this may be alleviated if "variadic kinds" makes it into Typescript:
// https://github.com/Microsoft/TypeScript/issues/5453
export default function<S = any>(
    recipe: (this: S, draftState: S, ...extraArgs: any[]) => void | S
): (currentState: S, ...extraArgs: any[]) => S
/**
 * Automatically freezes any state trees generated by immer.
 * This protects against accidental modifications of the state tree outside of an immer function.
 * This comes with a performance impact, so it is recommended to disable this option in production.
 * It is by default enabled.
 */
export function setAutoFreeze(autoFreeze: boolean): void

/**
 * Manually override whether proxies should be used.
 * By default done by using feature detection
 */
export function setUseProxies(useProxies: boolean): void

@ericanderson could you please point me to the discussion about why Pick is used instead of Partial? This have been causing me hours of grief (using plain setState(obj), not the callback version), and for now I'm going with this.setState(newState as State) as a workaround. I just want to understand why it was changed, because I must be missing something.

Hello @ericanderson ,

I have some problem with the latest definition.

My use case is briefly like this:

interface AppState {
  valueA: string;
  valueB: string;
  // ... something else
} 
export default class App extends React.Component <{}, AppState> {
  onValueAChange (e:React.ChangeEvent<HTMLInputElement>) {
    const newState: Partial<AppState> = {valueA: e.target.value}
    if (this.shouldUpdateValueB()) {
      newState.valueB = e.target.value;
    }
    this.setState(newState); // <-- this leads to a compiling error
  }
  // ... other methods
}

The error message is like:

Argument of type 'Partial<AppState>' is not assignable to parameter of type 'AppState | ((prevState: Readonly<AppState>, props: {}) => AppState | Pick<AppState, "valueA" | "v...'.
  Type 'Partial<AppState>' is not assignable to type 'Pick<AppState, "valueA" | "valueB" | "somethingElse">'.
    Types of property 'valueA' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.

It seems that Partial<AppState> is not compatible with the signature of setState. Of course I can solve this by type assertion like

this.setState(newState as Pick<AppState, 'valueA' | 'valueB'>)

but it's not ideal, because:

  1. this syntax is very verbose
  2. more importantly, type assertion could be violate my actual data. For example, newState as Pick<AppState, 'somethingElse'> also passes the check, although it doesn't fit my data.

I think Partial should somehow compatible with Pick<T, K extends keyof T>, since Partial just picks uncertain number of keys from T. I am not sure my understanding if accurate. Anyway the ideal usage from my point of view should be that I can pass the variable typed Partial directly into setState.

Could you kindly consider my suggestion or point out my misunderstanding if there is? Thank you!

This is a really old change first off. So unless someone else recently changed this you’re probably barking up the wrong tree.

That said. Partial allows undefined values.

const a : Partial<{foo: string}> = { foo: undefined }

a is valid but clearly the result of that update to your state puts your state with foo being undefined even though you declared that’s impossible.

Therefore a partial is not assignable to a Pick. And Pick is the right answer to ensure your types don’t lie

I feel that not allowing:

setState((prevState) => {
  if (prevState.xyz) {
    return { foo: "" };
  }
  return { bar: "" };
});

is super restrictive.

@Kovensky's workaround is the only sensible workaround I know of, but still painful to write.

Is there anything that can be done to support this (I'd say) fairly common pattern?

The only thing that can be done is remove typesafety

Can someone explain the reasoning for Pick<S, K> | S | null?

        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

Tbh I don't even now why the signature above even works for partial state updates as K is defined as keyof S so Pick<S, K> essentially recreates S?

shouldn't Partial<S> | null do the job as well?

        setState(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Partial<S> | null)) | (Partial<S> | null),
            callback?: () => void
        ): void;

Can someone explain...

It's been explained clearly a few replies up.

setState((prevState) => {
  if (prevState.xyz) {
    return { foo: "" };
  }
  return { bar: "" };
});

is super restrictive.

@Kovensky's workaround is the only sensible workaround I know of, but still painful to write.

I've just hit this exact problem, but I can't see any reference to Kovensky in the thread (maybe someone changed their username?). Can anyone point me to the currently recommended workaround

@timrobinson33 This comment explain the workaround #18365 (comment)

And it's a good thing we don't have to worry about this anymore with hooks 🙂

@timrobinson33 This comment explain the workaround #18365 (comment)

Thanks very much for that. In the end I thought my code looked nicer as several small setState calls with If statements outside them, even though this means some paths will call setState more than once.

I guess this is actually simlar to how we work with hooks, seeing a the state as several small things we update independently.

Sorry to necro this, but after reading through the conversation, I'm not sure what the proper typesafe way to accomplish a conditional partial state update is. All of the workarounds here resolve around updating one key or the other, not potential combinations of keys. The usage of pick as a type seems to force the definition of the object being sent to setState to be defined inline in the setState call and not have a conditional definition/construction.

What I'm attempting to do:

const newValue: number = ... // comes from method parameter in reality
const newState: Partial<localState> = {};
if (newValue !== 2) {
    newState.settingA = null;
    newState.settingB = null;
}
if (newValue === 3) {
    newState.settingC = true
}
newState.settingValue = newValue;
this.setState(newState);

The workaround from #18365 (comment) does not apply here, as the keys being updated are conditional, so we can't do a forced spread like { ...prevState, newKey: newValue }

@zak-thompson This is too long ago for me to remember the context but if you want to put conditionals in a spread you can do it like this { ...prevState, ...(needsNewValue ? {newKey: newValue} : null)}

the brackets aren't necessary but I added them to make it clearer what's happening

@timrobinson33 got it, makes sense. I was trying to avoid that as I prefer having each block of resets happening in its own IF block for clarity as opposed to having to parse every ternary operator, but if thats a limitation of the Pick type then I guess I'll move towards that.