-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Partial does not solve React.setState #12793
Comments
I believe this is what you're looking for: function setState<T, K extends keyof T>(obj: T, state: Pick<T, K>) {
for (let k in state) {
obj[k] = state[k];
}
}
interface Foo {
a: string;
b: number;
}
let foo: Foo = { a: "hello", b: 42 };
setState(foo, { a: "test", b: 43 })
setState(foo, { a: "hi" });
setState(foo, { b: 27 });
setState(foo, { });
setState(foo, { a: undefined }); // Error
setState(foo, { c: true }); // Error |
Looks good! Thanks. PS: You don't happen to have something like that, but for a recursive structure do you? :) |
@ahejlsberg Thanks for the post. However, the following situation then creates an issue: interface FooState {
bar: string;
foo?: string;
}
const defaultFooState: FooState = {
bar: "Hi",
foo: undefined,
};
class Foo extends React.Component<{}, FooState> {
public doStuff() {
this.setState(defaultFooState);
}
} And updating setState as follows: setState<K extends keyof S>(f: (prevState: S, props: P) => Pick<S, K>, callback?: () => any): void;
setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void; You get the error:
Updating the state to explicitly allow the optional to be undefined also produces the same error.
|
@ericanderson question: in your opinion, should |
Ive been kicking it around in my head. If If you implement |
interface TState = { foo?: string; }
let state: TState = { foo: "initial" }; Now I want state to become I prefer the Partial to the Pick in this case. Partial allows some invalid behavior, but Pick prohibits some valid expressions. If index types had the concept of optionality, then (Then I think for full completeness, if indexed types gain the concept of optionality they should also gain the concept of requiredness, so that we could undo optionality. type Required<T> = {
[K in keyof T]!: T[K]
}) |
@masonk if you define your state as But if we use |
@RyanCavanaugh If there is a bug here, I would suggest that any parameter who is an optional is also an implicit "T | undefined" so that when you convert it in something like |
Sure, but the idiomatic way to say |
@masonk Correct. I think we could say that there is currently a bug in TypeScript that the undefined get lost in the case of |
@masonk Also do note that they arent always assignable. type A = { foo?: string };
type B = { foo: string | undefined};
let a: A = { foo: "bar" }
let b: B = { foo: "bar" }
a = b; // okay
b = a; // not okay Update: the definition of |
I think the problem is that the symmetry between In an ideal world there would be a difference interface A {
foo?: string;
}
interface B {
foo: string | undefined;
}
interface C {
foo?: string | undefined;
}
let a: A = { }; // OK
let b: B = { }; // Error, missing 'foo'
let c: C = { }; // OK
a.foo = undefined; // Error, undefined is not string
b.foo = undefined; // OK
c.foo = undefined; // OK
setState(a, { }); // OK
setState(a, { foo: undefined }); // Error
setState(b, { }); // Error, foo missing
setState(b, { foo: undefined}); // OK
setState(c, { }); // OK
setState(c, { foo: undefined}); // OK We've sort of swept this under the rug, but with Object spread and |
@RyanCavanaugh I agree with you, however the example for |
@ericanderson I think the playground uses strictNullChecks: false |
@RyanCavanaugh If |
Ok, those examples from @ericanderson and @RyanCavanaugh are helpful. I now see that there are clear differences between "no key" and "key: undefined". Damnit, you know what this means? It means almost all of the interfaces in all of the code I've ever written are maltyped. Pretty much wherever I have said, I can't think of a single place in my code where I've made an optional property but wish for my compiler to insist that if the property exists then it must not be With that in mind, I now am leaning towards Pick as the correct expression of setState, and I just need to update all of my interfaces. |
Okay, I found my repro, it fails with interface C {
foo?: string | undefined;
}
let c: C = {}; // OK
let c2: C = {
foo: undefined,
} // OK
function setState<T, K extends keyof T>(obj: T, state: Pick<T, K>) {
for (let k in state) {
obj[k] = state[k];
}
}
setState(c, c2); // !OK = ERR Argument of type 'C' is not assignable to parameter of type 'Pick<C, "foo">'. |
Nevermind. I withdraw my complaint. In my above example, there is no way for TS to know that c2.foo, in general, was given a value or was left off. Thus if it promises that it returned a Pick<C, "foo">, then foo MUST be defined, which we can't guarantee. While this isn't my ideal, it seems to be correct, and to go back to the source of this (DefinitelyTyped/DefinitelyTyped#13155), Thanks again @ahejlsberg & @RyanCavanaugh |
I'm confused again. How could that possibly be right? It shouldn't matter to TS whether "c2.foo, in general, was given a value or was left off", because we've said that both are allowed. |
C cannot be assigned to Pick<C, "foo"> because later when you use the Pick version it MUST have a param of foo that is on the object. C isn't guaranteed to have that |
Yes, but how could that possibly be the correct behavior?
|
Yeah, that can't be right. You need to be able to reset some state, for instance the id of a timeout when it's over/cancelled should be put back to undefined. Since a React State is encapsulated, it's not overly helpful to make some of its keys optional, but just removing the interface C {
foo: string | undefined;
}
let c: C = {}; // OK
let c2: C = {
foo: undefined,
} // OK
function setState<T, K extends keyof T>(obj: T, state: Pick<T, K>) {
for (let k in state) {
obj[k] = state[k];
}
}
setState(c, c2);
// error TS2322: Type '{}' is not assignable to type 'C'.
// Property 'foo' is missing in type '{}'. Back to square 1! (and that's just the shallow merge :D) |
let c: C = {}; // OK Does that actually work? @RyanCavanaugh's example above that didn't work. |
Just to be clear, my comment above was normative not descriptive. Today we considered |
If we are considering {x?: number} to be the same as {x: number | undefined} currently, then currently there is a bug, given the example I pasted above, no? |
We've been discussing this and have come to the consensus that |
Thanks @ahejlsbeeg Will this be in 2.2 or the next 2.1 |
You guys are the best. Thank you. (By the way, mapped types are an amazing feature; I love them. In fact, 2.1 is an amazing release all around. Ya'll are killing it over there). |
I just want to remark that the rapidity of this feedback loop is awesome! An issue comes up, is discussed and gets resolved in less than 2 days, and this is a programming language we are talking about. Great work! |
sure thing mapped types got some love, let's not skip the legs day tho |
I found another problem with using declare class Component<P, S> {
setState<K extends keyof S>(f: (prevState: S, props: P) => Pick<S, K>, callback?: () => any): void;
}
declare const component: Component<{}, { foo: number; bar: string; }>;
component.setState(() => 0 === 0 ? { foo: 0 } : { bar: '' }); The last line raise an error, which says
|
I think if |
Yeah, typescript has a lot of issues inferring from function returns. I don't think it's an issue with Pick especially. |
The compiler should probably infer the return value to be 'Pick<T, "foo"> | Pick<T, "bar">' instead. Then it could infer that both of those are assignable to 'Pick<T, K>' I believe. |
@ericanderson I was misunderstanding
|
Well, certainly the issue I described is not specific to function f(s: string): void;
function f(n: number): void;
function f(arg: string | number): void {}
f(''); // no error
f(0); // no error
f(0 === 0 ? '' : 0); // error: Argument of type '"" | 0' is not assignable to parameter of type 'number'. Type '""' is not assignable to type 'number'. It is likely that although you can define an argument of a function as one of multiple types, it must have a single type per use. |
this is not strict enough for strictNullChecks see: microsoft/TypeScript#12793
I don't totally understand the @ahejlsberg solution - we're supposed to redefine setState on every component to get TS to settle down about setState with a partial object? |
I agree with @lukecwilliams's not getting how this is solved, especially given the title of this issue. The following does not work, and is a not-so-uncommon way to partially build up a state somewhere, to then apply it with interface State
{
a: string;
b: number;
}
class Comp extends React.Component< { }, State >
{
foo( )
{
const state: Partial< State > = { };
if ( !0 ) // obviously apply some useful logic here
state.a = "foo";
this.setState( state );
}
} It fails with:
Yes, one can Hawaii-cast things to make this.setState( state as State ); or, pick your poison: const state = { } as State; but this is not a solution. Or is it? I'm personally resentful to blatantly lie about types this way. It's a bad habit. |
I have another example of some strangeness around the Pick infrastructure as used in React types specifically. Unfortunately I don't fully understand it but this is something I ran into: typescript version is 2.6.2 and using "@types/react": "^16.0.31" export type ExportProperties = {
hpid: boolean,
gender: boolean,
age: boolean,
diagnosisCode: boolean,
medicationCode: boolean,
procedureCode: boolean
};
export class StudyExportComponent extends React.Component<{ study: Study }, ExportProperties> {
constructor(props: { study: Study }) {
super(props);
this.state = {
hpid: true,
gender: false,
age: false,
diagnosisCode: false,
medicationCode: false,
procedureCode: false
};
this.handleCheck = this.handleCheck.bind(this);
this.handleCheck2 = this.handleCheck2.bind(this);
this.handleCheck3 = this.handleCheck3.bind(this);
}
handleCheck(name: keyof ExportProperties) {
return (event: any) => {
let state = {};
state[name] = event.target.checked;
this.setState(state);
};
}
handleCheck2(name: 'hpid' | 'gender' | 'age' | 'diagnosisCode' | 'medicationCode' | 'procedureCode') {
return (event: any) => {
let boolValue: boolean = event.target.checked;
this.setState({ [name]: boolValue });
};
}
handleCheck3(name: keyof ExportProperties) {
return (event: any) => {
let boolValue: boolean = event.target.checked;
this.setState({ [name]: boolValue });
};
}
// Clipped rendering stuff With the following errors:
And this one for handleCheck3:
My work-around is clearly in handleCheck which does not have any TS errors but as you can see is clearly working around types in this case (at least I think it is). I also just noticed that this compiles just fine: handleCheck(name: keyof ExportProperties) {
return (event: any) =>
this.setState({ [name]: event.target.checked } as Pick<ExportProperties, keyof ExportProperties>);
} Now that I look at this a little more maybe that's the appropriate thing to do... And in fact when I change the property type of the function parameter name to something like name: 'catfish' I definitely get errors about casting the Pick<ExportProperties, keyof ExportProperties> which is exactly what I wanted. So I think this last example is how it should be done. Now just to be clear none of this is failing the most common use-case as shown below: this.setState({ hpid: true }); Hopefully this post is helpful to others even if not actionable to the Typescript language. Also cheers to the Typescript team and @types/react who have made this typing on top of react so freaking helpful. Everything I do in my tsx html and of course my state and props variables are all fully typed due to this work. It's incredibly useful. |
I think you're types could be improved. Specifically to prevent the problem, change the type of Unrelated but tip, if you use => notation for your methods, you dont have to manually bind them, they are bound for you. |
Traditionally in TS, the absence of value or an explicit undefined value was exactly the same thing.
Perhaps it still even makes sense for Partial, but I thought Partial was meant to solve the React setState problem; and I believe it's still not solved today.
TypeScript Version: 2.1.4
Code
Expected behavior:
A non nullable property should not be updatable with null/undefined with strictNullChecks.
We need to be able to say "give me an object that
By the way, I don't even use React, but this is a fairly common and useful behavior to have.
The text was updated successfully, but these errors were encountered: