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

Creado en 25 jul. 2017  ·  53Comentarios  ·  Fuente: DefinitelyTyped/DefinitelyTyped

Entiendo que Pick se usó para el tipo de setState porque devolver undefined para una clave que no debería estar indefinida daría como resultado que React estableciera la clave como indefinida.

Sin embargo, usar Pick causa otros problemas. Por un lado, el autocompletado del servicio del compilador se vuelve inútil, ya que usa el resultado de Pick para el autocompletado, y cuando solicita completaciones, el resultado de Pick aún no contiene la clave que puede desea autocompletar. Pero los problemas son particularmente graves al escribir setState con un argumento de devolución de llamada:

  1. La lista de claves se deriva de su primera instrucción return ; si no devuelve una clave en particular en su declaración de devolución, tampoco puede leerla en el argumento sin forzar la lista de claves a restablecerse a never . Varias declaraciones de devolución pueden ser difíciles de escribir si devuelven claves diferentes, especialmente si tiene una devolución indefinida en algún lugar (por ejemplo, if (state.busy) { return } ).

    • Esto se puede solucionar usando siempre la entrada en un spread (por ejemplo, this.setState(input => ({ ...input, count: +input.count + 1 })) ) pero esto es redundante y una desoptimización, particularmente para estados más grandes, ya que setState pasará el valor de retorno de la devolución de llamada hasta Object.assign .

  2. Si, por alguna razón, el tipo que está devolviendo no es compatible con el tipo de entrada, Pick elegirá never para sus claves, y la función podrá devolver _cualquier cosa_. Incluso las claves que coinciden con una clave existente permiten efectivamente any como valor; si no encaja, simplemente no es Pick ed, y se trata como una propiedad en exceso por {} , que no está marcado.
  3. Si se elige never como argumento genérico, por cualquiera de las razones enumeradas anteriormente, un argumento de devolución de llamada puede ser tratado como un argumento para la forma de objeto de setState ; esto hace que los argumentos de la devolución de llamada se escriban any lugar de {} . No estoy seguro de por qué esto no es un error 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
    }))
  }
}

En resumen, mientras que el uso de Pick , a pesar de algunos inconvenientes, ayuda a detectar errores de tipo en la forma sin devolución de llamada de setState , es completamente contraproducente en la forma de devolución de llamada; donde no solo no realiza la tarea prevista de prohibir undefined sino que también deshabilita cualquier tipo de verificación en las entradas o salidas de la devolución de llamada.

Quizás debería cambiarse, al menos para el formulario de devolución de llamada, a Partial y esperar que los usuarios sepan que no deben devolver los valores undefined , como se hacía en las definiciones anteriores.

Comentario más útil

La "solución" reciente me está causando problemas ahora con múltiples declaraciones de devolución dentro de una devolución setState() llamada

Mecanografiado: 2.6.2 con todas las opciones "estrictas" habilitadas excepto para "StrictFunctionTypes"
tipos / reaccionar: 16.0.30

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

Error del 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 comentarios

Gracias por tus comentarios. Este es un caso muy interesante que mencionas.

Necesito pensar un poco en las implicaciones antes de comprometerme con una opinión.

En un momento, @ahejlsberg quiso tratar la opcionalidad de manera diferente a | undefined . Por lo tanto, foo?: string significaría que foo no está configurado o es una cadena.

Al leer el valor de una propiedad, la diferencia es irrelevante el 99,9% de las veces, pero para las escrituras, especialmente en el caso de Partial<> , la distinción es tremendamente importante.

Desafortunadamente, este es un cambio de idioma importante, por lo que debemos esperar a 3.0 o tenerlo detrás de una bandera.

Si hubiéramos dicho cambio, Partial<> vuelve extremadamente útil para muchos en lugar de mi dogma actual que es rechazar su uso a primera vista.

@ahejlsberg Sé que eres un hombre ocupado, ¿qué tan difícil sería implementarlo? tal que indefinido no es un valor asignable implícito?

Muy bien, después de pasar un tiempo esta mañana pensando en el problema, veo algunas "soluciones" al problema que propones, cada una con algunos efectos secundarios bastante importantes.

1. Cambie la opción ( interface State { foo?: string } ) para que signifique cadena o no establecida.

Ejemplo:

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

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

Esto resolvería técnicamente el problema y nos permitiría usar Partial<> , a expensas de que el caso del 98% sea más difícil. Esto rompe el mundo, por lo que realmente no sería posible hacerlo hasta 3.xy, en general, muy pocas bibliotecas / casos de uso realmente se preocupan por la distinción entre no establecido y no definido.

2. Simplemente cambie a parcial

Ejemplo:

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

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

3. No hacer nada

Ejemplo:

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. Agregue lógica anidada a los tipos literales que se unen.

En este momento, cuando tenemos múltiples rutas de retorno, el compilador solo todas las claves potenciales en un Pick<State, "foo" | "bar"> gigante.

Sin embargo, un cambio compatible con versiones anteriores sería permitir que los literales se agrupen, como Pick<State, ("foo") | ("bar")> o en un caso más complejo: Pick<State, ("foo" | "bar") | ("baz")>

La falta de parens sugiere que es solo un conjunto de valores que es la funcionalidad existente.

Ahora, cuando intentamos lanzar {foo: number} a Pick<State, ("foo") | ("bar")> , podemos tener éxito. Lo mismo con {bar: number} .

Pick<State, ("foo") | ("bar")> también se puede convertir en Partial<State> y en {foo: number} | {bar: number} .

Mi conclusión personal

(1) y (2) simplemente no son factibles. Uno introduce complejidad para casi todo el mundo, pase lo que pase, y el otro ayuda a las personas a producir código que se compila pero que es claramente incorrecto.

(3) es completamente utilizable hoy y, aunque no recuerdo por qué agregué | S como valor de retorno potencial a la función en setState , no puedo entender ninguna otra razón para hacerlo.

(4) podría obviar la solución en (3) pero depende de los desarrolladores de TypeScript y tal vez si conseguimos que @ahejlsberg se interese lo suficiente, podríamos verlo más temprano que tarde.

Esto me deja pensando que los tipos son más correctos hoy que si los cambiamos.

El problema con el enfoque de "no hacer nada" es que el compilador no se comporta de la manera que usted describe en el caso de que realmente cometa un error de tipo real. Si modifico su ejemplo para, digamos, establecer el valor en 0 lugar de una cadena vacía:

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

Ya lo veo. De vuelta a la mesa de dibujo. Voy a ver si podemos crear una solución inteligente para esto. Si no podemos encontrar uno, entonces debemos evaluar la solución Partial<> que propones. Lo más importante a considerar es si un undefined inesperado en un valor sería más común / molesto que un tipo incorrecto inesperado.

Para aclarar, tenemos dos sobrecargas para esto ...

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;

Establecer uno o ambos en Partial<S> lugar de Pick<S, K> no soluciona el problema. Todavía pasa a la versión del objeto, que acepta el tipo incorrecto por alguna razón:

ts
estado de la interfaz {
barra: cuerda;
foo: número;
}

clase Foo extiende React.Component <{}, State> {
público bla () {
this.setState ((prevState) => ({bar: 1})); // No error: /
}
} `` `

¿Podríamos de alguna manera obligarlo a rechazar una función usando un extends object
¿restricción?
El viernes 28 de julio de 2017 a las 0:00, Eric Anderson [email protected] escribió:

Para aclarar, tenemos dos sobrecargas para esto ...

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

Establecer uno o ambos de estos en Parcial en lugar de Seleccionarno soluciona su problema.

estado de la interfaz {
barra: cuerda;
foo: número;
}
clase Foo extiende React.Component <{}, State> {
público bla () {
this.setState ((prevState) => ({bar: 1})); // No error: /
}
} `` `

-
Estás recibiendo esto porque eres el autor del hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-318388471 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAEdfeEJ_Ejfana14fRII1OZuS7qTuyuks5sSKX3gaJpZM4OiDrc
.

Lo intentamos. Las funciones son objetos.

Eric L. Anderson
Enviado desde mi iPhone

Vine aquí para informar un problema relacionado, que es que la refactorización del tipo de estado se rompe con este cambio.

Por ejemplo, consulte https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx - si abre esto en VS Code y F2 para refactorizar / renombrar something en la línea 6 (https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L6), actualizará esa línea y https://github.com/tomduncalf/react-types-issue/ blob / master / Test.tsx # L14 , pero se perderá el uso de esto en la llamada setState en https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx # L20

La única forma que he encontrado para que funcione correctamente es escribir explícitamente mi objeto as IState al llamar a setState , lo cual es un poco inconveniente y bastante fácil de olvidar.

No estoy familiarizado con la justificación del cambio exactamente, pero parece haber roto la seguridad de tipos de una manera bastante significativa cuando se trabaja con el estado, por lo que sería genial si hubiera una manera de resolverlo.

Gracias,
Tomás

Sí, nuevamente creo que sería mejor usar Partial ya que mantiene mejor la información de la relación.

Podría decirse que Pick no se refactoriza correctamente es una limitación / error del código de refactorización que podría mejorarse, pero todavía no creo que Pick proporcione la seguridad de tipos que https: // github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318385945 menciona. Si bien Partial permitiría undefined donde se supone que undefined no debe estar, y el usuario debe tener cuidado de no hacer eso, Pick permite cualquier cosa porque cualquier tipo que no se ajuste a la interfaz causaría, en lugar de un error de tipo, Pick generar una interfaz vacía en su lugar, que acepta cualquier cosa.

En cuanto a https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318388471, ¿podría tratarse de un error en el comprobador de exceso de propiedad? ¿O se probó esto antes de que el verificador de exceso de propiedad se hiciera más estricto en 2.3 o 2.4 (lo olvidé)?

Otro problema que acabo de notar aquí es que si un componente no tiene un tipo de estado (es decir, solo un parámetro de tipo para React.Component ), o el tipo de estado está vacío ( {} ), Typescript aún permitirá llamadas a setState sin arrojar un error, que me parece incorrecto; consulte https://github.com/tomduncalf/react-types-issue/blob/master/Test2.tsx

La solución que usa Parcial suena preferible para mí; podría intentar parchearla yo mismo y ver cómo funciona.

Gracias,
Tomás

¡Hola a todos!
No sé por qué, pero si me uno a las declaraciones setState a una declaración única:

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

Funciona como se esperaba para mí:

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

¿Puede esto cambiar lo suficiente para solucionar el problema original?

@mctep Buen hallazgo.

Y si tomo lo que hiciste y lo extiendo un poquito, para que sea Partial<S> & Pick<S, K> lugar de Pick<S, K> en algunos lugares, intellisense sugiere nombres clave para ti. Desafortunadamente, los nombres de las claves dicen que el valor de la propiedad es "algo | indefinido", pero cuando lo compila, lo sabe mejor:

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

Pondré este cambio más tarde hoy

La "solución" reciente me está causando problemas ahora con múltiples declaraciones de devolución dentro de una devolución setState() llamada

Mecanografiado: 2.6.2 con todas las opciones "estrictas" habilitadas excepto para "StrictFunctionTypes"
tipos / reaccionar: 16.0.30

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

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

También viendo el problema descrito por @UselessPickles como resultado de este cambio.

Estoy bastante seguro de que eso siempre ha sido un problema.

Estoy más que bastante seguro de que no siempre ha sido un problema. Tengo un proyecto con múltiples declaraciones de devolución en setState() devoluciones

Ha sido un problema desde el cambio de Partial a Pick. La solución alternativa sería dar la lista de claves que desea devolver al parámetro genérico de setState , pero luego se verá obligado a devolver siempre todas las claves ...

Lo que hago en esos casos es o siempre devuelvo todas las claves, con las claves no modificadas configuradas en key: prevState.key , o devolviendo un margen con prevState ( { ...prevState, newKey: newValue } ).

¿Quizás tengo un caso de borde más específico en mi código que funcionaba anteriormente por coincidencia? Un ejemplo real de mi proyecto es más parecido a este, donde devuelve un objeto vacío (para no cambiar ningún estado) o devuelve un objeto no vacío:

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 y return undefined también son valores de retorno perfectamente aceptables para no cambiar de estado (pasarán por Object.assign y, por lo tanto, no cambiarán this.state ).

Me recuerda que la firma actual no permite ninguno de ellos. Tal vez null debería permitirse como valor de retorno, al menos, ya que no es algo que pueda surgir de olvidar accidentalmente return en absoluto.


De todos modos, en los casos en que la firma es ambigua, TypeScript parece elegir solo la primera declaración return en el orden de origen y usarla para derivar los parámetros genéricos. Parece poder fusionar los tipos para genéricos simples (por ejemplo, Array o Promise ), pero nunca los fusiona si el tipo contextual es un tipo mapeado como Pick .

Veo algunas regresiones con la versión sin función con los tipos más recientes. En particular, el tipo de argumento "estado" al pasar un argumento ha cambiado de:

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

para:

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

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

La adición de & Partial<S> parece estar rompiendo cosas que antes funcionaban.

Aquí hay una reproducción mínima:

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

esto falla con error:

(429,18): Argumento de tipo '{baseProp: "foobar"; } 'no se puede asignar al parámetro de tipo' ((prevState: Readonly Type '{baseProp: "foobar";}' no se puede asignar al tipo 'Pick & Partial Type' {baseProp: "foobar";} 'no se puede asignar a tipo 'Parcial

Cambiar el tipo de estado de S &{ baseProp: string } a solo { baseProp: string } también hará que el error desaparezca (aunque eso romperá las clases reales que especifican el tipo S).

Eso es interesante. Me complace revertir la parte parcial y seleccionar la parte del cambio

Diré que lo que informa suena como un error en TS, específicamente:

Escriba '{baseProp: "foobar"; } 'no se puede asignar al tipo' Parcial

Bien. Quizás TS no pueda saber qué pasa porque no existe una relación entre S y baseProp.

Se podría pasar una S de tipo { baseProp: number }, en cuyo caso es correcto que no se pueda asignar una cadena a baseProp.

¿Quizás si S extendiera la versión baseProp?

Había intentado algo como esto 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' } );
    }
}

mientras el acceso es correcto, la llamada setState tiene el mismo error:

Argumento de tipo '{baseProp: "foobar"; } 'no se puede asignar a un parámetro de tipo' ((prevState: Readonly \ , props: P) => Pick & Partial \ ) |
} 'no se puede asignar al tipo' Pick & Partial \ '.Escriba '{baseProp: "foobar";

Probablemente no sea una buena forma de estructurarlo; después de todo, alguna clase secundaria podría declarar variables de estado que colisionan con variables en la clase base. Entonces, mecanografiado podría tener razón al quejarse de que no puede estar seguro de lo que sucederá allí. Sin embargo, es extraño que no tenga problemas para acceder a esos accesorios.

Mmm. Esto definitivamente se siente como un error en TS ahora.

Jugaré con esto hoy y posiblemente sacaré el & Partial.

También agregaré esto como un caso de prueba y probaré otra idea para hacer feliz a intellisense

Hola,
El mismo 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>
        );
    }
}

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

El error acaba de comenzar a ocurrir después de este cambio. En la versión 16.0.10 funcionó bien.

TypeScript tiene varios problemas relacionados, los han cerrado porque funcionan según lo diseñado:

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

Hice un resumen de algunos ejemplos aquí: https://gist.github.com/afarnsworth-valve/93d1096d1410b0f2efb2c94f86de9c84

Aunque el comportamiento todavía parece extraño. En particular, puede asignar a una variable escrita como clase base sin conversión, y luego hacer las mismas llamadas con eso sin problema. Este problema parece sugerir que la asignación y la comparación son dos cosas diferentes.

No los he olvidado, chicos. Lo arreglará pronto

El RP al que se hace referencia debería resolver este problema. También agregué pruebas que deberían proteger contra la rotura de este caso de borde nuevamente.

@ericanderson Gracias por la rápida respuesta :)

¿La nueva solución tiene limitaciones o desventajas?

Ninguno que yo sepa actualmente. Pude mantener intellisense (en realidad mejor que antes, ya que resuelve algunos casos extremos).

Para elaborar, la solución & Partial<S> fue engañar a intellisense para revelar posibles parámetros, pero lo hizo indicando que son X | undefined . Esto, por supuesto, no se compilaría, pero era un poco confuso. Obtener el | S corrige el intellisense para que sugiera todos los parámetros correctos y ahora no muestra un tipo falso.

Intenté agregar null como un posible valor de retorno de la devolución setState llamada return; también válido), pero eso hizo que la inferencia de tipo se rindiera por completo y eligiera never como claves 😢

Por ahora, en las devoluciones de llamada donde realmente quiero omitir el estado de actualización, he estado usando return null! . El tipo never de este retorno hace que TypeScript ignore el retorno de la inferencia de tipo genérico.

Hola tios...

La última confirmación solucionó mi problema.
Gracias por la rápida respuesta :)

¿Qué versión tiene la solución? ¿Está publicado en npm? Estoy llegando al caso en el que la versión de devolución de llamada de setState me dice que hay una discrepancia de propiedad en el valor de retorno, al igual que @UselessPickles encontró.

Debería ser bueno en los últimos @ tipos / reaccionar

Lo arreglé para las series 15 y 16

import produce from 'immer';

interface IComponentState
{
    numberList: number[];
}

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

La antigua definición de tipo de 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;

..produce este error:

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

Probé esta sugerencia:

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 ahora puede sentir la propiedad del estado de React. Sin embargo, la propiedad detectada ahora se percibe como possibly undefined .

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

Tendría que usar un operador de aserción nula a pesar de que numberList no es indefinido ni anulable:

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

Me quedaré en la definición de tipo antiguo hasta que se mejore la detección de tipos. Mientras tanto, solo declararé explícitamente el tipo en el parámetro genérico de immer produce. produce<IComponentState> es más fácil de razonar que list! .

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

Su primer error se debe a que no devuelve nada. No es así como funciona setState.

Funciona incluso cuando no devuelve una variable en producir. Acabo de seguir el ejemplo aquí (no TypeScript):

https://github.com/mweststrate/immer

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

Lo único que impide que TypeScript pueda ejecutar ese código es que tiene un tipo inferido incorrectamente de la definición de tipo de React. La definición de tipo informa un error de numberList does not exist on type Pick<IComponentState, never> . Solo puedo eliminar el error de compilación pasando explícitamente el tipo en el parámetro genérico de produce, es decir, produce<IComponentState> .

Incluso intenté devolver la variable en producir y ver si ayudaría a la definición de tipo de React a inferir el tipo (aunque un problema de pollito y huevo), pero aún no hay forma de que la definición de tipo de React detecte el tipo correcto del estado. Por lo tanto, el intellisense para borrador no aparece:

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

O tal vez tengo una expectativa incorrecta del compilador :) El compilador no puede crear un tipo para la variable borrador basado en el tipo de setState ya que el compilador procesa el código de adentro hacia afuera. Sin embargo, la definición de tipo sugerida de alguna manera me hizo pensar que el compilador puede procesar el código de afuera hacia adentro, que puede elegir el mejor tipo que puede pasar del código externo ( setState ) al 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;

Con la definición de tipo anterior, el compilador puede detectar que el borrador tiene una propiedad numberList . Aunque lo detecta como possibly undefined :

image

Tras realizar más retoques, hice que el compilador pudiera pasar el tipo del estado para producir el borrador agregando S a la definición 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;

El código se está compilando ahora :)

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

Su error no es con setState, su error está dentro del producto. ¿Cuál es su definición de tipo para producir?

Mi PR anterior tiene un error en el script de prueba de DefinitelyTyped, no probé localmente. Así que ahora lo estoy probando localmente.

Aquí está la definición de tipo 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, ¿ podría indicarme la discusión sobre por qué se usa Pick lugar de Partial ? Esto me ha estado causando horas de dolor (usando setState(obj) simple, no la versión de devolución de llamada), y por ahora voy con this.setState(newState as State) como solución. Solo quiero entender por qué se cambió, porque debo estar perdiendo algo.

Hola @ericanderson ,

Tengo algún problema con la última definición.

Mi caso de uso es brevemente así:

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
}

El mensaje de error es como:

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> no es compatible con la firma de setState . Por supuesto que puedo resolver esto mediante una afirmación de tipo como

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

pero no es ideal, porque:
1) esta sintaxis es muy detallada
2) lo que es más importante, la afirmación de tipo podría violar mis datos reales. Por ejemplo, newState as Pick<AppState, 'somethingElse'> también pasa la verificación, aunque no se ajusta a mis datos.

Creo que parcialdebería ser compatible de alguna manera con Pick, desde Parcialsimplemente elige un número incierto de claves de T. No estoy seguro de que mi comprensión sea precisa. De todos modos, el uso ideal desde mi punto de vista debería ser que pueda pasar la variable escrita Parcialdirectamente en setState.

¿Podría considerar amablemente mi sugerencia o señalar mi malentendido si lo hay? ¡Gracias!

Este es un cambio muy antiguo en primer lugar. Entonces, a menos que alguien más haya cambiado esto recientemente, probablemente esté ladrando al árbol equivocado.

Dicho eso. Parcial permite valores indefinidos.

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

a es válido, pero claramente el resultado de esa actualización de su estado pone a su estado con foo indefinido a pesar de que usted declaró que eso es imposible.

Por lo tanto, un parcial no se puede asignar a un Pick. Y Pick es la respuesta correcta para asegurarse de que sus tipos no mientan

Siento que no permitiendo:

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

es super restrictivo.

La solución de @Kovensky es la única solución sensata que conozco, pero sigue siendo dolorosa de escribir.

¿Hay algo que se pueda hacer para respaldar este (yo diría) patrón bastante común?

Lo único que se puede hacer es eliminar la seguridad de tipos

¿Alguien puede explicar el razonamiento 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, ni siquiera ahora, ¿por qué la firma anterior funciona incluso para actualizaciones de estado parciales, ya que K se define como keyof S entonces Pick<S, K> esencialmente recrea S ?

¿No debería Partial<S> | null hacer el trabajo también?

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

¿Alguien puede explicar ...

Se ha explicado claramente en algunas respuestas.

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

es super restrictivo.

La solución de @Kovensky es la única solución sensata que conozco, pero sigue siendo dolorosa de escribir.

Acabo de encontrar este problema exacto, pero no puedo ver ninguna referencia a Kovensky en el hilo (¿tal vez alguien cambió su nombre de usuario?). ¿Alguien puede indicarme la solución alternativa recomendada actualmente?

@ timrobinson33 Este comentario explica la solución https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -351884578

Y es bueno que ya no tengamos que preocuparnos por esto con los ganchos 🙂

@ timrobinson33 Este comentario explica la solución alternativa # 18365 (comentario)

Muchas gracias por eso. Al final, pensé que mi código se veía mejor con varias llamadas pequeñas a setState con declaraciones If fuera de ellas, aunque esto significa que algunas rutas llamarán a setState más de una vez.

Supongo que esto es similar a cómo trabajamos con los ganchos, viendo el estado como varias cosas pequeñas que actualizamos de forma independiente.

¿Fue útil esta página
0 / 5 - 0 calificaciones
bleepcoder.com utiliza la información de GitHub con licencia pública para proporcionar a los desarrolladores de todo el mundo soluciones a sus problemas. No estamos afiliados a GitHub, Inc. o a ningún desarrollador que use GitHub para sus proyectos. No alojamos ninguno de los vídeos o imágenes en nuestros servidores. Todos los derechos pertenecen a sus respectivos propietarios.
Fuente de esta página: Fuente

Lenguajes de programación populares
Proyectos populares de GitHub
Más proyectos de 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.