Definitelytyped: Sobre o uso de Pick para @ types / react's setState

Criado em 25 jul. 2017  ·  53Comentários  ·  Fonte: DefinitelyTyped/DefinitelyTyped

Eu entendo que Pick foi usado para o tipo de setState porque retornar undefined para uma chave que não deveria ser indefinida resultaria na chave sendo definida como indefinida por React.

No entanto, usar Pick causa outros problemas. Por um lado, o preenchimento automático do serviço de compilador torna-se inútil, pois usa o resultado de Pick para preenchimento automático e, quando você solicita conclusões, o resultado de Pick ainda não contém a chave que você pode deseja preencher automaticamente. Mas os problemas são particularmente graves ao escrever setState com um argumento de retorno de chamada:

  1. A lista de chaves é derivada de sua primeira instrução return ; se você não retornar uma chave específica em sua instrução return, também não poderá lê-la no argumento sem forçar a lista de chaves a ser redefinida para never . Várias instruções de retorno podem ser difíceis de escrever se retornarem chaves diferentes, especialmente se você tiver um retorno indefinido em algum lugar (por exemplo, if (state.busy) { return } ).

    • Isso pode ser contornado usando sempre a entrada em um spread (por exemplo, this.setState(input => ({ ...input, count: +input.count + 1 })) ), mas isso é redundante e uma desotimização, especialmente para estados maiores, pois setState passará o valor de retorno do retorno de chamada para Object.assign .

  2. Se, por algum motivo, o tipo que você está retornando não for compatível com o tipo de entrada, o Pick escolherá never para suas chaves, e a função terá permissão para retornar _qualquer coisa_. Mesmo as chaves que coincidem com uma chave existente permitem efetivamente any como um valor - se não couber, não é Pick ed e é tratada como uma propriedade em excesso para {} , que não está marcado.
  3. Se never for escolhido como o argumento genérico, por qualquer um dos motivos listados acima, um argumento de retorno de chamada pode, na verdade, ser tratado como um argumento para a forma de objeto de setState ; isso faz com que os argumentos do retorno de chamada sejam digitados any vez de {} . Não sei por que isso não é um erro implícito.
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
    }))
  }
}

Resumindo, embora o uso de Pick , apesar de alguns inconvenientes, ajude a detectar erros de tipo na forma não-callback de setState , é completamente contraproducente na forma callback; onde não apenas não cumpre a tarefa pretendida de proibir undefined mas também desativa qualquer tipo de verificação nas entradas ou saídas do callback.

Talvez deva ser alterado, pelo menos para o formulário de retorno de chamada, para Partial e esperar que os usuários saibam que não devem retornar undefined valores, como era feito nas definições anteriores.

Comentários muito úteis

A "correção" recente está causando problemas para mim agora com várias instruções de retorno em um retorno de chamada setState() .

Texto datilografado: 2.6.2 com todas as opções "strict" habilitadas, exceto para "strictFunctionTypes"
digita / reage: 16.0.30

Exemplo de código:

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

Erro do compilador:

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) => {
                          ~~~~~~~~~~~~~~~~

Todos 53 comentários

Obrigado pelo seu feedback! Este é um caso muito interessante que você mencionou.

Preciso pensar um pouco sobre as implicações antes de me comprometer com uma opinião.

A certa altura, @ahejlsberg queria tratar a opcionalidade de maneira diferente de | undefined . Assim, foo?: string significaria que foo não está definido ou é uma string.

Ao ler um valor de propriedade, a diferença é irrelevante 99,9% do tempo, mas para gravações, especialmente no caso de Partial<> , a distinção é extremamente importante.

Infelizmente, esta é uma mudança de linguagem de quebra, então devemos esperar pelo 3.0 ou tê-lo atrás de uma bandeira.

Se tivéssemos dito mudança, Partial<> torna-se extremamente útil para muitos, em vez do meu dogma atual que é rejeitar seu uso à primeira vista.

@ahejlsberg Eu sei que você é um homem ocupado, seria muito difícil implementá-lo? de forma que indefinido não é um valor atribuível implícito?

Tudo bem, depois de passar algum tempo esta manhã pensando sobre o problema, eu vejo algumas "soluções" para o problema que você propõe, cada uma com alguns efeitos colaterais bastante pesados.

1. Altere a opcionalidade ( interface State { foo?: string } ) para significar string ou não definida.

Exemplo:

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

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

Isso resolveria tecnicamente o problema e nos permitiria usar Partial<> , à custa de 98% do caso ser mais difícil. Isso quebra o mundo, então não seria realmente possível fazer até 3.xe geralmente, muito poucas bibliotecas / casos de uso realmente se preocupam com a distinção entre não definido e indefinido.

2. Basta mudar para parcial

Exemplo:

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

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

3. Não faça nada

Exemplo:

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. Adicione lógica aninhada a tipos literais que são unidos

Agora, quando temos vários caminhos de retorno, o compilador apenas todas as chaves potenciais em um gigante Pick<State, "foo" | "bar"> .

No entanto, uma alteração compatível com versões anteriores seria permitir que os literais fossem agrupados, como Pick<State, ("foo") | ("bar")> ou em um caso mais complexo: Pick<State, ("foo" | "bar") | ("baz")>

A falta de parênteses sugere que é apenas um único conjunto de valores que é a funcionalidade existente.

Agora, quando tentamos lançar {foo: number} em Pick<State, ("foo") | ("bar")> , podemos ter sucesso. O mesmo vale para {bar: number} .

Pick<State, ("foo") | ("bar")> também pode ser convertido em Partial<State> e em {foo: number} | {bar: number} .

Minha conclusão pessoal

(1) e (2) simplesmente não são viáveis. Um introduz complexidade para quase todos, não importa o que aconteça, e o outro ajuda as pessoas a produzir código que compila, mas é claramente incorreto.

(3) é completamente utilizável hoje e embora eu não me lembre por que adicionei | S como um valor de retorno potencial para a função em setState , não consigo imaginar outra razão para fazê-lo.

(4) poderia evitar a solução alternativa em (3), mas fica por conta dos desenvolvedores do TypeScript e talvez se @ahejlsberg se interessasse o suficiente, pudéssemos ver isso mais cedo ou mais tarde.

Isso me faz pensar que os tipos estão mais corretos hoje do que se os mudássemos.

O problema com a abordagem "não fazer nada" é que o compilador não está se comportando da maneira que você está descrevendo, caso você realmente cometa um erro de tipo real. Se eu modificar seu exemplo para, digamos, definir o valor para 0 vez de uma string vazia:

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

Eu vejo agora. De volta à prancheta. Vou ver se podemos criar uma solução inteligente para isso. Se não conseguirmos encontrar uma, precisamos avaliar a solução Partial<> que você propõe. A grande coisa a se considerar é se um undefined inesperado em um valor seria ou não mais comum / irritante do que um tipo incorreto inesperado.

Para esclarecer, temos duas sobrecargas para isso ...

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;

Definir um ou ambos para Partial<S> vez de Pick<S, K> não corrige seu problema. Ele ainda passa para a versão do objeto, que aceita o tipo errado por algum motivo:

`` `ts
interface State {
barra: string;
foo: número;
}

classe Foo estende React.Component <{}, State> {
blah public () {
this.setState ((prevState) => ({bar: 1})); // Erro não: /
}
} `` `

Poderíamos de alguma forma forçá-lo a rejeitar uma função usando um extends object
limitação?
Na sexta-feira, 28 de julho de 2017 às 0:00 Eric Anderson [email protected] escreveu:

Para esclarecer, temos duas sobrecargas para isso ...

setState(f: (prevState: Readonly , props: P) => Pick , callback ?: () => any): void; setState(estado: Pick , callback ?: () => any): void;

Definir um ou ambos como Parcial em vez de Selecionarnão resolve seu problema.

interface State {
barra: string;
foo: número;
}
classe Foo estende React.Component <{}, State> {
blah public () {
this.setState ((prevState) => ({bar: 1})); // Erro não: /
}
} `` `

-
Você está recebendo isso porque é o autor do tópico.
Responda a este e-mail diretamente, visualize-o no GitHub
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-318388471 ,
ou silenciar o tópico
https://github.com/notifications/unsubscribe-auth/AAEdfeEJ_Ejfana14fRII1OZuS7qTuyuks5sSKX3gaJpZM4OiDrc
.

Nós tentamos isso. Funções são objetos.

Eric L Anderson
Enviado do meu iPhone

Vim aqui para relatar um problema relacionado, que é que a refatoração do tipo de estado foi interrompida com essa mudança.

Por exemplo, consulte https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx - se você abrir no VS Code e F2 para refatorar / renomear something na linha 6 (https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L6), ele atualizará essa linha e https://github.com/tomduncalf/react-types-issue/ blob / master / Test.tsx # L14 , mas perderá o uso disso na chamada setState em https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx # L20

A única maneira que encontrei de fazê-lo funcionar corretamente é digitar explicitamente meu objeto as IState ao chamar setState , o que é um pouco inconveniente e fácil de esquecer.

Não estou familiarizado com a justificativa para a mudança exatamente, mas ela parece ter quebrado a segurança de tipo de uma forma bastante significativa ao trabalhar com o estado, então seria ótimo se houvesse uma maneira de resolver isso!

Obrigado,
Tom

Sim, novamente eu acredito que seria melhor usar Partial porque mantém essa informação de relação melhor.

Indiscutivelmente, Pick não sendo refatorado corretamente é uma limitação / bug do código de refatoração que poderia ser melhorado, mas ainda não acredito que Pick esteja fornecendo a segurança de tipo que https: // github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318385945 menções. Enquanto Partial permitiria undefined onde undefined não deveria estar, e o usuário então precisa ter cuidado para não fazer isso, Pick permite qualquer coisa porque qualquer tipo que não esteja em conformidade com a interface faria com que, em vez de um erro de tipo, Pick gerasse uma interface vazia, que aceita qualquer coisa.

Quanto a https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318388471, isso poderia ser um bug no verificador de propriedade em excesso? Ou isso foi testado antes de o verificador de propriedade em excesso se tornar mais rígido no 2.3 ou 2.4 (esqueci)?

Outro problema que acabei de notar aqui é que se um componente não tiver nenhum tipo de estado (ou seja, apenas um parâmetro de tipo para React.Component ) ou o tipo de estado estiver vazio ( {} ), o Typescript ainda permitirá chamadas para setState sem gerar um erro, o que me parece incorreto - consulte https://github.com/tomduncalf/react-types-issue/blob/master/Test2.tsx

A solução usando Partial parece preferível para mim - posso tentar fazer o patch sozinho e ver como funciona.

Obrigado,
Tom

Olá a todos!
Não sei por que, mas se eu juntar setState declarações a uma única declaração:

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

Funciona como esperado para mim:

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

Isso pode mudar o suficiente para corrigir o problema original?

@mctep Nice find.

E se eu pegar o que você fez e estendê-lo um pouquinho, para ser Partial<S> & Pick<S, K> vez de Pick<S, K> em alguns lugares, o intellisense sugere nomes-chave para você. Infelizmente, os nomes das chaves dizem que o valor da propriedade é "algo | indefinido", mas quando você o compila, ele sabe melhor:

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

Vou colocar essa mudança mais tarde hoje

A "correção" recente está causando problemas para mim agora com várias instruções de retorno em um retorno de chamada setState() .

Texto datilografado: 2.6.2 com todas as opções "strict" habilitadas, exceto para "strictFunctionTypes"
digita / reage: 16.0.30

Exemplo de código:

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

Erro do compilador:

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) => {
                          ~~~~~~~~~~~~~~~~

Também estou vendo o problema descrito por

Tenho quase certeza de que isso sempre foi um problema.

Tenho certeza de que nem sempre foi um problema. Eu tenho um projeto com várias declarações de retorno em setState() callbacks que foram compilados sem problemas por mais de 2 meses. Tenho acompanhado as atualizações de dependência do NPM a cada 2 semanas ou mais, e este erro do compilador começou a acontecer para mim hoje, depois de atualizar para a versão mais recente de types / react. A versão anterior não produziu este erro do compilador.

Tem sido um problema desde a mudança de parcial para escolha. A solução alternativa seria fornecer a lista de chaves que você pretende retornar para o parâmetro genérico de setState , mas então você será forçado a sempre retornar todas as chaves ...

O que eu faço nesses casos é sempre retornar todas as chaves, com as chaves não modificadas sendo definidas como key: prevState.key , ou retornando um spread com prevState ( { ...prevState, newKey: newValue } ).

Talvez eu realmente tenha um caso de borda mais específico em meu código que estava funcionando anteriormente por coincidência? Um exemplo real do meu projeto é mais parecido com este, em que retorna um objeto vazio (para não alterar nenhum estado) ou retorna um objeto não vazio:

interface TestState {
    a: boolean,
    b: boolean
}

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

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

return null e return undefined também são valores de retorno perfeitamente aceitáveis ​​para não alterar o estado (eles passarão por Object.assign e, portanto, não farão alterações em this.state ).

Me lembra, a assinatura atual não permite nenhum deles. Talvez null deva ser permitido como um valor de retorno, pelo menos, pois não é algo que pode resultar do esquecimento acidental de return .


De qualquer forma, nos casos em que a assinatura é ambígua, o TypeScript parece escolher apenas a primeira instrução return na ordem de origem e usá-la para derivar os parâmetros genéricos. Parece ser capaz de mesclar os tipos de genéricos simples (por exemplo, Array ou Promise ), mas nunca os mescla se o tipo contextual for um tipo mapeado como Pick .

Estou vendo algumas regressões com a versão não funcional com os tipos mais recentes. Em particular, o tipo de argumento "state" ao passar um argumento mudou de:

setState( estado: Pick , callback ?: () => any): void;

para:

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

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

A adição de & Partial<S> parece estar quebrando coisas que funcionavam anteriormente.

Aqui está uma reprodução mínima:

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

isso falha com o erro:

(429,18): Argumento do tipo '{baseProp: "foobar"; } 'não é atribuível ao parâmetro do tipo' ((prevState: Somente leitura Tipo '{baseProp: "foobar";}' não é atribuível ao tipo 'Escolha & tipo parcial ' {baseProp: "foobar";} 'não é atribuível a tipo 'parcial

Alterar o tipo de estado de S &{ baseProp: string } para apenas { baseProp: string } também fará com que o erro desapareça (embora isso interrompa as classes reais que especificam o tipo S).

Isso é interessante. Estou feliz em reverter a parte parcial e escolher a parte da mudança

Direi que o que você relata parece um bug no TS, especificamente:

Digite '{baseProp: "foobar"; } 'não pode ser atribuído ao tipo' Parcial

Tudo bem. Talvez o TS não saiba o que está acontecendo porque não há relação entre S e baseProp.

Pode-se passar um S do tipo { baseProp: número }, caso em que é correto que você não pode atribuir uma string a baseProp.

Talvez se S estendesse a versão baseProp?

Eu tinha tentado algo assim antes:

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

enquanto o acesso está ok, a chamada setState tem o mesmo erro:

Argumento do tipo '{baseProp: "foobar"; } 'não é atribuível ao parâmetro do tipo' ((prevState: Readonly \ , props: P) => Pick & Partial \ ) |
} 'não pode ser atribuído ao tipo' Seleção e parcial \ '.Digite '{baseProp: "foobar";

Provavelmente não é uma boa maneira de estruturá-lo - afinal, alguma classe filha poderia declarar variáveis ​​de estado que colidiram com variáveis ​​na classe base. Portanto, o texto datilografado pode estar certo em reclamar que não é possível ter certeza do que vai acontecer lá. É estranho que ele não tenha nenhum problema em acessar esses adereços.

Hmm. Isso definitivamente parece um bug no TS agora.

Vou brincar com isso hoje e, possivelmente, tirar o & Partial.

Vou adicionar isso como um caso de teste também e tentar outra ideia para tornar o intellisense feliz

Oi,
Mesmo problema aqui...

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

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

O erro só começou a acontecer após essa mudança. Na versão 16.0.10 funcionou bem.

O texto datilografado tem vários problemas relacionados, eles os fecharam conforme planejado:

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

Fiz alguns exemplos aqui: https://gist.github.com/afarnsworth-valve/93d1096d1410b0f2efb2c94f86de9c84

Embora o comportamento ainda pareça estranho. Em particular, você pode atribuir a uma variável digitada como a classe base sem conversão e, em seguida, fazer as mesmas chamadas sem problemas. Este problema parece sugerir que atribuição e comparação são duas coisas diferentes.

Não esqueci de vocês. Vai consertar em breve

O PR referenciado deve resolver este problema. Eu também adicionei testes que devem proteger contra quebrar este caso extremo novamente.

@ericanderson Obrigado pela resposta rápida :)

A nova correção tem alguma limitação / desvantagem?

Nenhum que eu conheça atualmente. Consegui manter o intellisense (na verdade melhor do que antes, pois resolve alguns casos extremos).

Para elaborar, a solução & Partial<S> era enganar o intellisense para revelar possíveis parâmetros, mas o fez afirmando que eles são X | undefined . Isso, é claro, não conseguiria compilar, mas era um pouco confuso. Obter | S corrige o intellisense de forma que sugere todos os parâmetros corretos e agora não mostra um tipo falso.

Tentei adicionar null como um possível valor de retorno do retorno de chamada setState (o que você pode retornar para indicar que não deseja alterar nenhum estado, sem fazer return; também válido), mas isso fez com que a inferência de tipo desistisse completamente e pegasse never como as chaves 😢

Por enquanto, nos retornos de chamada em que realmente desejo pular o estado de atualização, estou usando return null! . O tipo never desse retorno faz com que o TypeScript ignore o retorno para a inferência de tipo genérico.

Oi, pessoal...

O último commit corrigiu meu problema.
Obrigado pela resposta rápida :)

Qual versão tem a correção? É publicado no npm? Estou chegando ao caso em que a versão de retorno de chamada de setState está me informando que há uma incompatibilidade de propriedade no valor de retorno, assim como @UselessPickles encontrado.

Deve ser bom nos últimos @ types / react

Consertei para as séries 15 e 16

import produce from 'immer';

interface IComponentState
{
    numberList: number[];
}

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

A definição de tipo antigo do 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;

..produz este erro:

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

Tentei esta sugestão:

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

O Intellisense agora pode detectar a propriedade do estado de React. No entanto, a propriedade detectada agora é percebida como possibly undefined .

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

Teria que usar o operador de asserção nulo, embora numberList não seja indefinido nem anulável:

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

Vou ficar na definição de tipo antigo até que o sensor de tipo seja aprimorado. Por enquanto, apenas declararei explicitamente o tipo no parâmetro genérico do produto immer. produce<IComponentState> é mais fácil de raciocinar do que list! .

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

Seu primeiro erro é porque você não está devolvendo nada. Não é assim que setState funciona.

Funciona mesmo quando não retorna uma variável no produto. Acabei de seguir o exemplo aqui (não TypeScript):

https://github.com/mweststrate/immer

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

A única coisa que impede o TypeScript de executar esse código é que ele inferiu incorretamente o tipo da definição de tipo React. A definição de tipo relata um erro de numberList does not exist on type Pick<IComponentState, never> . Consegui apenas eliminar o erro de compilação passando explicitamente o tipo no parâmetro genérico do produto, ou seja, produce<IComponentState> .

Eu até tentei retornar a variável em produzir e ver se isso ajudaria a definição de tipo do React a inferir o tipo (um problema de ovo e galinha), mas ainda não há como a definição de tipo do React detectar o tipo correto do estado. Portanto, o intellisense para o calado não está aparecendo:

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

Ou talvez eu tenha uma expectativa errada do compilador :) O compilador não pode fazer um tipo para a variável de rascunho com base no tipo de setState enquanto o compilador processa o código de dentro para fora. No entanto, a definição de tipo sugerida de alguma forma me fez pensar que o compilador pode processar o código de fora para dentro, que pode escolher o melhor tipo que pode passar do código externo ( setState ) para o código interno ( 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;

Com a definição de tipo acima, o compilador pode detectar que o rascunho possui uma propriedade numberList . Ele detecta como possibly undefined embora:

image

Após alguns ajustes, tornei o compilador capaz de passar o tipo do estado para o rascunho do produto adicionando S à definição de tipo de 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;

O código está compilando agora :)

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

Seu erro não é com setState, seu erro está dentro do produto. Qual é a sua definição de tipo para produto?

Meu PR acima tem um erro no script de teste do DefinitelyTyped, não testei localmente. Então, estou testando localmente agora.

Aqui está a definição do tipo immer / produzir.

/**
 * 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, você poderia me indicar a discussão sobre por que Pick é usado em vez de Partial ? Isso tem me causado horas de pesar (usando apenas setState(obj) , não a versão de retorno de chamada) e, por enquanto, vou usar this.setState(newState as State) como solução alternativa. Só quero entender por que mudou, porque devo estar faltando alguma coisa.

Olá @ericanderson ,

Tenho alguns problemas com a definição mais recente.

Meu caso de uso é resumidamente assim:

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
}

A mensagem de erro é assim:

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'.

Parece que Partial<AppState> não é compatível com a assinatura de setState . Claro que posso resolver isso por tipo de asserção, como

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

mas não é o ideal, porque:
1) esta sintaxe é muito detalhada
2) mais importante, a asserção de tipo pode violar meus dados reais. Por exemplo, newState as Pick<AppState, 'somethingElse'> também passa na verificação, embora não se encaixe em meus dados.

Eu acho parcialdeve de alguma forma compatível com Pick, já que parcialapenas escolhe um número incerto de chaves de T. Não tenho certeza se meu entendimento está correto. De qualquer forma, o uso ideal do meu ponto de vista deve ser que eu possa passar a variável digitada Partialdiretamente em setState.

Você poderia gentilmente considerar minha sugestão ou apontar meu mal-entendido, se houver? Obrigado!

Esta é uma mudança muito antiga, em primeiro lugar. Portanto, a menos que alguém tenha mudado isso recentemente, você provavelmente está latindo na árvore errada.

Dito isto. Parcial permite valores indefinidos.

const a: Parcial <{foo: string}> = {foo: indefinido}

a é válido, mas claramente o resultado dessa atualização em seu estado coloca seu estado com foo sendo indefinido, embora você tenha declarado que isso é impossível.

Portanto, um parcial não pode ser atribuído a um Pick. E Pick é a resposta certa para garantir que seus tipos não mentem

Eu sinto que não permitindo:

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

é super restritivo.

A solução alternativa de @Kovensky é a única solução sensata que conheço, mas ainda dolorosa de escrever.

Existe algo que pode ser feito para apoiar este (eu diria) padrão bastante comum?

A única coisa que pode ser feita é remover o tipo de segurança

Alguém pode explicar o motivo de 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, eu nem mesmo agora por que a assinatura acima funciona para atualizações de estado parciais como K é definido como keyof S então Pick<S, K> essencialmente recria S ?

Partial<S> | null não deveria fazer o trabalho também?

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

Alguém pode explicar ...

Foi explicado claramente algumas respostas acima.

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

é super restritivo.

A solução alternativa de @Kovensky é a única solução sensata que conheço, mas ainda dolorosa de escrever.

Acabei de encontrar exatamente esse problema, mas não consigo ver nenhuma referência a Kovensky no tópico (talvez alguém tenha alterado o nome de usuário?). Alguém pode me indicar a solução alternativa atualmente recomendada

@ timrobinson33 Este comentário explica a solução alternativa https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -351884578

E é uma coisa boa não termos que nos preocupar mais com isso com ganchos 🙂

@ timrobinson33 Este comentário explica a solução alternativa # 18365 (comentário)

Muito obrigado por isso. No final, achei que meu código parecia melhor com várias pequenas chamadas setState com instruções If fora delas, embora isso signifique que alguns caminhos chamarão setState mais de uma vez.

Eu acho que isso é realmente semelhante a como trabalhamos com ganchos, vendo o estado como várias pequenas coisas que atualizamos independentemente.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

Zzzen picture Zzzen  ·  3Comentários

csharpner picture csharpner  ·  3Comentários

JudeAlquiza picture JudeAlquiza  ·  3Comentários

jbreckmckye picture jbreckmckye  ·  3Comentários

alisabzevari picture alisabzevari  ·  3Comentários
bleepcoder.com usa informações licenciadas publicamente pela GitHub para fornecer aos desenvolvedores em todo o mundo soluções para seus problemas. Não somos afiliados à GitHub, Inc. nem a nenhum desenvolvedor que utilize GitHub para seus projetos. Nós não hospedamos nenhum dos vídeos ou imagens em nossos servidores. Todos os direitos pertencem a seus respectivos proprietários.
Fonte para esta página: Fonte

Linguagens de programação populares
Projetos populares do GitHub
Mais projetos GitHub

© 2024 bleepcoder.com - Contact
Made with in the Dominican Republic.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.