Definitelytyped: React.d.ts ReadOnly<T> in state and props

Created on 25 Jan 2017  ·  91Comments  ·  Source: DefinitelyTyped/DefinitelyTyped

Hi @ericanderson

I'm having many problems with this change when used in practice:

Problem 1: Go To Definition

When you press Go To Definition on a property of props or state, Typescript is not able to resolve it.

interface MyComponentProps {
    name: string;
}

export abstract class MyComponent extends React.Component<MyComponentProps , void> {
    myMethood() {
       this.props.name; //<-- Go To definition in name
   }
}

image

Makes sense because the member is synthetically generated, but is annoying anyway.

Problem 2: Hierarchies of components (Generic P with constraints)

More important, if you make abstract component like this:

interface MyBaseProps {
    onChange?: (val: any) => void;
}

export abstract class MyBase<P extends MyBaseProps> extends React.Component<P, void> {
    myMethood() {
        this.props.onChange!(2); //The type is S["onChange"] instead of (val: any) => void and so is not invocable. 
   }
}

TS is able to show that there is a property onChange, but sometimes can not discover his type.

image

This is the most important change since if prevents me to have hierarchies of components that share common props and functionality. It looks like a problem in TS compiler but until it's fixed.

Problem 3: Not so Readonly.

While I agree that this change captures well the functional intent of React, there are valid situations where you can modify the state imperatively, like in the constructor, and also if you change the state and call forceUpdate everithing works OK.

C# this.state.name = "John"; this.forceUpdate(); //Ok as long as you don't setState afterwards, but calling setState also is annoying with the callback.

Is it recomended? No.
Is it forbidden? Also not, otherwise forceUpdate won't exist.

Of course you could cast the state to S (or any) and make the change, but if it's a common pattern gets cumbersome.

Conclusion: Is it worth?

I feel sad that the new shiny feature of TS makes more problems than solutions in this case, but I honestly think this is the case here.

On the other side, the change of setState is great 👍 , didn't know about Pick<S,K>.

Most helpful comment

Problem 3 is up for debate I guess.

You are correct that you can _technically_ do the above example in React, but I would definitely argue that its not the way React was intended to be used.

This can be broken down into 3 distinct cases.

Generic Initialization

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

class Foo extends React.Component<Props, State> {
  public state: State = {
    bar: 5,
  };
}

Initialization based on props

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

class Foo extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      bar: props.baz,
    };

    // or
    this.setState({
      bar: props.baz,
    });
  }
}

Random assignment with forceUpdate

Given that I think its better to push people towards the "right" thing, you can easily work around this issue by redeclaring public state:

interface State {
  bar: number;
}

class Foo extends React.Component<{}, State> {
  public state: State;
  public myMethod() {
    this.state.bar = 5;
  }
}

All 91 comments

What version of typescript is your visual studio using?

@vsaio for sa

For problem 1, with TS 2.1.5 and the latest VSCode, this works fine for me. I don't have windows/VS so I can't check there, but I would bet there are updates to your plugins or you're not on TS 2.1.5

Same for Problem 2

VS 2015 with TS 2.1.5.0

Problem 3 is up for debate I guess.

You are correct that you can _technically_ do the above example in React, but I would definitely argue that its not the way React was intended to be used.

This can be broken down into 3 distinct cases.

Generic Initialization

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

class Foo extends React.Component<Props, State> {
  public state: State = {
    bar: 5,
  };
}

Initialization based on props

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

class Foo extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      bar: props.baz,
    };

    // or
    this.setState({
      bar: props.baz,
    });
  }
}

Random assignment with forceUpdate

Given that I think its better to push people towards the "right" thing, you can easily work around this issue by redeclaring public state:

interface State {
  bar: number;
}

class Foo extends React.Component<{}, State> {
  public state: State;
  public myMethod() {
    this.state.bar = 5;
  }
}

My issues are with the generics variance. specifically for typing within the class that is generically typed. below is a pretty minimal sample of where things break down.

class TBaseState {
  public value: string;
}

function globalFunc<T extends Readonly<TBaseState>>(item: T) {
}

class MyComponent<TProps, TState extends TBaseState> extends React.Component<TProps, TState> {
  broken() {
    // typing of this.state is Readonly<TState>
    // this is not assignable to Readonly<TBase>
    globalFunc(this.state);

    // this is a horrible hack to fix the generics variance issue
    globalFunc(this.state as TState as Readonly<TBaseState>);
  }
}

class MyState extends TBaseState {
}

let component: MyComponent<any, MyState>;

// here the typing of component.state is Readonly<MyState>
// this is assignable to Readonly<TBase>
globalFunc(component.state);

I am in TS 2.1.5.0

image

but could be that in VS we have a worst TS experience than in VS code...

for Problem 1, go to definition TS also doesn't work in VS Code:

interface MyComponentProps {
    name: string;
}

export abstract class MyComponent extends React.Component<MyComponentProps , void> {
    fullName: string;
    myMethood() {
       this.props.name; //<-- doesnt work
       this.fullName; //<-- works
   }
}

for Problem 2 it's true that VS Code behaves better:

image

while VS looks confused:

image

I think for VSCode and problem 1, its working because I'm using the plugin for the "Latest Typescript and Javascript Grammar" which must have smarter handling.

@patsissons thats an interesting example, although I think its more representative of a bug in typescript than a bug in the definition file. For example, setState used to take S which meant to do partials we used to have to do weird tricks like setState({foo:5} as any as State) or use the one that takes a function. I'm not sure the lack of expressivity of the compiler makes the typings "wrong". I think this is a decent argument for a change to the README to mark this edge case.

Have you filed an issue on TS?

So this change nowadays breaks all the VS and disables Go To Definition in all the VS Codes except if you have a plug-in...

Also there is the completeness argument. There are zillions of APIs that are meant to be Readonly and are not nowadays, just in React.d.ts

 interface ComponentLifecycle<P, S> {
        componentWillMount?(): void;
        componentDidMount?(): void;
        componentWillReceiveProps?(nextProps: Readonly<P>, nextContext: any): void;
        shouldComponentUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: Readonly<any>): boolean;
        componentWillUpdate?(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: Readonly<any>): void;
        componentDidUpdate?(prevProps: Readonly<P>, prevState: Readonly<S>, prevContext: Readonly<any>): void;
        componentWillUnmount?(): void;
    }

I think readonly should be used for 'freeze' or 'Inmmutable.js' not for the long tail of thinks that are not meant to be modified, like event objects, for example.

Haven't filed, im just retrofitting my code today to handle the new Readonly<T> types, this was a case i ran into that I didn't have a properly typed solution to. Go ahead and file an issue, i will be busy most of the rest of the day today with patching code.

Ah yes, I knew I missed some. @olmobrutall If we keep the state changes I introduced to mark as readonly, I agree that those methods should be updated. I feel like we need a consensus on the right thing to do first though.

As for the VS breaks, I don't know whats right. Should the types be held back because some tools aren't staying up to date?

@patsissons You could always provide your own typings for react for now if you want to wait to see how this pans out before updating all your code. https://ericlanderson.com/using-custom-typescript-definitions-with-ts-2-x-3121db84015d#.ftlkojwnb

in our experience, VS is always a little behind. Our shop uses vscode to do any active typescript development and VS is used more to simply patch code files or for non-typescript developers to look through code, not necessarily for active development.

@ericanderson the hack is not so bad for now, i just have to winnow Readonly<T> to get my T which is assignable to the Readonly<Base>.

We talking about 'react.d.ts', this single member declaration is massively used. I think is worth to hold back till VS is ready.

Also, like 50% of the types in the world are meant to be readonly, like objects you get from APIs, I don't think we need to annotate that.

I think Readonly should be used for objects that have been explicitly converted to have get-only properties. Like freeze.

@olmobrutall Readonly is new, so the exact best practice isn't really defined. I would personally prefer everything declare that it takes a Readonly<> of things to help signify that it will not mutate it. Similarly, React does not expect you to modify state outside of setState and thus this change ensures that accidents don't introduce bugs, which is one of the primary benefits of using TypeScript over javascript.

If performance was more consistent across browsers for Object.freeze, I would imagine the React folks would actually start freezing after setState.

What's the purpose of forceUpdate then?

I'm curious about other peoples thoughts on how DefinitelyTyped should work with respect to tooling updates as well as philosophy about Readonly on react (and other libraries whose intent is that you dont modify certain objects).

cc/ @johnnyreilly @vsaio @pspeter3 for thoughts on react specifically and other thoughts in general
cc/ @andy-ms @mhegazy for thoughts on how DefinitelyTyped should proceed philosophically for tooling updates and zealous use of Readonly

@olmobrutall we use forceUpdate to queue a render on the react side, driven from observable events on the state side.

UPDATE:
I'll clarify our scenario a bit so it is not misunderstood. Our state objects are long living immutable objects (so Readonly<T> is actually very suitable for us). These state objects contain multiple rxjs observable streams that funnel into a notification observable called stateChanged. React components watch this observable for events and funnel those events into a call to forceUpdate (after debouncing). In effect, our mutable state lives within the state, but the state itself and the members that exist in the state are all immutable. This is certainly not the standard usecase for React, but it is a very similar in flow. We simply have smart state objects that know how to inform the components when a re-rendering is required.

@ericanderson the main issue is that these type definitions are suffering from SemVer issues. Because the type definition versions are largely tied to their respective module versions we end up with a minor version bump that brings in breaking type definition changes, which means we have to pin @types versions in our package.json file.

@olmobrutall From the docs of react:

Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render().

The react guide in fact tells you not to update state directly: https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly

forceUpdate, as I read it, is only for forcing your component to update when your component is based on data _not_ in your props or your state.

@patsissons I may be wrong, but I believe SemVer is designed to be backwards compatible with APIs and semantic intents. Just because you use a library in a way it was not intended (as per the docs) does not mean that the library must keep supporting said unintended uses. Library authors are well within SemVer for changing semantics that were incorrect but happened to be used by some people.

That said, perhaps Readonly<> to state is too big of a change, but suppose for a moment that it is the right change. When should it be released into DefinitelyTyped? Your code will always suffer the need to change once you get the update that finally marks state as Readonly<>.

I still don't know whats right about Readonly<> being applied to state which makes it hard to debate semver, or tooling, or anything else. My gut was that it was right. The people who reviewed the change never brought it up as an issue. It seems inline with the intent of the React team.

I'm happy to defer to any of the reviewers for react in DefinitelyTyped (I cc'd them all above).

So the Observable changes the state an you forceUpdate? So changing the state imperatively is some how allowed.

I ageee that where Readonly should be used is not 100% defined. But do we need to start with a controversial and massively used property before the tools are ready?

I'm all for strongly typed, I mantain 6 projects all with strictNullChecks, but the benefits here are much smaller than the problems it currently produces.

@ericanderson I believe SemVer2 is designed to allow node version declarations like ^15.0.0 and expect that any minor or patch upgrades (i.e., 15.0.1 or 15.1.0) to the module are transparent or at least backwards compatible from an external perspective. Any major upgrades (16.0.0) would require an adjustment to the version declaration to bring in the change. This essentially gates breaking changes from being brought into the typing system. But currently type definition versions cannot deviate from their respective module versions major version (by convention), which results in this discontinuity.

The short version is that type definitions can have breaking changes introduced without the module itself changing at all, and breaking changes require a major version bump.

But you won't make a PR removing forceUpdate, do you?

Then if forceUpdate is there, changing the state imperatively should be there too.

In theory you should use a deep inmutable state graph, but very often changing the State imperatively is just fine and not all of the developers buy into the persistent data structures idea.

Fortunately React allows a scape route and make direct changes in the state possible, are we going to forbid TS developers to take that route? Isn't that too paternalistic?

Vue.js for example promotes the imperative changes, I won't be surprised if it influences React.

Also I was reading a blog post of some React author not long ago (I can recall) encouraging using React without all the Redux ceremony.

My position otherwise is to get types definitions published as soon as possible, my only concern is automation. since given the current state of type definition versioning a successive build with no source changes can break. These types of non-deterministic CI failures are troubling. Nobody wants to see their build break because they pushed a change and then find out their change hand nothing to do with the build breaking.

After sleeping on it my personal opinions are as follows:

  • Good practice would involve using yarn or npm locks, so you wouldn't be getting a surprise unless you upgraded locally first.
  • Making state readonly is how React is intended to be used. The documentation and examples confirm this.
  • The workflow where you don't want to use setState is within your code base and within your own rights. You are correct that React provides forceUpdate, but it's use is intended for causing a render when you are outside of the intended use case. Thus, if you don't want to use state as intended that's fine, but at that point you don't need to use the instance variable state. In fact you can just use regular private variables.
  • Yes this project is depended on by a lot of people, and yet thus far these are the only two complaints thus far making me think this is not a wide spread issue. Further the issue raised about the global function can just be rewritten to take the generic differently (see linked TypeScript issue)

Given the above thoughts as well as the work arounds for non-standard React apps, I think Readonly is correct and the only needed change for completeness is to update the life cycle methods to match.

I agree that these changes completely make sense, my use case that was causing issues was a very unique corner case that should seldom ever be encountered. By adapting my code base to a readonly props and state I caught some otherwise undetectable typing issues. There is no doubt that getting the changes published was the correct choice.

Patsisson, using VS, VS Code or any other editor?

My point is that while React promotes a functional approach, it allows a videogame-like workflow where u make changes to the world imperatively (state) and then render (forceUpdate). This approach is now forbidden in TS. Kind of func-damentalism :)

I'll think of alternatives to make my current ecosystem workable then...

Problem 2 throwing error only with strictNullChecks.

[TS] Cannot invoke an expression whose type lacks a call signature. Type '((val: any) => void) | undefined' has no compatible call signatures.

+1 I'm using strictNullChecks

@ericanderson ?

As stated above, this is related to the tooling, which is clearly outside the scope of DT. If you are on VSCode and install the preview of the upcoming TS parser, I did not see this issue with strictNullChecks when I was writing any of the above.

I do not have windows so cannot speak to VS proper.

this Pick patch broke suggestions under VSCode. when doing this.setState({ | }) (Ctrl + Space), nothing is shown, even though the State is clearly defined and using Partial<State> because setState can set members state selectively

IMHO the correct code should be setState(state: Partial<S>, callback?: () => any): void;

As discussed in the original pull request branch, we started with Partial. However if your state object is:

interface State {
foo: string;
}

Then with partial you could do the following:

setState({foo: undefined});

Which is clearly wrong.

I'm sorry - but again about Readonly

React is not only Redux and setState. React is also mobx and other observable patterns, where assigning of state properties is the MAIN FEATURE. The discussed change fully kills the mobx usage with typescript.

So why add the behavior which doesn't exist in original react code to .d.ts file? .d.ts is to be reflection of original library but not the teaching people to the right coding style, according to the author point of view!

@lezious, I'm afraid I don't understand your position. Can you provide a code sample and what is broken about the typings in respect to the sample? Thanks!

No problem:

this is my state class

class UserInfoBlockState  
{
    @observable                  <- this is mobx way to declare state
    public updating: boolean;
    @observable 
    public deleted: boolean;
}

and this is my component

@observer       <-- this is mobx way to make component react to state change
export class UserPanel extends React.Component<IUserInfoBlockProps, UserInfoBlockState>
{
   ......
     private updateUser()
    {
        this.state.updating = true;
        UsersAPI.update(this.props.user)
       .then(() =>
            {
                this.state.updating = false;      <--- this is the mobx way to work with the state
            }
        ).catch(() =>
            {
                this.showErrror("Server error");
                this.state.updating = false;
            });
    }
   ....

}

and now we (our company with huge project written on react+mobx) updated the DT and React on start of new release circle and ... 3000+ compilation errors "property is readonly". Wow. What you suggest me to do - rewrite whole project to redux, never update react.d.ts or always keep and support forked version?

@mweststrate, plz check this.

@Iezious I appreciate your position and I ask that you please calm down. I'm trying to work with you. This has nothing to do with Redux, but pure React.

I don't want DT to block a use case that has worked previously, however I don't think how you describe the use of mobx with react is consistent with react documentation (nor even the mobx documentation now that I've read up on it).

React clearly states in the documentation that there are 3 ways to use state correctly, and the very first one is "Do not modify state directly".

This leads me to believe that the way your code base currently works is quite likely to break in future versions of react. In looking through https://github.com/mobxjs/mobx-react, I do not see any suggestion that you use state in this way. In fact, it seems they want you to use properties.

Reviewing https://mobx.js.org/getting-started.html and googling for "mobx react state", I fail to find any documentation that suggests you use mobx the way you are doing so.

DT is supposed to convey the spirit of the underlying library at best and the actual implementation at worst and it is clear that buying into react and extending component means respecting the implied contract.

I'm not sure what I suggest you do. A few options I can think of off hand:

  1. A "cheap" option, if you insist on taking over the state variable is to search and replace React.Component with MyComponent and defining MyComponent to be a subclass of React.Component without the readonly constraints.
  2. Another, based on the idiomatic examples posted in the documentation for mobx is to take the time to stop using this.state and just use variables on the actual React.Component. This may be a little painful but at least new people to your project will be able to see the patterns in your code base as they are described online.
  3. You could redeclare state in each component if you want to keep doing it similar to how you're doing.
  4. You could search and replace this.state with this.somethingElse and manually declare.
  5. You could stop taking updates to react from DT (and possibly in the future from react in general depending on how future changes might affect your use case).

If it were my project, I would probably do number 2, although I don't know enough about mobx to know for sure.

Sorry I couldn't come around to agreeing with you (doesn't mean others wont agree with you). I tried to find a reason to revert that part but I can't seem to get on board at this time.

I'll mention again our strategy, which is a custom application of RxJs observables to drive React state changes and rendering which closely resembles the mobx pattern. We use actions to receive input from the view (React) layer. Actions are synonymous with a function that consumes input and produces an observable, which would then further drive other state observables. This pattern allows the state to remain immutable from the React layer perspective, as you only ever query for observable values and execute state actions. Internally, your state may "mutate" itself as a result of the actions, as the internal state has no read-only constraints.

I can't calm down. I have the project in production and I need to spend huge amount of time of teams, and this means money since this change.

And what is the reason? Does this change reflect the reality in react? No. It adds the restrictions and behavior which does not exist in react, the restriction which is added by somehow just because he thinks that this is right.

What is the aim of DT project? To describe JS libraries as precise as possbile, or describe our vision of way of right usage of those libraries? According to name "Definitely Typed" it's first. So, if this restriction doesn't exist in original JS library, it MUST NOT exist in DT too. This is my point. Where am I wrong in this point?

I understand you're frustrated, however, this is the way that react was intended to be used by reading the docs. I hardly see how we should make DT less specific because your team abused the library and violated the implied contract.

Show me one ounce of documentation put out by the react team that suggests that state should be directly mutable and I will immediately change the code back.

https://facebook.github.io/react/docs/react-component.html#state

Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.

It seems pretty clear that react considers this.state immutable. React does not consider the _properties_ of this.state immutable (which is the redux assumption). You're free to do:

this.state.user.name = "foo";

in idiomatic react.

My preference is towards typing the API accurately (which in this case means Readonly) and expressing all of the invariants that the react team states.

@ericanderson sorry I've only just noticed this. FWIW I think the change is reasonable and that tooling will catch up. As a by the by have you heard they're considering deprecating the setState overload that takes an object? The future is the reduce style setState by all accounts.

@amoreland Disagree. Per: https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly

Do Not Modify State Directly

For example, this will not re-render a component:

// Wrong
this.state.comment = 'Hello';

Instead, use setState():

// Correct
this.setState({comment: 'Hello'});

The only place where you can assign this.state is the constructor.

@johnnyreilly I had not. Thats interesting. Source?

It was covered in one of the talks in the recent react conference. It's available on YouTube. It may have been the talk by Lin Clark. One of the first talks on day 1 - covering the upcoming changes to react for v16. Fiber etc

Sorry @gaearon i meant

The reason why mobx is performing state changes, is because the observables are driving React updates, instead of _replacing_ the state completely, the state becomes an engine that drives rendering. so by doing something like this.state.updating = true; you are actually doing the equivalent of a state replacement, but instead the state is smart enough to trigger a new render by notifying the component that the state is altered from its previous content. I agree that this is not _conventional_ React usage, but rather a much more comprehensive and higher level usage of React. I would argue that conventional React usage is only useful for small projects with a handful of components. When you end up with 100's of components each with multiple dozens of reactive mutation drivers, you can no longer reliably use conventional React state alterations (i.e., setState) and must being to entertain architectural changes that involve _smart_ state (which is what mobx essentially does).

So that said, I understand why he is upset, because the typing changes are affecting a more advanced usage of the React system. The observable state assignments are not technically _mutating_ state, but rather invoking observable events for the RHS value. It just so happens that the syntax that mobx has chosen to issue these observable events conflicts with the express intent of React immutable state.

@Iezious if you need a quick fix for this type of problem, you can kind of get away with it by augmenting the React typings and refactor your components to use an extension to the React.Component type defs.

import * as React from 'react';
declare module 'react' {
  class MutableStateComponent<P, S> extends React.Component<P, S> {
    state: S;
  }
}
(React as any).MutableStateComponent = React.Component;

and now you can just create mutable state components just like regular components, except their state member is no longer marked as readonly.

export class MyComponent extends React.MutableStateComponent<MyProps, MyState> {
  // this.state.name is writable
}

@patsissons yes, that exactly the reason.

I'm not mutating the state, I'm using mobx observables, which calls setState for me, I'm doing it since my HUGE project code looks much more clear and understandable.

I know that I can make my component, I also can in my npm server make script wich will always revert this change for our company. I can use hack like "this.state.state.updated" and lot of any other hacks.

I just want to say, that this change affects using observable patterns like mobx, which in reality follow the react way, but now, since this change can't be compiled and need some hacks and workarounds to work. And that's why I think that this change is not right.

perhaps instead of my suggested MutableStateComponent we instead call it ObservableComponent that is more aligned with the Observable React pattern.

If you drop Readonly then people who are using the react types with regular react (and/or any number of other state-management systems aside from mobx) are exposed to errors. I don't use mobx in my huge project and I appreciate the compiler errors when someone makes a typo and accidentally uses this.state.foo = bar.

If there is an unavoidable tradeoff between standard react and non-standard react usage the standard react types should lean towards the former.

Further, if you use mobx the idiomatic way, this isnt a problem.

If you drop Readonly then people who are using the react types with regular react (and/or any number of other state-management systems aside from mobx) are exposed to errors. I don't use mobx in my huge project and I appreciate the compiler errors when someone makes a typo and accidentally uses this.state.foo = bar.

So once again - you are TEACHING TO WRITE THE CODE

This project is not about teaching to write the code, this project is to describe existing functionality of JS libraries. The discussed limitation does not exist in the original library and must be deleted.

That's all.

@patsissons

if you need a quick fix for this type of problem, you can kind of get away with it by augmenting the React typings and refactor your components to use an extension to the React.Component type defs.

is not right. In the enterprise world, from where I am, there are no "quick fixes". When something changes in SDK's it or MUST be backward compatible, or it takes years. There is no quick fixes in 2M+ lines of code project. It's weeks of changes, testing, A/B prod testing, roll-outs for big number of people. It costs real money. And all this huge effort just because of someone added not backward compatible change which DOES NOT EXIST IN REAL LIBRARY?

Why do you think the forceUpdate still exists in the react? They are talking on confs about the right style, but making the changes to prevent the other ways of usage. Why? Since it's big company and they know that libraries must be backward compatible.

@ericanderson wrote that

this.state.state.value = 1 

is not legit too from his point of view. Next time he will obtain the tool from TS and add the limitation which will prevent this code. Or make component final class, or something else just because "it's right style and prevent people from making the mistakes".

Preventing people from mitakes - it's the task of FB, if they want to do it, they can easy add proxy and disallow state mutation. The purpose of DT is add descriptions, and nothing else.

@Iezious

I think the point is that the React team cannot make state immutable with JavaScript, but if they could they would have. TypeScript on the other hand can make state immutable, so that is why this change was made to the type definitions. It was absolutely the intent of the React team to disallow modifications to state members directly (as evident by their official docs on using state correctly), but they did not have the language constructs to impose that constraint. This constraint has never been an unknown, it has been well documented from the beginning. For those of us operating outside of the _conventional_ usage of React, we must at least adhere to the React official usage recommendations. For my team, that meant architecting a solution that allowed us to drive state changes without directly mutating state. This was specifically done to ensure that we were not going to encounter any issues such as this one (even though this change did affect us slightly, but only through function signatures and not fundamental design).

If refactoring is not possible in your situation then either pin @types/react at 15.0.1 before the change was made, or just don't use @types/react and instead maintain your own variant of the type defs and simply alter the state typing for Component. I really don't think you'll have any success convincing anyone to revert the change.

forceUpdate exists because it is documented as the recommended code path for driving rendering when internal structural state is modified (or when render() uses data outside of state that is mutable). The usage of forceUpdate is designed for exactly the usage pattern that my team uses with React.

React team CAN make state immutable with JS, It's easy. But not backward compatible.

Once again, is:

this.state.state.value = 1 

legit or not?

I think it is, but my opinion is clear already...

I don't think the conversation about mutability / immutability is relevant _yet_. Clearly the bug in the TS compiler (regarding Readonly) combined with this change makes generic components completely impossible to use, breaking lots of existing code. Surely that's a universally accepted valid use-case and is enough reason to roll this back for now???

interface test1 {
    num: number;
}

function add<T extends test1>(initial: T, add: number) {
    var copy: Readonly<T> = initial;

    //ERROR HERE: [ts] Operator '+' cannot be applied to types 'T["num"]' and 'number'.
    return copy.num + add;
}

Does anyone know if there is an open issue with the Typescript team about this? I can't seem to find the relevant issue on their tracker.

@caesay See Microsoft/TypeScript#15501

i have to init state in constructor, but tslint shows "state is readonly"....

constructor(props) {
  super(props);
  this.state = {
     value: props.defaultValue,
  };
}

how can i fix it...............

Use setState

setState not works in constructor

Or consider componentWillMount w/setState

thanks

Hello,

I read the whole thread but it is not clear to me whether there are plans to handle @alanwei0 scenario?

I fully agree that it makes sense to have state as ReadOnly. That being said not being able to set the initial state in the constructor complicates things quite a bit because render is called before componentDidMount is done.

@pawelpabich using this.state = { in the constructor is not a problem. You can assign to readonly variables in the constructor, and Readonly<T> doesn't prevent assignment (ever!).

interface TInterface {
    test: string;
}

class TClass {
    readonly blah: Readonly<TInterface>;
    constructor() {
        this.blah = { test: "constructor" };
    }

    fn = () => {
        this.blah = { test: "fn" };
    }
}

The only error here will be inside fn - not because of Readonly<T>, but because of the readonly keyword. In fact, the latest version of the typings doesn't even use the readonly keyword, so you can actually assign the state anywhere, just not change properties inside state.

The issue we were talking about here was a bug with the typescript compiler that caused the state properties to lose their types in inherited components. This has since been fixed I believe, and if so this issue can be closed.

@caesay sorry, I thought that was the case we are talking about here. My problem is that I can't set the state in the base class. I'm on TS 2.4.1. Do you by any chance the id of the issue so I can check that?

You can always call setState (in componentWillMount).

@pawelpabich Again, this isn't an issue :) you can't assign the state from the base class on purpose. How could you? you don't know the prop contract being used by the derived component.

interface BaseCompState {
    baseState1?: string;
}

class BaseComp<TState extends BaseCompState> extends React.Component<any, TState> {
    constructor(props) {
        super(props);
        this.state = {
            baseState1: "fromBase",
        };
    }
}

interface TopCompState extends BaseCompState {
    topState1?: string;
}

class TopComp extends BaseComp<TopCompState> {
    constructor(props) {
        super(props);
        this.state = {
            baseState1: "fromTop",
            topState1: "fromTop",
        };
    }
}

That's a simple example of a derived component (props omitted, but same idea). the this.state = in the base class obviously can't work, because it doesn't know what TState is. furthermore, if it _did_ work, then it would just be over-writing the state set by the parent. The final state would be { baseState1: "fronBase" }. What happened to the topState property?

If you're absolutely convinced you need to handle state in your base component, you could pass the state from the derived component into the base component constructor (as TState so you can assign it) - that might look something like this:

interface BaseCompState {
    baseState1?: string;
}

class BaseComp<TState extends BaseCompState> extends React.Component<any, TState> {
    constructor(props, state: TState) {
        super(props);
        this.state = Object.assign({
            baseState1: "fromTop",
        }, state);
    }
}

interface TopCompState extends BaseCompState {
    topState1?: string;
}

class TopComp extends BaseComp<TopCompState> {
    constructor(props) {
        super(props, {
            topState1: "fromTop",
        });
    }
}

Or even simpler yet, you could call this.setState( from within your base component (yes, you can do that in the constructor!)

There is no issue here.

@caesay I fully agree that if there are no constraints then the assignment does not make sense. But following code still generates compilation errors though compiler has all required information to verify that the code is correct.

import * as React from "react";

/* tslint:disable:max-classes-per-file*/

interface BaseProps {
    baseProp: string;
}

interface BaseState {
    baseState: string;
}

class Base<TProps extends BaseProps, TState extends BaseState> extends React.Component<TProps, TState> {
    constructor(props) {
        super(props);

        this.state = {
            baseState: props.baseProp
        };
    }

    render() {
        return <div>{this.state.baseState}</div>;
    }
}

interface DerivedProps extends BaseProps {
    derivedProp: string;
}

interface DerivedState extends BaseState {
    derivedState: string;
}

export class Derived extends Base<DerivedProps, DerivedState> {
    constructor(props) {
        super(props);

        this.state = {
            derivedState: props.derivedProp
        };
    }

    render() {
        return <div>{this.state.derivedState}</div>;
    }
}

Errors

webpack: Compiled successfully.
ERROR at Test.tsx(17,9):
TS2322: Type '{ baseState: any; }' is not assignable to type 'Readonly<TState>'.

ERROR at Test.tsx(39,9):
TS2322: Type '{ derivedState: any; }' is not assignable to type 'Readonly<DerivedState>'.
  Property 'baseState' is missing in type '{ derivedState: any; }'.

Version: typescript 2.4.1

First. Your props in base in the constructor is not typed. Thus props.baseProp is any, which is not assignable!

Second, your props in Derived have the same problem AND you are missing baseState. Which of course will not work, regardless of Readonly

TProps extends BaseProps which means that TProps has at least the same members that BaseProps has. So how is that not defined? I understand that compiler might not be able to infer it but saying that is not defined doesn't seem to be correct. Same thinking can be applied to Derived.

@caesay setState in constructor is not a reliable solution because this method is async and you can still get to render without initial state set.

The only reliable solution I can see is to set the whole state in the derived classes. This has an obvious drawback that:

  1. This needs to be duplicated in every derived class
  2. Derived classes need to know about state they don't care about.

The example I showed above would work in C# so it would be nice if it could work in TypeScript.

  1. ~setState is safe in the constructor~
  2. In the case of a derived class, the best case that I can see is to call setState in your componentWillMount. Your base doesn’t know what should be in the state for its child, so it cannot possibly get this.state into a safe configuration. However your subclass can call the parent’s componentWillMount and then set whatever state it thinks it also needs.
  3. There are language features in many languages that would be nice to have in typescript. Some are feasible. Others are not. Either way it’s not an argument for their inclusion.
  4. The errors you are seeing make sense. They don’t suggest a bug in typescript nor the typings. In each case you are literally trying to assign this.state with an object that doesn’t match the expected type.

EDITED, to reflect that I was incorrect about setState in the constructor and to add backticks for readability.

@ericanderson I don't think we are making any progress here. I showed you an example and I would appreciate if you we could focus on it. Otherwise it is hard to have a discussion.

Re setState it is not safe to use in constructor. It does not throw an error but it won't set the state before render happens. I have code that breaks because of that and React docs are very clear that is should not be used there.

@pawelpabich No, this is not the place to have this argument. You are fundamentally wrong in your understanding of the language. In the example you provided, you have not satisfied the "State" contract in EITHER of your assignments to the state.

For example, when you do

this.state = { baseState: props.baseProp };
// now the state is exactly { baseState: props.baseProp }

The state is exactly { baseState: props.baseProp } and this does NOT satisfied the requirement of TProps extends BaseProps (because we don't know what TProps is! it could have any properties in it).

After that, you are doing a _SEPARATE_ assignment.

this.state = { derivedState: props.derivedProp };
// now the state is exactly {  derivedState: props.derivedProp }

the state is now exactly { derivedState: props.derivedProp } (you have overwritten your base state assignment!!) and this doesn't satisfy DerivedState OR BaseProps.

You are totally misguided in thinking that this should work. If you have a problem with how basic variable assignment works take it up with the language designers in a new issue and get shot down over there so we stop getting notifications here please.

As a side note - you're ALSO overriding your base render() method, which means your base component will not be able to render anything. If you are convinced this is what you want, you can add a protected member getBaseState() and call this from the derived constructor when you set the state (so you don't have to duplicate the base state logic). But what I think you really want is to not use derived components at all. Try restructuring to use composition (where you have one object containing multiple child objects). I think you'll find it will turn out a lot cleaner.

I hardly want to step away from reading to create a new project just to debate this with you, but alas...

I will stand corrected about setState() in the constructor, but that doesn't change how I feel about using it in componentWillMount.

Working example of how this would be done:
https://github.com/ericanderson/set-state-example

Specifically, index.tsx:

import * as React from "react";
import * as ReactDOM from "react-dom";

/* tslint:disable:max-classes-per-file*/

interface BaseProps {
  baseProp: string;
}

interface BaseState {
  baseState: string;
}

class Base<TProps extends BaseProps, TState extends BaseState> extends React.Component<TProps, TState> {
  public componentWillMount() {
    this.setState({
      baseState: this.props.baseProp,
    });
  }

  public render() {
    return (
      <p>
        <code>this.state.baseState: </code>
        {this.state.baseState}
      </p>
    );
  }
}

interface DerivedProps extends BaseProps {
  derivedProp: string;
}

interface DerivedState extends BaseState {
  derivedState: string;
}

export class Derived extends Base<DerivedProps, DerivedState> {
  public componentWillMount() {
    super.componentWillMount();
    this.setState({
      derivedState: this.props.derivedProp,
    });
  }

  public render() {
    return (
      <div>
        <p>
          <code>this.state.derivedState: </code>
          {this.state.derivedState}
        </p>
        {super.render()}
      </div>
    );
  }
}

ReactDOM.render(<Derived derivedProp="its derived" baseProp="its basic" />, document.getElementById("main"));

@pawelpabich if you want to implement a polymorphic component with an initial state, you will need to make your base component abstract and create an abstract getInitialState() (or similarly themed) function to implement in your derived class. You only want to assign the state once, either in the constructor or with setState as @ericanderson has shown.

below is your example converted to a fully polymorphic solution, with complete separation of concerns with respect to state construction:

interface BaseProps {
  baseProp: string;
}

interface BaseState {
  baseState: string;
}

abstract class Base<TProps extends BaseProps, TState extends BaseState> extends React.Component<TProps, TState> {
  constructor(props: TProps) {
      super(props);

      this.state = this.getInitialState();
  }

  protected abstract getInitialState(): TState;

  protected getBaseState() {
    return this.props.baseProp;
  }

  render() {
      return <div>{this.state.baseState}</div>;
  }
}

interface DerivedProps extends BaseProps {
  derivedProp: string;
}

interface DerivedState extends BaseState {
  derivedState: string;
}

export class Derived extends Base<DerivedProps, DerivedState> {
  getInitialState(): DerivedState {
    return {
      baseState: this.getBaseState(),
      derivedState: this.props.derivedProp,
    };
  }

  render() {
      return <div>{this.state.derivedState}</div>;
  }
}

@patsissons thanks!

@caesay I admit I was wrong and for some reason did not see that the assignments will override each other. That being said the use of CAPS and ! did not help me get out of my hole.

@patsissons and @ericanderson focused on the problem and now we have a solution that others can use.

@pawelpabich I agree my mannerisms were less than professional - but understandably so considering I gave you several explanations, samples, etc, and you choose not to listen to me.

then it would just be over-writing the state set by the parent.

[_if you want to_] handle state in your base component, you could pass the state from the derived component into the base component constructor

[_if you want to handle the state in your derived component_] you can add a protected member getBaseState() and call this from the derived constructor when you set the state.

What @patsissons did was take the comments already mentioned here and provide a code sample - which shouldn't have been necessary. This isn't stackoverflow, and we don't often provide ready made code samples there either.

I am new to react and typescript, perhaps I don't know sth, but even though the app compiles with no error, warning and hint, I get a runtime error. Below is an example component. I attribute the error to Readonly state. If the app worked prior to the Readonly change, then after this change it stoped working and it gives no compile time errors.

import * as React from 'react';

export default class HomePage extends React.Component<any, Map<string, string>> {

  public componentWillMount() {
    const map = new Map<string, string>();
    map.set('aKey', 'aValue');
    this.setState(map);
  }

  public render() {

      return (
        <div className="home">
          <div className="greeting">
            Home page: {this.state.get('aKey')} // <-- I get an error here
          </div>
        </div>
      );
  }
}

The error:

homePage.tsx:12 Uncaught TypeError: this.state.get is not a function
    at HomePage.render (homePage.tsx:12)
    at eval (ReactCompositeComponent.js:793)
    at measureLifeCyclePerf (ReactCompositeComponent.js:73)
    at ReactCompositeComponentWrapper._renderValidatedComponentWithoutOwnerOrContext (

State should always be a plain keyed object afaik, so instead define state
as something like: { values: Map <string, string> } and read
this.state.values.get('aKey')

Op vr 29 sep. 2017 om 09:01 schreef Janusz Białobrzewski <
[email protected]>:

I am new to react and typescript, perhaps I don't know sth, but even
though the app compiles with no error, warning and hint, I get a runtime
error. Below is an example component. I attribute the error to Readonly
state. If the app worked prior to the Readonly change, then after this
change it stoped working and it gives no compile time errors.

import * as React from 'react';
export default class HomePage extends React.Component> {

public componentWillMount() {
const map = new Map();
map.set('aKey', 'aValue');
this.setState(map);
}

public render() {

  return (
    <div className="home">
      <div className="greeting">
        Home page: {this.state.get('aKey')} // <-- I get an error here
      </div>
    </div>
  );

}
}

The error:

homePage.tsx:12 Uncaught TypeError: this.state.get is not a function
at HomePage.render (homePage.tsx:12)
at eval (ReactCompositeComponent.js:793)
at measureLifeCyclePerf (ReactCompositeComponent.js:73)
at ReactCompositeComponentWrapper._renderValidatedComponentWithoutOwnerOrContext (


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/14250#issuecomment-333047367,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABvGhM5hDyRNyUeZuIiGeTZk1N-rfuA4ks5snJW5gaJpZM4LuDWV
.

Thanks, but it seems to be a pointless effort to declare state as Readonly<S>, for its nested members are not affected by the Readonly.

Its possible one day that Readonly will apply recursively, but for now, its on you to be sure you've handle it right. In your case, you should really declare ReadonlyMap or

interface State {
    readonly [key: string]: string;
}

or nested:

interface State {
    map: { readonly [key: string]: string };
}

We can use it for deep read only:

export type DeepReadonly<T> =
  T extends Array<any> ?
  ReadonlyArray<T[0]> :
  T extends Date ?
  T :
  T extends Function ?
  T :
  T extends object ?
  { readonly [P in keyof T]: DeepReadonly<T[P]> } :
  T;

export type Writable<T> =
  T extends ReadonlyArray<any> ?
  Array<WritableObject<T[0]>> :
  T extends Array<any> ?
  Array<WritableObject<T[0]>> :
  WritableObject<T>;

type WritableObject<T> =
  T extends Date ?
  T :
  T extends Function ?
  T :
  T extends object ?
  { -readonly [P in keyof T]: Writable<T[P]> } :
  T;
Was this page helpful?
0 / 5 - 0 ratings