Definitelytyped: React.d.ts 只读<t>在状态和道具</t>

创建于 2017-01-25  ·  91评论  ·  资料来源: DefinitelyTyped/DefinitelyTyped

@ericanderson

在实践中使用此更改时,我遇到了很多问题:

问题 1:转到定义

当您在 props 或 state 的属性上按 Go To Definition 时,Typescript 无法解析它。

interface MyComponentProps {
    name: string;
}

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

image

有意义,因为该成员是综合生成的,但无论如何都很烦人。

问题 2:组件的层次结构(带约束的通用 P)

更重要的是,如果你像这样制作抽象组件:

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 能够显示有一个属性 onChange,但有时无法发现他的类型。

image

这是最重要的变化,因为如果阻止我拥有共享通用道具和功能的组件层次结构。 它看起来像 TS 编译器中的一个问题,但直到它被修复。

问题 3:不是只读的。

虽然我同意此更改很好地捕捉了 React 的功能意图,但在某些有效的情况下,您可以命令式地修改状态,例如在构造函数中,并且如果您更改状态并调用 forceUpdate 一切正常。

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.

是否推荐? 不。
是禁止的吗? 也不是,否则 forceUpdate 将不存在。

当然,您可以将状态转换为S (或any )并进行更改,但是如果它是一种常见的模式会很麻烦。

结论:值得吗?

在这种情况下,TS 的新闪亮特性带来的问题多于解决方案,我感到难过,但老实说,我认为这里就是这种情况。

另一边, setState的变化很棒👍,不知道Pick<S,K>

最有用的评论

我想问题 3 有待讨论。

您可以_技术上_在 React 中执行上述示例是正确的,但我肯定会争辩说这不是 React 的预期使用方式。

这可以分为 3 种不同的情况。

通用初始化

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

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

基于 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,
    });
  }
}

随机分配forceUpdate

鉴于我认为最好将人们推向“正确”的事情,您可以通过重新声明public state轻松解决此问题:

interface State {
  bar: number;
}

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

所有91条评论

您的视觉工作室使用什么版本的打字稿?

@vsaio为 sa

对于问题 1,使用 TS 2.1.5 和最新的 VSCode,这对我来说很好。 我没有 windows/VS,所以我不能在那里检查,但我敢打赌你的插件有更新,或者你不在 TS 2.1.5 上

问题 2 相同

VS 2015 与 TS 2.1.5.0

我想问题 3 有待讨论。

您可以_技术上_在 React 中执行上述示例是正确的,但我肯定会争辩说这不是 React 的预期使用方式。

这可以分为 3 种不同的情况。

通用初始化

interface State {
  bar: number;
}

interface Props {
  baz: number;
}

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

基于 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,
    });
  }
}

随机分配forceUpdate

鉴于我认为最好将人们推向“正确”的事情,您可以通过重新声明public state轻松解决此问题:

interface State {
  bar: number;
}

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

我的问题与泛型差异有关。 专门用于在一般类型的类中键入。 下面是一个非常小的示例,说明了事情发生的地方。

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);

我在 TS 2.1.5.0

image

但可能是在 VS 中我们的 TS 体验比在 VS 代码中更差......

对于问题 1,转到定义 TS 在 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
   }
}

对于问题 2,VS Code 的表现确实更好:

image

而 VS 看起来很困惑:

image

我认为对于 VSCode 和问题 1,它可以正常工作,因为我正在使用“最新打字稿和 Javascript 语法”的插件,它必须具有更智能的处理。

@patsissons这是一个有趣的例子,尽管我认为它比定义文件中的错误更能代表打字稿中的错误。 例如, setState过去使用S ,这意味着我们过去不得不做一些奇怪的技巧,比如setState({foo:5} as any as State) ,或者使用带函数的技巧。 我不确定编译器缺乏表达能力是否会导致输入“错误”。 我认为这是一个不错的论据,可以更改 README 以标记这种极端情况。

你在 TS 上提交过问题吗?

所以现在这个改变打破了所有的 VS 并禁用了所有 VS 代码中的 Go To Definition 除非你有一个插件......

还有完整性论证。 有数以万计的 API 是只读的,而现在不是,仅在 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;
    }

我认为 readonly 应该用于 'freeze' 或 'Inmmutable.js' 而不是用于不打算修改的长尾思想,例如事件对象。

还没有提交,我今天只是改造我的代码来处理新的Readonly<T>类型,这是我遇到的一个案例,我没有正确输入的解决方案。 继续提交问题,我今天剩下的大部分时间都在忙于修补代码。

啊,是的,我知道我错过了一些。 @olmobrutall如果我们保留我引入的状态更改以标记为只读,我同意应该更新这些方法。 我觉得我们需要就首先要做的正确事情达成共识。

至于VS休息,我不知道什么是对的。 是否应该因为某些工具没有保持最新而保留类型?

@patsissons如果您想在更新所有代码之前等待查看结果如何,您可以随时提供自己的类型以进行响应。 https://ericlanderson.com/using-custom-typescript-definitions-with-ts-2-x-3121db84015d#.ftlkojwnb

根据我们的经验,VS 总是有点落后。 我们的商店使用 vscode 进行任何主动的 typescript 开发,而 VS 更多地用于简单地修补代码文件或供非 typescript 开发人员查看代码,不一定用于主动开发。

@ericanderson黑客现在还不错,我只需要筛选Readonly<T> T获得可分配给Readonly<Base>

我们谈论'react.d.ts',这个单一成员声明被大量使用。 我认为值得坚持到 VS 准备好。

此外,就像世界上 50% 的类型是只读的,就像你从 API 获得的对象一样,我认为我们不需要注释。

我认为 Readonly 应该用于已显式转换为具有 get-only 属性的对象。 像冻结一样。

@olmobrutall Readonly是新的,因此没有真正定义确切的最佳实践。 我个人更喜欢所有声明它需要Readonly<>的东西来帮助表示它不会改变它。 同样,React 不希望您在state setState ,因此此更改可确保意外不会引入错误,这是使用 TypeScript 而不是 javascript 的主要好处之一。

如果Object.freeze跨浏览器的性能更加一致,我想 React 人员实际上会在setState之后开始冻结。

那么 forceUpdate 的目的是什么?

我很好奇其他人对相关工具更新以及关于 Readonly 的 React 理念(以及其他旨在不修改某些对象的库)的 DefinitiveTyped 应该如何工作的想法。

cc/ @johnnyreilly @vsaio @pspeter3关于具体反应和一般其他想法的想法
cc/@andy-ms @mhegazy了解有关 DefinedTyped 应该如何从哲学上进行工具更新和积极使用Readonly的想法

@olmobrutall我们使用forceUpdate在反应端排队渲染,由状态端的可观察事件驱动。

更新
我会稍微澄清一下我们的情况,以免被误解。 我们的状态对象是长寿的不可变对象(所以Readonly<T>实际上非常适合我们)。 这些状态对象包含多个rxjs observable 流,这些流汇集到一个名为stateChanged的通知 observable 中。 React 组件监视这个 observable 的事件并将这些事件汇集到对forceUpdate的调用中(在去抖动之后)。 实际上,我们的可变状态存在于状态中,但状态本身和状态中存在的成员都是不可变的。 这当然不是 React 的标准用例,但它在流程上非常相似。 我们只是拥有智能状态对象,它们知道如何在需要重新渲染时通知组件。

@ericanderson主要问题是这些类型定义存在 SemVer 问题。 因为类型定义版本在很大程度上与它们各自的模块版本相关,所以我们最终会遇到一个小版本凸起,这会带来破坏性的类型定义更改,这意味着我们必须将@types版本固定在我们的package.json中文件。

@olmobrutall来自 react 的文档:

通常你应该尽量避免使用 forceUpdate() 并且只从 render() 中的 this.props 和 this.state 中读取。

反应指南实际上告诉你不要直接更新状态: https: //facebook.github.io/react/docs/state-and-lifecycle.html#do -not-modify-state-directly

forceUpdate ,正如我所读到的,仅当您的组件基于您的道具或您的状态中的数据_not_时强制您的组件更新。

@patsissons我可能错了,但我相信 SemVer 旨在向后兼容 API 和语义意图。 仅仅因为您以非预期的方式使用库(根据文档)并不意味着该库必须继续支持所述非预期用途。 图书馆作者在 SemVer 中很好地更改了不正确但碰巧被某些人使用的语义。

也就是说,也许从Readonly<>state的变化太大了,但暂时假设这是正确的变化。 什么时候应该将它发布到DefiniteTyped 中? 一旦您获得最终将state标记为Readonly<>的更新,您的代码将始终需要更改。

我仍然不知道将Readonly<>应用于state的正确之处在于,这使得辩论 semver、工具或其他任何东西变得很困难。 我的直觉是它是正确的。 审查更改的人从未将其作为问题提出。 这似乎符合 React 团队的意图。

我很高兴听从任何评论者的意见,以便在 DefinitiveTyped 中做出反应(我在上面抄送了所有评论)。

所以 Observable 改变了你强制更新的状态? 因此,强制更改状态是允许的。

我知道应该使用 Readonly 的地方不是 100% 定义的。 但是,在工具准备好之前,我们是否需要从一个有争议且被大量使用的财产开始?

我完全支持强类型,我维护了 6 个项目,所有项目都使用 strictNullChecks,但这里的好处比它目前产生的问题要小得多。

@ericanderson我相信 SemVer2 旨在允许像^15.0.0这样的节点版本声明,并期望模块的任何次要或补丁升级(即15.0.115.1.0 )是透明的或至少从外部角度来看是向后兼容的。 任何重大升级 ( 16.0.0 ) 都需要调整版本声明以引入更改。 这基本上阻止了将更改带入打字系统。 但是当前类型定义版本不能偏离它们各自的模块版本主要版本(按照约定),这导致了这种不连续性。

简短的版本是,类型定义可以引入破坏性更改,而模块本身根本不改变,而破坏性更改需要主要版本升级。

但是您不会进行 PR 删除 forceUpdate,对吗?

然后,如果 forceUpdate 存在,那么强制更改状态也应该存在。

从理论上讲,您应该使用深层的不可变状态图,但通常命令式地更改状态就可以了,并不是所有的开发人员都接受持久数据结构的想法。

幸运的是,React 允许使用 scape 路线并可以直接更改状态,我们是否要禁止 TS 开发人员走这条路线? 是不是太家长式了?

例如 Vue.js 促进了命令式的变化,如果它影响 React 我不会感到惊讶。

另外,不久前(我记得)我正在阅读一些 React 作者的博客文章,鼓励在没有所有 Redux 仪式的情况下使用 React。

否则,我的立场是尽快发布类型定义,我唯一关心的是自动化。 因为给定类型定义版本的当前状态,没有源代码更改的连续构建可能会中断。 这些类型的非确定性 CI 故障令人不安。 没有人希望看到他们的构建中断,因为他们推动了变更,然后发现他们的变更手与构建中断无关。

睡了之后,个人意见如下:

  • 好的做法是使用 yarn 或 npm 锁,所以除非你先在本地升级,否则你不会感到惊讶。
  • 将状态设为只读是 React 的预期用途。 文档和示例证实了这一点。
  • 您不想使用 setState 的工作流程在您的代码库中并且在您自己的权利范围内。 React 提供 forceUpdate 是正确的,但它的用途是在您超出预期用例时导致渲染。 因此,如果您不想按预期使用状态,那很好,但此时您不需要使用实例变量状态。 实际上,您可以只使用常规私有变量。
  • 是的,这个项目受到很多人的依赖,但到目前为止,这是迄今为止仅有的两个抱怨,让我认为这不是一个广泛传播的问题。 此外,关于全局函数的问题可以重写以采用不同的泛型(请参阅链接的 TypeScript 问题)

鉴于上述想法以及非标准 React 应用程序的变通方法,我认为 Readonly 是正确的,唯一需要更改的完整性是更新生命周期方法以匹配。

我同意这些更改完全有意义,导致问题的我的用例是一个非常独特的极端案例,应该很少遇到。 通过将我的代码库调整为只读道具和状态,我发现了一些其他无法检测到的打字问题。 毫无疑问,发布更改是正确的选择。

Patsisson,使用 VS、VS Code 或任何其他编辑器?

我的观点是,虽然 React 提倡一种功能性方法,但它允许类似视频游戏的工作流程,您可以在其中强制更改世界(状态),然后渲染(强制更新)。 这种方法现在在 TS 中被禁止。 一种功能主义:)

我会想办法让我目前的生态系统可行……

问题 2 仅使用strictNullChecks引发错误。

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

+1 我正在使用 strictNullChecks

@ericanderson

如上所述,这与工具有关,这显然超出了 DT 的范围。 如果您在 VSCode 上并安装了即将推出的 TS 解析器的预览版,那么在编写上述任何内容时,我都没有看到 strictNullChecks 的这个问题。

我没有窗户,所以不能正确地与 VS 交谈。

这个Pick补丁破坏了 VSCode 下的建议。 执行this.setState({ | }) (Ctrl + Space) 时,没有显示任何内容,即使状态已明确定义并使用Partial<State>因为setState可以选择性地设置成员状态

恕我直言,正确的代码应该是setState(state: Partial<S>, callback?: () => any): void;

正如在原始拉取请求分支中所讨论的,我们从 Partial 开始。 但是,如果您的状态对象是:

接口状态{
富:字符串;
}

然后使用 partial 您可以执行以下操作:

setState({foo: undefined});

这显然是错误的。

我很抱歉 - 但又是关于 Readonly

React 不仅是 Redux 和 setState。 React 也是 mobx 和其他可观察模式,其中状态属性的分配是MAIN FEATURE 。 所讨论的更改完全消除了使用 typescript 的 mobx 使用。

那么为什么要将原始反应代码中不存在的行为添加到 .d.ts 文件中呢? 根据作者的观点,.d.ts 是原始库的反映,而不是教人们正确的编码风格!

@lezious ,恐怕我不明白你的立场。 您能否提供代码示例以及与示例相关的类型有什么问题? 谢谢!

没问题:

这是我的州课

class UserInfoBlockState  
{
    <strong i="7">@observable</strong>                  <- this is mobx way to declare state
    public updating: boolean;
    <strong i="8">@observable</strong> 
    public deleted: boolean;
}

这是我的组件

<strong i="12">@observer</strong>       <-- 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;
            });
    }
   ....

}

现在我们(我们的公司在 react+mobx 上编写了巨大的项目)在新的发布周期开始时更新了 DT 和 React 并且...... 3000+ 编译错误“属性是只读的”。 哇。 你建议我做什么 - 将整个项目重写为 redux,从不更新 react.d.ts 或始终保持并支持分叉版本?

@mweststrate ,请检查一下。

@Iezious我很欣赏你的立场,我请你冷静下来。 我正在努力与你合作。 这与 Redux 无关,而是纯粹的 React。

我不希望 DT 阻止以前工作的用例,但是我不认为你如何描述 mobx 与 react 的使用与 react 文档(甚至现在我已经阅读过的 mobx 文档也不一致)它)。

React 在文档中明确指出,正确使用 state 有 3 种方法,第一种是“不要直接修改 state”。

这让我相信你的代码库当前的工作方式很可能会在未来的 react 版本中中断。 在浏览https://github.com/mobxjs/mobx-react时,我没有看到任何建议您以这种方式使用状态。 事实上,他们似乎希望你使用属性。

查看https://mobx.js.org/getting-started.html并在谷歌上搜索“mobx 反应状态”,我找不到任何建议您按照自己的方式使用 mobx 的文档。

DT 最多应该传达底层库的精神,最糟糕的是实际实现,很明显,购买 react 和扩展组件意味着尊重隐含的合同。

我不确定我建议你做什么。 我能想到的几个选项:

  1. 如果您坚持接管state变量,一个“便宜”的选择是搜索React.Component并将其替换为MyComponent并将MyComponent定义为子类React.Component没有只读约束。
  2. 另一个基于 mobx 文档中发布的惯用示例是花时间停止使用this.state并仅在实际React.Component上使用变量。 这可能有点痛苦,但至少您项目的新人将能够看到您的代码库中的模式,因为它们是在线描述的。
  3. 如果你想继续做类似于你正在做的事情,你可以在每个组件中重新声明state
  4. 您可以搜索并用 $#$9$ this.state this.somethingElse并手动声明。
  5. 您可以停止更新以从 DT 做出反应(并且可能在未来做出一般反应,具体取决于未来的更改可能如何影响您的用例)。

如果这是我的项目,我可能会做第二个,尽管我对 mobx 的了解还不够,无法确定。

抱歉,我无法同意你的观点(并不意味着其他人不会同意你的观点)。 我试图找到一个理由来恢复那部分,但我现在似乎无法加入。

我将再次提到我们的策略,它是 RxJs observables 的自定义应用程序,用于驱动 React 状态更改和渲染,非常类似于 mobx 模式。 我们使用动作来接收来自视图(React)层的输入。 动作与消耗输入并产生可观察对象的函数同义,该函数将进一步驱动其他状态可观察对象。 从 React 层的角度来看,这种模式允许状态保持不变,因为您只需要查询可观察值并执行状态操作。 在内部,您的状态可能会因操作而“改变”自身,因为内部状态没有只读约束。

我无法冷静下来。 我有这个项目在生产中,我需要花费大量的团队时间,这意味着自从这次改变以来的钱。

原因是什么? 这种变化是否反映了react中的现实? 不,它添加了react中不存在的限制和行为,只是因为他认为这是正确的,所以以某种方式添加了限制。

DT项目的目的是什么? 尽可能精确地描述 JS 库,或者描述我们对正确使用这些库的方式的看法? 根据名称“Definitely Typed”,它是第一个。 所以,如果这个限制在原始 JS 库中不存在,它也不能在 DT 中存在。 这是我的观点。 在这一点上我哪里错了?

我知道您很沮丧,但是,这是通过阅读文档来使用 react 的方式。 我几乎看不出我们应该如何降低 DT 的具体性,因为您的团队滥用了库并违反了隐含的合同。

给我看一盎司由 react 团队发布的文档,这些文档表明状态应该是直接可变的,我会立即将代码改回来。

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

永远不要直接改变 this.state,因为之后调用 setState() 可能会替换你所做的改变。 将 this.state 视为不可变的。

很明显,react 认为this.state是不可变的。 React 不认为this.state的 _properties_ 是不可变的(这是 redux 的假设)。 你可以自由地做:

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

在惯用的反应。

我的偏好是准确地键入 API(在这种情况下意味着Readonly )并表达反应团队声明的所有不变量。

@ericanderson抱歉,我才注意到这一点。 FWIW 我认为这种变化是合理的,并且工具会迎头赶上。 顺便说一句,您听说他们正在考虑弃用接受对象的setState重载吗? 未来是所有帐户的减少样式setState

@amoreland不同意。 每: https: //facebook.github.io/react/docs/state-and-lifecycle.html#do -not-modify-state-directly

不要直接修改状态

例如,这不会重新渲染组件:

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

相反,使用 setState():

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

唯一可以分配 this.state 的地方是构造函数。

@johnnyreilly我没有。 那很有意思。 资源?

在最近的反应会议上的一次会谈中对此进行了讨论。 它可以在 YouTube 上找到。 这可能是林克拉克的谈话。 第 1 天的第一次会谈之一 - 涵盖即将对 v16 做出反应的变化。 纤维等

抱歉@gaearon我的意思是

mobx 执行状态更改的原因,是因为 observables 正在驱动 React 更新,而不是完全_替换_状态,状态成为驱动渲染的引擎。 因此,通过执行this.state.updating = true;之类的操作,您实际上是在进行状态替换,但是状态足够智能,可以通过通知组件状态已从其先前的内容更改来触发新的渲染。 我同意这不是 _conventional_ React 的用法,而是更全面和更高级别的 React 用法。 我会争辩说,传统的 React 用法仅对具有少量组件的小型项目有用。 当你最终拥有 100 多个组件,每个组件都有多个响应式突变驱动程序时,你不能再可靠地使用传统的 React 状态更改(即 setState),并且必须接受涉及 _smart_ 状态的架构更改(这就是 mobx 本质上所做的) )。

话虽如此,我理解他为什么不高兴,因为打字的变化正在影响 React 系统的更高级的使用。 可观察状态分配在技术上不是 _mutating_ 状态,而是为 RHS 值调用可观察事件。 碰巧 mobx 选择发出这些可观察事件的语法与 React 不可变状态的明确意图相冲突。

@Iezious如果您需要快速解决此类问题,您可以通过增加 React 类型并重构您的组件以使用React.Component类型定义的扩展来摆脱它。

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;

现在您可以像常规组件一样创建可变状态组件,只是它们的state成员不再标记为readonly

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

@patsissons是的,这正是原因。

我没有改变状态,我使用的是 mobx observables,它为我调用 setState,我这样做是因为我的 HUGE 项目代码看起来更加清晰易懂。

我知道我可以制作我的组件,我也可以在我的 npm 服务器中制作脚本,它总是会为我们公司恢复这个更改。 我可以使用像“this.state.state.updated”这样的黑客和许多其他黑客。

我只想说,这种变化会影响使用像 mobx 这样的可观察模式,它实际上遵循反应方式,但是现在,因为这种变化无法编译并且需要一些技巧和变通方法才能工作。 这就是为什么我认为这种改变是不对的。

也许不是我建议的MutableStateComponent ,而是更符合 Observable React 模式的ObservableComponent

如果你放弃Readonly ,那么那些使用常规 react 的 react 类型(和/或除 mobx 之外的任何其他状态管理系统)的人将面临错误。 我不在我的大型项目中使用 mobx,当有人打错并意外使用this.state.foo = bar时,我很感激编译器错误。

如果在标准反应和非标准反应使用之间存在不可避免的权衡,标准反应类型应该倾向于前者。

此外,如果您以惯用的方式使用 mobx,这不是问题。

如果您放弃 Readonly,那么使用带有常规反应的反应类型(和/或除 mobx 之外的任何其他状态管理系统)的人将面临错误。 我没有在我的大型项目中使用 mobx,当有人打错字并意外使用 this.state.foo = bar 时,我很感激编译器错误。

所以再一次 - 你正在教写代码

这个项目不是教写代码,这个项目是描述 JS 库的现有功能。 所讨论的限制在原始库中不存在,必须删除。

就这样。

@patsissons

如果您需要快速解决此类问题,您可以通过增加 React 类型并重构您的组件以使用 React.Component 类型定义的扩展来摆脱它。

是不正确的。 在我所在的企业世界中,没有“快速修复”。 当 SDK 发生变化时,它或必须向后兼容,否则需要数年时间。 2M+ 行代码项目中没有快速修复。 这是数周的变化、测试、A/B 产品测试、面向大量人员的推出。 它需要真钱。 而所有这些巨大的努力仅仅是因为有人添加了不存在于真实库中的不向后兼容的更改?

为什么你认为 forceUpdate 仍然存在于 react 中? 他们在 confs 上谈论正确的风格,但进行更改以防止其他使用方式。 为什么? 因为它是大公司,他们知道库必须向后兼容。

@ericanderson写道

this.state.state.value = 1 

从他的角度来看也不合法。 下次他将从 TS 获取工具并添加阻止此代码的限制。 或者让组件最终类,或者其他只是因为“它是正确的样式并防止人们犯错误”。

防止人们犯错 - 这是 FB 的任务,如果他们想这样做,他们可以轻松添加代理并禁止状态突变。 DT 的目的是添加描述,仅此而已。

@Iezious

我认为关键是 React 团队无法使用 JavaScript 使状态不可变,但如果可以的话,他们会做到的。 另一方面,TypeScript可以使状态不可变,这就是对类型定义进行此更改的原因。 React 团队的意图绝对是禁止直接对状态成员进行修改(正如他们关于正确使用状态的官方文档所证明的那样),但他们没有施加这种约束的语言结构。 这种约束从来都不是未知数,它从一开始就被很好地记录在案。 对于我们这些在 React 的_传统_用法之外操作的人,我们至少必须遵守 React 官方的使用建议。 对于我的团队来说,这意味着构建一个解决方案,使我们能够在不直接改变状态的情况下驱动状态更改。 这样做是为了确保我们不会遇到诸如此类的任何问题(尽管此更改确实对我们产生了轻微影响,但仅通过函数签名而不是基本设计)。

如果在您的情况下无法进行重构,则在进行更改之前将@types/react固定在15.0.1 ,或者只是不使用@types/react而是维护您自己的变体输入 defs 并简单地改变state输入Component 。 我真的不认为你会成功说服任何人恢复更改。

forceUpdate存在是因为它被记录为在修改内部结构状态时(或当render()使用可变状态之外的数据时)驱动渲染的推荐代码路径。 forceUpdate的使用正是为我的团队在 React 中使用的使用模式而设计的。

React 团队可以使用 JS 使状态不可变,这很容易。 但不向后兼容。

再次,是:

this.state.state.value = 1 

合法与否?

我认为是,但我的观点已经很清楚了……

我不认为关于可变性/不变性的对话是相关的。 显然,TS 编译器中的错误(关于Readonly )与此更改相结合使通用组件完全无法使用,从而破坏了许多现有代码。 当然,这是一个普遍接受的有效用例,并且有足够的理由暂时回滚???

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;
}

有谁知道 Typescript 团队对此是否存在未解决的问题? 我似乎无法在他们的追踪器上找到相关问题。

@caesay见微软/TypeScript#15501

我必须在构造函数中初始化状态,但 tslint 显示“状态是只读的”....

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

我该如何解决

使用 setState

setState 在构造函数中不起作用

或者考虑使用 setState 的 componentWillMount

谢谢

你好,

我阅读了整个线程,但我不清楚是否有计划处理@alanwei0场景?

我完全同意将state作为ReadOnly是有意义的。 据说无法在构造函数中设置初始状态会使事情变得相当复杂,因为在componentDidMount完成之前调用了 $#$ render $#$。

@pawelpabich在构造函数中使用this.state = {不是问题。 您可以在构造函数中分配给readonly变量,并且Readonly<T>不会阻止分配(永远!)。

interface TInterface {
    test: string;
}

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

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

这里唯一的错误将在fn内部 - 不是因为Readonly<T> ,而是因为readonly关键字。 事实上,最新版本的类型甚至没有使用readonly关键字,因此您实际上可以在任何地方分配状态,只是不要更改状态内部的属性。

我们在这里讨论的问题是 typescript 编译器的一个 bug,它导致 state 属性在继承的组件中丢失它们的类型。 我相信这已经得到修复,如果是这样,这个问题可以关闭。

@caesay抱歉,我以为我们在这里讨论的就是这种情况。 我的问题是我无法在基类中设置状态。 我在 TS 2.4.1 上。 你有没有机会知道问题的ID,所以我可以检查一下?

您始终可以调用 setState(在 componentWillMount 中)。

@pawelpabich同样,这不是问题:) 您不能故意从基类分配状态。 你怎么能? 您不知道派生组件使用的道具合约。

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",
        };
    }
}

这是派生组件的一个简单示例(省略了道具,但想法相同)。 基类中的this.state =显然不能工作,因为它不知道TState是什么。 此外,如果它_did_ 工作,那么它只会覆盖父级设置的状态。 最终状态将是{ baseState1: "fronBase" } 。 topState 属性发生了什么变化?

如果您绝对确信需要处理基础组件中的状态,则可以将派生组件中的状态传递给基础组件构造函数(作为TState以便您可以分配它) - 这可能看起来像这:

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",
        });
    }
}

或者更简单的是,您可以在基础组件中调用this.setState( (是的,您可以在构造函数中执行此操作!)

这里没有问题。

@caesay我完全同意,如果没有限制,那么分配没有意义。 但是下面的代码仍然会产生编译错误,尽管编译器拥有所有必需的信息来验证代码是否正确。

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>;
    }
}

错误

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

第一的。 您在构造函数中的 base 道具没有输入。 因此props.basePropany ,这是不可分配的!

其次,您在Derived中的道具有同样的问题,并且您缺少baseState 。 无论 Readonly 如何,哪个当然行不通

TProps extends BaseProps这意味着TProps至少具有与BaseProps相同的成员。 那怎么不定义呢? 我知道编译器可能无法推断它,但说未定义似乎并不正确。 同样的想法也适用于Derived

@caesay setState in constructor 不是一个可靠的解决方案,因为此方法是异步的,您仍然可以在没有设置初始状态的情况下访问render

我能看到的唯一可靠的解决方案是在派生类中设置整个状态。 这有一个明显的缺点:

  1. 这需要在每个派生类中重复
  2. 派生类需要知道他们不关心的状态。

我上面展示的示例可以在 C# 中运行,所以如果它可以在 TypeScript 中运行就好了。

  1. ~setState 在构造函数中是安全的~
  2. 对于派生类,我能看到的最好的情况是在componentWillMount中调用setState #$ 。 你的基地不知道它的孩子的状态应该是什么,所以它不可能让this.state进入安全配置。 但是,您的子类可以调用父类的componentWillMount ,然后设置它认为它还需要的任何状态。
  3. 许多语言的语言特性在打字稿中会很不错。 有些是可行的。 其他人不是。 无论哪种方式,这都不是他们包含的论据。
  4. 您看到的错误是有道理的。 他们不建议打字稿或打字中的错误。 在每种情况下,您实际上都在尝试将this.state分配给与预期类型不匹配的对象。

已编辑,以反映我对构造函数中的 setState 不正确并添加反引号以提高可读性。

@ericanderson我认为我们在这里没有取得任何进展。 我向您展示了一个示例,如果您能专注于它,我将不胜感激。 否则很难进行讨论。

Re setState在构造函数中使用是不安全的。 它不会抛出错误,但不会在渲染发生之前设置状态。 我的代码因此而中断,React 文档非常清楚不应该在那里使用。

@pawelpabich不,这不是有这个论点的地方。 你对语言的理解从根本上是错误的。 在您提供的示例中,您对国家的任一项分配都不满足“国家”合同。

例如,当你这样做

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

状态正好是{ baseState: props.baseProp } ,这不满足TProps extends BaseProps的要求(因为我们不知道 TProps 是什么!它可以有任何属性)。

之后,您正在执行 _ SEPARATE _ 任务。

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

现在状态正好是{ derivedState: props.derivedProp } (你已经覆盖了你的基本状态分配!!),这不满足 DerivedState OR BaseProps。

您认为这应该起作用是完全错误的。 如果您对基本变量分配的工作方式有疑问,请在新问题中与语言设计人员讨论并在那里被击落,因此我们请停止在此处收到通知。

作为旁注 - 您还覆盖了您的基础render()方法,这意味着您的基础组件将无法呈现任何内容。 如果您确信这是您想要的,您可以添加一个受保护的成员getBaseState()并在设置状态时从派生构造函数中调用它(因此您不必复制基本状态逻辑)。 但我认为你真正想要的是根本不使用派生组件。 尝试重组以使用组合(其中一个对象包含多个子对象)。 我想你会发现它会变得更干净。

我几乎不想离开阅读来创建一个新项目只是为了和你讨论这个问题,但是唉......

我将在构造函数中纠正setState () ,但这不会改变我在componentWillMount中使用它的感觉。

如何做到这一点的工作示例:
https://github.com/ericanderson/set-state-example

具体来说,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如果要实现具有初始状态的多态组件,则需要使基本组件抽象并创建抽象getInitialState() (或类似主题)函数以在派生类中实现。 您只想在构造函数中或setState分配状态一次,如@ericanderson所示。

以下是将您的示例转换为完全多态的解决方案,完全分离了与状态构造相关的关注点:

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谢谢!

@caesay我承认我错了,由于某种原因没有看到分配会相互覆盖。 话虽如此,使用 CAPS 和!并没有帮助我摆脱困境。

@patsissons@ericanderson专注于这个问题,现在我们有了一个其他人可以使用的解决方案。

@pawelpabich我同意我的举止不够专业——但可以理解的是,考虑到我给了你几个解释、示例等,你选择不听我的。

那么它只会覆盖父级设置的状态。

[_if you want to_] 处理基础组件中的状态,您可以将派生组件中的状态传递给基础组件构造函数

[_如果你想在你的派生组件中处理状态_]你可以添加一个受保护的成员 getBaseState() 并在你设置状态时从派生的构造函数中调用它。

@patsissons所做的是获取这里已经提到的评论并提供代码示例——这不应该是必要的。 这不是 stackoverflow,我们也不经常在那里提供现成的代码示例。

我是 react 和 typescript 的新手,也许我不知道某事,但即使应用程序编译时没有错误、警告和提示,我也会收到运行时错误。 下面是一个示例组件。 我将错误归因于Readonly状态。 如果应用程序在Readonly更改之前工作,那么在此更改之后它停止工作并且它不会给出编译时错误。

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>
      );
  }
}

错误:

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 (

状态应该始终是一个普通的键控对象 afaik,所以改为定义状态
就像: { values: Map <string, string> }并阅读
this.state.values.get('aKey')

操作 vr 9 月 29 日。 2017 年 09:01 schreef Janusz Białobrzewski <
通知@github.com>:

我是反应和打字的新手,也许我不知道,但即使
尽管应用程序编译时没有错误、警告和提示,但我得到了一个运行时
错误。 下面是一个示例组件。 我将错误归因于 Readonly
状态。 如果应用程序在只读更改之前工作,那么在此之后
更改它停止工作并且它没有给出编译时错误。

从'react'导入*作为React;
导出默认类 HomePage 扩展 React.Component> {

公共组件WillMount() {
常量地图 = 新地图();
map.set('aKey', 'aValue');
this.setState(map);
}

公共渲染(){

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

}
}

错误:

主页。 tsx:12未捕获的类型错误:this.state.get 不是函数
在 HomePage.render (homePage.tsx:12)
在 eval (ReactCompositeComponent.js:793)
在 measureLifeCyclePerf (ReactCompositeComponent.js:73)
在 ReactCompositeComponentWrapper._renderValidatedComponentWithoutOwnerOrContext (


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/14250#issuecomment-333047367
或使线程静音
https://github.com/notifications/unsubscribe-auth/ABvGhM5hDyRNyUeZuIiGeTZk1N-rfuA4ks5snJW5gaJpZM4LuDWV
.

谢谢,但是将 state 声明为Readonly<S>似乎是毫无意义的,因为它的嵌套成员不受 Readonly 的影响。

Readonly可能有一天会递归应用,但现在,你要确保你已经正确处理了它。 在您的情况下,您应该真正声明ReadonlyMap

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

或嵌套:

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

我们可以将它用于深度读取:

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;
此页面是否有帮助?
0 / 5 - 0 等级