Definitelytyped: 关于@types/react 的 setState 使用 Pick

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

我知道Pick用于setState的类型,因为为不应未定义的键返回undefined会导致键被 React 设置为未定义。

但是,使用Pick会导致其他问题。 一方面,编译器服务的自动完成变得无用,因为它使用Pick进行自动完成,并且当您请求完成时, Pick的结果尚未包含您可能需要的密钥想要自动完成。 但是在使用回调参数编写setState时问题尤其严重:

  1. 键列表源自您的第一个return语句; 如果您不在 return 语句中返回特定键,则也无法在不强制键列表重置为never情况下在参数中读取它。 如果返回不同的键,则多个 return 语句可能很难编写,尤其是当您在某处有未定义的 return 时(例如if (state.busy) { return } )。

    • 这可以通过始终使用价差中的输入(例如this.setState(input => ({ ...input, count: +input.count + 1 })) )来解决,但这是多余的和去优化的,特别是对于较大的状态,因为setState将传递回调的返回值到Object.assign

  2. 如果由于某种原因,您返回的类型与输入类型不兼容, Pick将选择never作为其键,并且该函数将被允许返回 _anything_。 即使与现有键重合的键有效地允许any作为值——如果它不适合,它只是不是Pick ed,并且被视为{}的多余属性
  3. 如果选择never作为泛型参数,出于上面列出的任何原因,回调参数实际上可能被视为setState的对象形式的参数; 这会导致回调的参数被键入any而不是{} 。 我不确定为什么这不是隐含的任何错误。
interface State {
  count: string // (for demonstration purposes)
}

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

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

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

综上所述,虽然Pick虽然有一些不便,但有助于在setState的非回调形式中捕获类型错误,但在回调形式中完全适得其反; 它不仅没有执行禁止undefined的预期任务,而且还完全禁用对回调输入或输出的任何类型检查。

也许应该将其更改为,至少对于回调表单,更改Partial并希望用户知道不要返回undefined值,就像在旧定义中所做的那样。

最有用的评论

最近的“修复”现在给我带来了问题,在setState()回调中使用多个返回语句。

打字稿:2.6.2 启用所有“严格”选项,“strictFunctionTypes”除外
类型/反应:16.0.30

代码示例:

interface TestState {
    a: boolean,
    b: boolean
}

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

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

编译器错误:

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

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

所有53条评论

感谢您的反馈意见。 这是你提出的一个非常有趣的案例。

在我发表意见之前,我需要稍微考虑一下其中的含义。

曾几何时,@ahejlsberg希望以不同于| undefined方式对待可选性。 因此foo?: string意味着 foo 要么未设置,要么是一个字符串。

在读取属性值时,差异在 99.9% 的情况下无关紧要,但对于写入,尤其是在Partial<>的情况下,差异非常重要。

不幸的是,这是一个重大的语言更改,因此我们必须等待 3.0 或将其置于标志之后。

如果我们说改变, Partial<>对许多人来说变得非常有用,而不是我目前的教条是拒绝使用它。

@ahejlsberg我知道你很忙,实施起来有多难? 这样 undefined 不是一个隐式的可分配值?

好的,今天早上花了一些时间思考这个问题之后,我看到了你提出的问题的一些“解决方案”,每个都有一些非常严重的副作用。

1. 将可选性 ( interface State { foo?: string } ) 更改为字符串或未设置。

例子:

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

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

这将在技术上解决问题并允许我们使用Partial<> ,代价是 98% 的情况更加困难。 这打破了世界,所以在 3.x 之前真的不可能做到,而且一般来说,很少有库/用例真正关心未设置与未定义之间的区别。

2.只需切换到部分

例子:

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

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

3. 什么都不做

例子:

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

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

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

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

4. 为连接的字面量类型添加嵌套逻辑

现在,当我们有多个返回路径时,编译器只是将所有潜在的键放入一个巨大的Pick<State, "foo" | "bar">

但是,向后兼容的更改是允许对文字进行分组,例如Pick<State, ("foo") | ("bar")>或更复杂的情况: Pick<State, ("foo" | "bar") | ("baz")>

缺少括号表明它只是一组值,即现有功能。

现在,当我们尝试将{foo: number}Pick<State, ("foo") | ("bar")> ,我们可以成功。 与{bar: number}

Pick<State, ("foo") | ("bar")>也可以转换为Partial<State>{foo: number} | {bar: number}

我个人的结论

(1)和(2)只是不可行。 一个为几乎每个人都引入了复杂性,无论如何,另一个帮助人们生成可编译但显然不正确的代码。

(3) 今天完全可用,虽然我不记得为什么我添加| S作为setState函数的潜在返回值,但我想不出任何其他原因这样做。

(4) 可以避免 (3) 中的解决方法,但这取决于 TypeScript 开发人员,也许如果我们让@ahejlsberg足够感兴趣,我们迟早会看到它。

这让我觉得今天的类型比我们改变它们更正确。

“什么都不做”方法的问题在于,在您实际犯了真正的类型错误的情况下,编译器的行为与您描述的方式不同。 如果我将您的示例修改为将值设置为0而不是空字符串:

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

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

我现在明白了。 回到绘图板。 我将看看我们是否可以为此创建一个聪明的解决方案。 如果我们无法想出一个,那么我们需要评估您提出的Partial<>解决方案。 需要考虑的重要问题是值上意外的undefined是否比意外的错误类型更常见/更烦人。

为了澄清,我们有两个重载...

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

将其中一个或两个设置为Partial<S>而不是Pick<S, K>不能解决您的问题。 它仍然下降到对象版本,由于某种原因它接受错误的类型:

```ts
接口状态{
酒吧:字符串;
富:数字;
}

class Foo 扩展 React.Component<{}, State> {
公共废话(){
this.setState((prevState) => ({bar: 1})); // 没有错误:/
}
}```

我们能否以某种方式强制它通过使用extends object拒绝一个函数
约束?
2017 年 7 月 28 日星期五 0:00 Eric Anderson [email protected]写道:

为了澄清,我们有两个重载...

设置状态(f: (prevState: Readonly , props: P) => Pick , callback?: () => any): void;setState(state: Pick , callback?: () => any): void;

将其中一个或两个设置为 Partial而不是 Pick不能解决你的问题。

接口状态{
酒吧:字符串;
富:数字;
}
class Foo 扩展 React.Component<{}, State> {
公共废话(){
this.setState((prevState) => ({bar: 1})); // 没有错误:/
}
}```


您收到此消息是因为您创作了该线程。
直接回复本邮件,在GitHub上查看
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-318388471
或静音线程
https://github.com/notifications/unsubscribe-auth/AAEdfeEJ_Ejfana14fRII1OZuS7qTuyuks5sSKX3gaJpZM4OiDrc
.

我们试过了。 函数是对象。

埃里克·L·安德森
从我的iPhone发送

我来这里报告一个相关问题,即状态类型的重构被此更改破坏了。

例如,请参阅https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx - 如果您在 VS Code 和 F2 中打开它以重构/重命名something 6 行的https://github.com/tomduncalf/react-types-issue/ blob/master/Test.tsx#L14 ,但会错过在https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsxsetState调用中使用它#L20

我发现让它正常工作的唯一方法是在调用setState时显式键入我的对象as IState setState ,这有点不方便并且很容易忘记。

我不完全熟悉更改的基本原理,但是在处理状态时它似乎确实以相当重要的方式破坏了类型安全,因此如果有办法解决它会很棒!

谢谢,
汤姆

是的,我再次相信使用Partial会更好,因为它可以更好地保持关系信息。

可以说,没有正确重构Pick是重构代码的一个限制/错误,可以改进,但我仍然不相信Pick提供了https://的类型安全Partial将允许undefinedundefined不应该是,然后用户需要小心不要这样做, Pick允许任何事情,因为任何不符合接口的类型都会导致Pick而不是类型错误,而是生成一个空接口,它接受任何东西。

至于https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318388471,这可能是多余属性检查器中的错误吗? 或者这是在 2.3 或 2.4 中对多余属性检查器变得更严格之前进行了测试(我忘记了)?

我刚刚注意到的另一个问题是,如果一个组件没有状态类型(即React.Component只有一个类型参数),或者状态类型为空( {} ),Typescript 仍然允许调用setState没有抛出错误,这对我来说似乎不正确 – 请参阅https://github.com/tomduncalf/react-types-issue/blob/master/Test2.tsx

使用 Partial 的解决方案听起来更适合我——我可能会尝试自己修补它并看看它是如何工作的。

谢谢,
汤姆

大家好!
我不知道为什么,但是如果我将setState声明加入单个声明:

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

它按我的预期工作:

import * as React from 'react';

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

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

这是否足以解决原始问题?

@mctep不错的发现。

如果我采用您所做的并将其扩展一点,在某些地方使用Partial<S> & Pick<S, K>而不是Pick<S, K> ,intellisense 会为您建议关键名称。 不幸的是,键名说属性值是“something | undefined”,但是当你编译它时,它知道得更好:

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

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

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

我将在今天晚些时候进行此更改

最近的“修复”现在给我带来了问题,在setState()回调中使用多个返回语句。

打字稿:2.6.2 启用所有“严格”选项,“strictFunctionTypes”除外
类型/反应:16.0.30

代码示例:

interface TestState {
    a: boolean,
    b: boolean
}

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

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

编译器错误:

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

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

由于此更改,还看到了@UselessPickles描述的问题。

我很确定这一直是一个问题。

我非常肯定它并不总是一个问题。 我有一个在setState()回调中包含多个 return 语句的项目,该项目已经编译了 2 个多月没有问题。 我一直在每 2 周左右更新一次 NPM 依赖项升级,在升级到最新版本的类型/反应后,这个编译器错误今天才开始发生在我身上。 以前的版本没有产生这个编译器错误。

自从从 Partial 更改为 Pick 以来,这一直是一个问题。 解决方法是将您打算返回的键列表提供给setState的通用参数,但是您必须始终返回所有键...

在这些情况下,我所做的是要么始终返回所有键,将未修改的键设置为key: prevState.key ,要么返回带有 prevState ( { ...prevState, newKey: newValue } ) 的价差。

也许我的代码中实际上有一个更具体的边缘情况,它以前是巧合的? 我的项目中的一个实际示例更像是这样,它要么返回一个空对象(不改变任何状态),要么返回一个非空对象:

interface TestState {
    a: boolean,
    b: boolean
}

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

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

return nullreturn undefined也是完全可以接受的不改变状态的返回值(它们将经历Object.assign并且因此不要改变this.state )。

提醒我,当前的签名不允许它们中的任何一个。 也许null应该被允许作为返回值,至少,因为它根本不会因为意外忘记return而产生。


无论如何,在签名不明确的情况下,TypeScript 似乎只选择源代码顺序中的第一个return语句并使用它来派生通用参数。 它似乎能够合并简单泛型的类型(例如ArrayPromise ),但如果上下文类型是像Pick这样的映射类型,它永远不会合并它们。

我看到最新类型的非函数版本有一些回归。 特别是,传递参数时的“状态”参数类型已从:

设置状态( state: Pick , callback?: () => any): void;

到:

状态: ((prevState: Readonly\ , props: P) => (Pick & Partial\ )) |

https://github.com/DefinitelyTyped/DefinitelyTyped/commit/62c2219a6ed6dc34ea969b8c2c87a41d31002660#diff -96b72df8b13a8a590e4f160cbc51f40c

& Partial<S>似乎打破了以前有效的东西。

这是一个最小的重现:

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

这失败并出现错误:

(429,18): '{ baseProp: "foobar"; }' 不可分配给类型 '((prevState: Readonly Type '{ baseProp: "foobar"; }' 不可分配给类型 'Pick & Partial Type '{ baseProp: "foobar"; }' 不可分配给输入“部分

将状态类型从S &{ baseProp: string }更改为仅{ baseProp: string }也会使错误消失(尽管这会破坏指定 S 类型的实际类)。

那很有意思。 我很高兴回滚更改的部分和选择部分

我会说您报告的内容听起来像是 TS 中的错误,具体来说:

输入 '{ baseProp: "foobar"; }' 不可分配到类型 'Partial

好吧。 也许 TS 不知道发生了什么,因为 S 和 baseProp 之间没有关系。

可以传递类型为 { baseProp:number } 的 S,在这种情况下,您不能为 baseProp 分配字符串是正确的。

也许如果 S 扩展了 baseProp 版本?

我以前尝试过这样的事情:

interface BaseState_t
{
    baseProp: string
}

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

虽然访问正常,但 setState 调用有相同的错误:

'{ baseProp: "foobar"; 类型的参数 }' 不可分配给类型为 '((prevState: Readonly\ , props: P) => Pick & Partial\ ) | 的参数
}' 不可分配给类型 'Pick & Partial\ '。输入 '{ baseProp: "foobar";

这可能不是构造它的好方法 - 毕竟,某些子类可以声明与基类上的变量发生冲突的状态变量。 所以打字稿抱怨它不能确定那里会发生什么可能是正确的。 奇怪的是,访问这些道具没有问题。

唔。 这现在绝对感觉像是 TS 中的一个错误。

我今天会玩这个,可能会去掉 & Partial。

我也会将此添加为测试用例,并尝试另一个想法让智能感知快乐

你好,
同样的问题在这里...

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

interface SomeComponentState {
    field: string;
}

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

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

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

设置状态错误:
TS2345 (TS) Argument of type '{ field: "test"; }' is not assignable to parameter of type '((prevState: Readonly<TState>, props: TProps) => Pick<TState, "field"> & Partial<TState>) | (Pick...'. Type '{ field: "test"; }' is not assignable to type 'Pick<TState, "field"> & Partial<TState>'. Type '{ field: "test"; }' is not assignable to type 'Partial<TState>'.

此更改后才开始发生错误。 在 16.0.10 版本中它运行良好。

Typescript 有几个相关的问题,他们已经按照设计的方式关闭了它们:

https://github.com/Microsoft/TypeScript/issues/19388

我在这里做了一些例子的要点: https :

虽然这种行为看起来仍然很奇怪。 特别是,您可以在没有强制转换的情况下分配给类型为基类的变量,然后毫无问题地进行相同的调用。 这个问题似乎表明分配和比较是两件不同的事情。

没有忘记你们。 很快就会修复

引用的 PR 应该可以解决这个问题。 我还添加了应该防止再次打破这种边缘情况的测试。

@ericanderson感谢您的快速回复:)

新修复是否有任何限制/缺点?

没有我目前知道的。 我能够保持智能感知(实际上比以前更好,因为它解决了一些边缘情况)。

详细地说, & Partial<S>解决方案是欺骗智能感知来揭示可能的参数,但它通过声明它们是X | undefined 。 这当然会无法编译,但有点令人困惑。 获取| S修复了智能感知,以便它建议所有正确的参数,现在它不会显示错误类型。

我尝试研究从setState回调中添加null作为可能的返回值(您可以返回到不想更改任何状态的状态,而无需使return;也有效),但这反而使类型推断完全放弃并选择never作为键 😢

现在,在我确实想跳过更新状态的回调中,我一直在使用return null! 。 此返回的never类型使 TypeScript 忽略泛型类型推断的返回。

嗨,大家好...

最后一次提交解决了我的问题。
感谢您及时的回复 :)

哪个版本有修复? 发布到 npm 了吗? 我遇到了 setState 的回调版本告诉我返回值中存在属性不匹配的情况,就像发现的@UselessPickles一样。

应该适用于最新的@types/react

我为 15 和 16 系列修复了它

import produce from 'immer';

interface IComponentState
{
    numberList: number[];
}

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

React 的旧类型定义..

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

..产生这个错误:

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

我试过这个建议:

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

Intellisense 现在可以从 React 的状态感知属性。 然而,感知的属性现在被视为possibly undefined

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

即使 numberList 不是未定义也不是可空的,也必须使用空断言运算符:

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

我将继续使用旧的类型定义,直到类型检测得到改进。 同时,我将在 immerproduce 的泛型参数上明确说明类型。 produce<IComponentState>list!更容易推理。

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

你的第一个错误是因为你没有返回任何东西。 这不是 setState 的工作方式。

即使在生产中没有返回变量,它也能工作。 我只是按照这里的例子(非 TypeScript):

https://github.com/mweststrate/immer

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

阻止 TypeScript 运行该代码的唯一原因是它从 React 类型定义中错误地推断出类型。 类型定义报告numberList does not exist on type Pick<IComponentState, never> 。 能够通过在生产的泛型参数上显式传递类型来消除编译错误,即produce<IComponentState>

我什至尝试在 generate 中返回变量,看看它是否有助于 React 类型定义推断类型(尽管这是一个鸡和蛋的问题),但是 React 类型定义仍然无法检测状态的正确类型。 因此草稿的智能感知没有出现:

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

或者也许我对编译器有错误的期望:) 编译器无法根据 setState 的类型为草稿变量创建类型,因为编译器从内到外处理代码。 然而,建议的类型定义不知何故让我认为编译器可以从外到内处理代码,它可以选择它可以从外部代码( setState )传递到内部代码( produce )。

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

使用上述类型定义,编译器可以检测到该草稿具有numberList属性。 虽然它检测为possibly undefined

image

经过进一步的修改,我通过将S到 setState 的类型定义,使编译器能够将状态的类型传递给生成的草稿:

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

代码现在正在编译:)

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

您的错误与 setState 无关,您的错误在产品内部。 您对产品的类型定义是什么?

我上面的 PR 在绝对类型的测试脚本上有错误,我没有在本地测试。 所以我现在在本地测试它。

这是 immer/produce 类型定义。

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

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

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

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

@ericanderson能否请您指出有关为什么使用Pick而不是Partial ? 这给我带来了数小时的悲伤(使用普通的setState(obj) ,而不是回调版本),现在我将使用this.setState(newState as State)作为解决方法。 我只是想了解为什么要更改它,因为我一定遗漏了一些东西。

你好@ericanderson

我对最新的定义有一些问题。

我的用例大概是这样的:

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

错误信息是这样的:

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

似乎Partial<AppState>setState的签名不兼容。 当然我可以通过类型断言来解决这个问题

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

但这并不理想,因为:
1) 这种语法非常冗长
2)更重要的是,类型断言可能会违反我的实际数据。 例如, newState as Pick<AppState, 'somethingElse'>也通过了检查,尽管它不适合我的数据。

我认为部分应该以某种方式与 Pick 兼容, 由于部分只是从 T 中选择不确定数量的键。我不确定我的理解是否准确。 无论如何,从我的角度来看,理想的用法应该是我可以传递类型为 Partial 的变量直接进入setState。

您能否考虑一下我的建议或指出我的误解(如果有)? 谢谢!

首先这是一个非常古老的变化。 因此,除非其他人最近更改了这一点,否则您可能会找错树。

那说。 部分允许未定义的值。

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

a 是有效的,但很明显,更新到您的状态的结果会使您的状态 foo 未定义,即使您声明这是不可能的。

因此部分不能分配给选择。 Pick 是确保您的类型不会说谎的正确答案

我觉得不允许:

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

是超级限制。

@Kovensky的解决方法是我所知道的唯一明智的解决方法,但编写起来仍然很痛苦。

有什么可以做来支持这种(我会说)相当常见的模式吗?

唯一可以做的就是删除类型安全

有人可以解释Pick<S, K> | S | null吗?

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

Tbh 我什至不知道为什么上面的签名甚至适用于部分状态更新,因为K被定义为keyof S所以Pick<S, K>基本上重新创建了S

Partial<S> | null不应该也做这项工作吗?

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

谁能解释一下...

几条回复已经解释清楚了。

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

是超级限制。

@Kovensky的解决方法是我所知道的唯一明智的解决方法,但编写起来仍然很痛苦。

我刚刚遇到了这个确切的问题,但我在线程中看不到任何对 Kovensky 的引用(也许有人更改了他们的用户名?)。 谁能指出我目前推荐的解决方法

@timrobinson33此评论解释了解决方法https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -351884578

这是一件好事,我们不必再用钩子担心这个了🙂

@timrobinson33此评论解释了解决方法#18365(评论)

非常感谢。 最后,我认为我的代码看起来更好,因为几个小的 setState 调用在它们之外带有If语句,即使这意味着某些路径将多次调用 setState。

我想这实际上类似于我们如何使用钩子,将状态视为我们独立更新的几个小事情。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

ArtemZag picture ArtemZag  ·  3评论

jamespero picture jamespero  ·  3评论

lilling picture lilling  ·  3评论

victor-guoyu picture victor-guoyu  ·  3评论

csharpner picture csharpner  ·  3评论