Definitelytyped: Über die Verwendung von Pick für @types/react's setState

Erstellt am 25. Juli 2017  ·  53Kommentare  ·  Quelle: DefinitelyTyped/DefinitelyTyped

Ich verstehe, dass Pick für den Typ von setState da die Rückgabe von undefined für einen Schlüssel, der nicht undefiniert sein sollte, dazu führen würde, dass der Schlüssel von React auf undefiniert gesetzt wird.

Die Verwendung von Pick verursacht jedoch andere Probleme. Zum einen wird die Autovervollständigung des Compiler-Dienstes nutzlos, da er das Ergebnis von Pick für die automatische Vervollständigung verwendet, und wenn Sie Vervollständigungen anfordern, enthält das Ergebnis von Pick noch nicht den Schlüssel, den Sie möglicherweise automatisch vervollständigen möchten. Aber die Probleme sind besonders schlimm, wenn man setState mit einem Callback-Argument schreibt:

  1. Die Liste der Schlüssel wird von Ihrer ersten return Anweisung abgeleitet; Wenn Sie in Ihrer return-Anweisung keinen bestimmten Schlüssel zurückgeben, können Sie ihn auch nicht im Argument lesen, ohne die Liste der Schlüssel auf never . Mehrere Return-Anweisungen können schwer zu schreiben sein, wenn sie unterschiedliche Schlüssel zurückgeben, insbesondere wenn Sie irgendwo eine undefinierte Rückgabe haben (zB if (state.busy) { return } ).

    • Dies kann umgangen werden, indem immer die Eingabe in einem Spread verwendet wird (zB this.setState(input => ({ ...input, count: +input.count + 1 })) ), aber dies ist redundant und eine Deoptimierung, insbesondere für größere Staaten, da setState den Rückgabewert des Callbacks weitergibt zu Object.assign .

  2. Wenn der zurückgegebene Typ aus irgendeinem Grund nicht mit dem Eingabetyp kompatibel ist, wählt never Pick never für seine Schlüssel und die Funktion darf _alles_ zurückgeben. Sogar Schlüssel, die mit einem vorhandenen Schlüssel übereinstimmen, erlauben effektiv any als Wert – wenn er nicht passt, wird er einfach nicht Pick ed und wird als überschüssige Eigenschaft für {} , die nicht überprüft wird.
  3. Wenn never als generisches Argument ausgewählt wird, kann ein Callback-Argument aus einem der oben aufgeführten Gründe tatsächlich als Argument für die Objektform von setState ; dies bewirkt, dass die Argumente des Callbacks any anstelle von {} eingegeben werden. Ich bin mir nicht sicher, warum dies kein impliziter Fehler ist.
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
    }))
  }
}

Zusammenfassend lässt sich sagen, dass die Verwendung von Pick trotz einiger Unannehmlichkeiten dazu beiträgt, Typfehler in der Nicht-Callback-Form von setState , aber in der Callback-Form ist sie völlig kontraproduktiv; wo es nicht nur die beabsichtigte Aufgabe des Verbietens von undefined nicht erfüllt, sondern auch jegliche Typprüfung an den Ein- oder Ausgängen des Callbacks deaktiviert.

Vielleicht sollte es zumindest für das Callback-Formular in Partial geändert werden und hoffen, dass die Benutzer wissen, dass sie keine undefined Werte zurückgeben, wie es in den älteren Definitionen der Fall war.

Hilfreichster Kommentar

Der jüngste "Fix" verursacht bei mir jetzt Probleme mit mehreren Return-Anweisungen innerhalb eines setState() Rückrufs.

Typescript: 2.6.2 mit allen "strict"-Optionen außer "strictFunctionTypes" aktiviert
Typen/Reagieren: 16.0.30

Codebeispiel:

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

Compilerfehler:

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

Alle 53 Kommentare

Vielen Dank für Ihr Feedback. Dies ist ein sehr interessanter Fall, den Sie ansprechen.

Ich muss ein wenig über die Auswirkungen nachdenken, bevor ich mich zu einer Meinung verabschiede.

An einem Punkt wollte @ahejlsberg die Optionalität anders behandeln als | undefined . Somit würde foo?: string bedeuten, dass foo entweder nicht gesetzt ist oder ein String ist.

Beim Lesen eines Eigenschaftswerts ist der Unterschied in 99,9 % der Fälle irrelevant, aber für Schreibvorgänge, insbesondere im Fall von Partial<> , ist die Unterscheidung äußerst wichtig.

Leider ist dies eine bahnbrechende Sprachänderung, also müssen wir auf 3.0 warten oder es hinter einer Flagge haben.

Hätten wir Veränderung gesagt, würde Partial<> für viele äußerst nützlich sein, anstelle meines derzeitigen Dogmas, dessen Verwendung auf den ersten Blick abzulehnen.

@ahejlsberg Ich weiß, du bist ein vielbeschäftigter Mann, wie schwer wäre es, dies umzusetzen? so dass undefined kein implizit zuweisbarer Wert ist?

Okay, nachdem ich heute Morgen einige Zeit damit verbracht habe, über das Problem nachzudenken, sehe ich ein paar "Lösungen" für das von Ihnen vorgeschlagene Problem, jede mit einigen ziemlich starken Nebenwirkungen.

1. Ändern Sie die Optionalität ( interface State { foo?: string } ) in Zeichenfolge oder nicht festgelegt.

Beispiel:

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

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

Dies würde das Problem technisch lösen und es uns ermöglichen, Partial<> , auf Kosten des 98%-Falls, der schwieriger ist. Dies bricht die Welt, so dass es vor 3.x nicht wirklich möglich wäre, und im Allgemeinen kümmern sich nur sehr wenige Bibliotheken / Anwendungsfälle tatsächlich um die Unterscheidung zwischen nicht gesetzt und undefiniert.

2. Wechseln Sie einfach zu partiell

Beispiel:

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

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

3. Nichts tun

Beispiel:

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. Fügen Sie verschachtelte Logik zu Literaltypen hinzu, die verbunden sind

Im Moment, wenn wir mehrere Rückgabepfade haben, macht der Compiler nur alle möglichen Schlüssel zu einem riesigen Pick<State, "foo" | "bar"> .

Eine abwärtskompatible Änderung wäre jedoch die Gruppierung der Literale, wie Pick<State, ("foo") | ("bar")> oder in einem komplexeren Fall: Pick<State, ("foo" | "bar") | ("baz")>

Das Fehlen von Parens deutet darauf hin, dass es sich nur um einen einzigen Satz von Werten handelt, der die vorhandene Funktionalität darstellt.

Wenn wir nun versuchen, {foo: number} in Pick<State, ("foo") | ("bar")> wandeln, können wir erfolgreich sein. Gleiches mit {bar: number} .

Pick<State, ("foo") | ("bar")> auch auf Partial<State> und auf {foo: number} | {bar: number} .

Mein persönliches Fazit

(1) und (2) sind einfach nicht machbar. Das eine führt zu Komplexität für fast jeden, egal was passiert, und das andere hilft den Leuten, Code zu erstellen, der kompiliert, aber eindeutig falsch ist.

(3) ist heute vollständig verwendbar und obwohl ich mich nicht erinnern kann, warum ich | S als möglichen Rückgabewert der Funktion in setState hinzugefügt habe, kann ich mir keinen anderen Grund dafür vorstellen.

(4) könnte die Problemumgehung in (3) umgehen, liegt aber an den TypeScript-Entwicklern, und wenn wir @ahejlsberg interessiert genug bekommen,

Dies lässt mich denken, dass die Typen heute richtiger sind, als wenn wir sie geändert hätten.

Das Problem mit dem "Nichts tun"-Ansatz besteht darin, dass sich der Compiler nicht so verhält, wie Sie es beschreiben, falls Sie tatsächlich einen echten Typfehler machen. Wenn ich Ihr Beispiel ändere, um den Wert beispielsweise auf 0 anstelle einer leeren Zeichenfolge zu setzen:

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

Ich sehe jetzt. Zurück zum Zeichenbrett. Ich werde sehen, ob wir eine clevere Lösung dafür finden können. Wenn wir keine Lösung finden können, müssen wir die Partial<> Ihnen vorgeschlagene undefined für einen Wert häufiger/ärgerlicher wäre als ein unerwarteter falscher Typ.

Zur Verdeutlichung haben wir dafür zwei Überladungen...

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;

Wenn Sie eines oder beide auf Partial<S> anstelle von Pick<S, K> setzen, wird Ihr Problem nicht behoben. Es fällt immer noch auf die Objektversion durch, die aus irgendeinem Grund den falschen Typ akzeptiert:

```ts
Schnittstellenstatus {
Balken: Schnur;
foo: Zahl;
}

Klasse Foo erweitert React.Component<{}, State> {
öffentliches bla () {
this.setState((prevState) => ({bar: 1})); // Fehler nicht :/
}
}```

Könnten wir es irgendwie zwingen, eine Funktion abzulehnen, indem wir ein extends object
Zwang?
Am Freitag, 28. Juli 2017 um 0:00 schrieb Eric Anderson [email protected] :

Zur Verdeutlichung haben wir dafür zwei Überladungen...

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

Setzen Sie eines oder beide auf Partial statt Pickbehebt dein Problem nicht.

Schnittstellenstatus {
Balken: Schnur;
foo: Zahl;
}
Klasse Foo erweitert React.Component<{}, State> {
öffentliches bla () {
this.setState((prevState) => ({bar: 1})); // Fehler nicht :/
}
}```


Sie erhalten dies, weil Sie den Thread verfasst haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-318388471 ,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AAEdfeEJ_Ejfana14fRII1OZuS7qTuyuks5sSKX3gaJpZM4OiDrc
.

Das haben wir versucht. Funktionen sind Objekte.

Eric L Anderson
von meinem Iphone gesendet

Ich bin hierher gekommen, um ein verwandtes Problem zu melden, nämlich dass die Umgestaltung des Zustandstyps mit dieser Änderung unterbrochen wird.

Siehe zum Beispiel https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx – wenn Sie dies in VS Code und F2 öffnen, um something in Zeile 6 zu refaktorieren/umzubenennen (https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx#L6), diese Zeile wird aktualisiert und https://github.com/tomduncalf/react-types-issue/ blob/master/Test.tsx#L14 , vermisst jedoch die Verwendung im setState Aufruf unter https://github.com/tomduncalf/react-types-issue/blob/master/Test.tsx #L20

Der einzige Weg, den ich gefunden habe, um es richtig zu machen, besteht darin, mein Objekt as IState beim Aufrufen von setState explizit

Ich bin mit den Gründen für die Änderung nicht genau vertraut, aber sie scheint die Typsicherheit bei der Arbeit mit dem Zustand ziemlich stark beeinträchtigt zu haben, daher wäre es großartig, wenn es eine Möglichkeit gäbe, das Problem zu lösen!

Vielen Dank,
Tom

Ja, wieder glaube ich, dass es besser wäre, Partial da dies die Beziehungsinformationen besser hält.

Dass Pick nicht korrekt refaktoriert wird, ist wohl eine Einschränkung/ein Fehler des Refactoring-Codes, der verbessert werden könnte, aber ich glaube immer noch nicht, dass Pick die Typsicherheit bietet, die https:// github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318385945 Erwähnungen. Während Partial undefined zulassen würde, wo undefined nicht sein sollte, und der Benutzer muss dann vorsichtig sein, dies nicht zu tun, Pick erlaubt alles, weil jeder Typ, der nicht der Schnittstelle entspricht, würde anstelle eines Typfehlers Pick dazu führen, stattdessen eine leere Schnittstelle zu generieren, die alles akzeptiert.

Was https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -318388471 betrifft, könnte dies ein Fehler in der Prüfung auf überschüssige Eigenschaften sein? Oder wurde dies getestet, bevor die Prüfung auf überschüssige Eigenschaften in 2.3 oder 2.4 strenger wurde (ich habe es vergessen)?

Ein weiteres Problem, das mir hier gerade aufgefallen ist, ist, dass Typescript immer noch zulässt, wenn eine Komponente entweder keinen Zustandstyp hat (dh nur einen Typparameter für React.Component ) oder der Zustandstyp leer ist ( {} ). ruft setState ohne einen Fehler auszulösen, was mir falsch erscheint – siehe https://github.com/tomduncalf/react-types-issue/blob/master/Test2.tsx

Die Lösung mit Partial klingt für mich vorzuziehen – ich könnte versuchen, es selbst zu patchen und zu sehen, wie es funktioniert.

Vielen Dank,
Tom

Hallo zusammen!
Ich weiß nicht warum, aber wenn ich setState Deklarationen zu einer einzelnen Deklaration verbinde:

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

Bei mir funktioniert es wie erwartet:

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

Kann sich dies genug ändern, um das ursprüngliche Problem zu beheben?

@mctep Schöner Fund.

Und wenn ich nehme, was Sie getan haben, und es ein wenig erweitere, um an manchen Stellen Partial<S> & Pick<S, K> statt Pick<S, K> zu sein, schlägt Intellisense Schlüsselnamen vor. Leider sagen die Schlüsselnamen, dass der Eigenschaftswert "etwas | undefiniert" ist, aber wenn Sie es kompilieren, weiß es es besser:

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

Ich werde diese Änderung heute später vornehmen

Der jüngste "Fix" verursacht bei mir jetzt Probleme mit mehreren Return-Anweisungen innerhalb eines setState() Rückrufs.

Typescript: 2.6.2 mit allen "strict"-Optionen außer "strictFunctionTypes" aktiviert
Typen/Reagieren: 16.0.30

Codebeispiel:

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

Compilerfehler:

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

Sehen Sie auch das von @UselessPickles beschriebene

Ich bin mir ziemlich sicher, dass das immer ein Problem war.

Ich bin mir mehr als ziemlich sicher, dass es nicht immer ein Problem war. Ich habe ein Projekt mit mehreren Rückgabeanweisungen in setState() Rückrufen, das seit mehr als 2 Monaten ohne Probleme kompiliert wird. Ich habe alle 2 Wochen oder so mit NPM-Abhängigkeits-Upgrades Schritt gehalten, und dieser Compiler-Fehler trat bei mir heute erst auf, nachdem ich auf die neueste Version von Typen/Reagieren aktualisiert hatte. Die vorherige Version hat diesen Compilerfehler nicht erzeugt.

Es ist ein Problem seit dem Wechsel von Partial zu Pick. Die Problemumgehung wäre, die Liste der Schlüssel, die Sie zurückgeben möchten, an den generischen Parameter von setState , aber dann sind Sie gezwungen, immer alle Schlüssel zurückzugeben ...

In diesen Fällen gebe ich entweder immer alle Schlüssel zurück, wobei die nicht geänderten Schlüssel auf key: prevState.key , oder indem ich einen Spread mit prevState ( { ...prevState, newKey: newValue } ) zurückgebe.

Vielleicht habe ich tatsächlich einen spezifischeren Randfall in meinem Code, der zuvor zufällig funktioniert hat? Ein tatsächliches Beispiel aus meinem Projekt sieht eher so aus, wo es entweder ein leeres Objekt zurückgibt (um keinen Zustand zu ändern) oder ein nicht leeres Objekt zurückgibt:

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 und return undefined sind auch vollkommen akzeptable Rückgabewerte, um den Status nicht zu ändern (sie durchlaufen Object.assign und nehmen daher keine Änderungen an this.state ).

Erinnert mich daran, dass die aktuelle Signatur keines von beiden zulässt. Vielleicht sollte null zumindest als Rückgabewert erlaubt sein, da dies nicht durch versehentliches Vergessen von return .


Wie auch immer, in Fällen, in denen die Signatur mehrdeutig ist, scheint TypeScript nur die erste return Anweisung in der Quellreihenfolge auszuwählen und daraus die generischen Parameter abzuleiten. Es scheint in der Lage zu sein, die Typen für einfache Generika (zB Array oder Promise ) zusammenzuführen, aber es führt sie nie zusammen, wenn der kontextbezogene Typ ein gemappter Typ wie Pick .

Ich sehe einige Regressionen mit der Nicht-Funktionsversion mit den neuesten Typen. Insbesondere hat sich der Argumenttyp "state" bei der Übergabe eines Arguments geändert von:

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

zu:

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

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

Das Hinzufügen von & Partial<S> scheint Dinge zu zerstören, die zuvor funktioniert haben.

Hier ist eine minimale Repro:

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

dies schlägt mit Fehler fehl:

(429,18): Argument vom Typ '{ baseProp: "foobar"; }' ist dem Parameter vom Typ '((prevState: Readonly Typ '{ baseProp: "foobar"; }' nicht zuweisbar dem Typ 'Pick & Partial Type '{ baseProp: "foobar"; }' ist nicht zuweisbar für geben Sie 'Teilweise' ein

Wenn Sie den Zustandstyp von S &{ baseProp: string } auf nur { baseProp: string } ändern, wird der Fehler ebenfalls behoben (obwohl die tatsächlichen Klassen, die den S-Typ angeben, beschädigt werden).

Das ist interessant. Ich freue mich, den Teil der Änderung rückgängig zu machen und einen Teil der Änderung auszuwählen

Ich werde sagen, dass das, was Sie melden, sich wie ein Fehler in TS anhört, insbesondere:

Geben Sie '{ baseProp: "foobar" ein; }' ist nicht dem Typ 'Partial zuweisbar

In Ordung. Vielleicht kann TS nicht wissen, was los ist, weil keine Beziehung zwischen S und baseProp besteht.

Man könnte ein S vom Typ { baseProp:number } übergeben. In diesem Fall ist es richtig, dass Sie baseProp keinen String zuweisen können.

Vielleicht, wenn S die baseProp-Version erweitert?

Ich hatte sowas schon mal probiert:

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

während der Zugriff in Ordnung ist, hat der setState-Aufruf den gleichen Fehler:

Argument vom Typ '{ baseProp: "foobar"; }' ist dem Parameter vom Typ '((prevState: Readonly\ , props: P) => Pick & Partial\ ) |
}' ist nicht dem Typ 'Pick & Partial\ ' zuweisbar Geben Sie '{ baseProp: "foobar" ein;

Es ist wahrscheinlich keine gute Möglichkeit, es zu strukturieren - schließlich könnten einige untergeordnete Klassen Zustandsvariablen deklarieren, die mit Variablen der Basisklasse kollidiert sind. Typoskript könnte also richtig sein, sich zu beschweren, dass es nicht sicher sein kann, was dort passieren wird. Es ist seltsam, dass es kein Problem mit dem Zugriff auf diese Requisiten hat.

Hmm. Das fühlt sich jetzt definitiv wie ein Fehler in TS an.

Ich werde heute damit spielen und eventuell das & Partial rausnehmen.

Ich werde dies auch als Testfall hinzufügen und eine andere Idee ausprobieren, um Intellisense glücklich zu machen

Hi,
Selbes Problem hier...

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

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

Der Fehler trat erst nach dieser Änderung auf. In Version 16.0.10 hat es gut funktioniert.

Typescript hat mehrere verwandte Probleme, sie haben sie so geschlossen, dass sie wie geplant funktionieren:

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

Ich habe hier einige Beispiele zusammengefasst: https://gist.github.com/afarnsworth-valve/93d1096d1410b0f2efb2c94f86de9c84

Obwohl das Verhalten immer noch seltsam erscheint. Insbesondere können Sie eine Variable, die als Basisklasse typisiert ist, ohne Umwandlung zuweisen und dann problemlos die gleichen Aufrufe damit durchführen. Dieses Problem scheint darauf hinzudeuten, dass Zuordnung und Vergleich zwei verschiedene Dinge sind.

Hab euch nicht vergessen. Wird bald behoben

Die referenzierte PR sollte dieses Problem lösen. Ich habe auch Tests hinzugefügt, die davor schützen sollen, dieses Edge-Case erneut zu brechen.

@ericanderson Danke für die schnelle Antwort :)

Hat der neue Fix irgendwelche Einschränkungen/Nachteile?

Keine die mir derzeit bekannt sind. Ich konnte Intellisense beibehalten (eigentlich besser als zuvor, da es einige Randfälle löst).

Um das auszuarbeiten, die & Partial<S> Lösung bestand darin, Intellisense auszutricksen, um mögliche Parameter aufzudecken, aber es tat dies, indem sie angab, dass sie X | undefined . Dies würde natürlich nicht kompilieren, aber es war ein bisschen verwirrend. Das Abrufen von | S behebt den Intellisense, so dass er alle richtigen Parameter vorschlägt und jetzt keinen falschen Typ anzeigt.

Ich habe versucht, null als möglichen Rückgabewert aus dem setState Rückruf hinzuzufügen (was Sie in den Zustand zurückgeben können, den Sie nicht ändern möchten, ohne dass return; auch gültig), aber das führte stattdessen dazu, dass die Typinferenz einfach komplett aufgegeben wurde und never als Schlüssel ausgewählt wurde 😢

Im Moment habe ich in den Rückrufen, bei denen ich die Aktualisierung des Status überspringen möchte, return null! . Der never Typ dieser Rückgabe bewirkt, dass TypeScript die Rückgabe für den generischen Typrückschluss ignoriert.

Hallo Leute...

Der letzte Commit hat mein Problem behoben.
Danke für die schnelle Antwort :)

Welche Version hat den Fix? Wird es auf npm veröffentlicht? Ich treffe auf den Fall, in dem die Rückrufversion von setState mir mitteilt, dass der Rückgabewert nicht übereinstimmt, genau wie @UselessPickles gefunden wurde.

Sollte auf den neuesten @types/react gut sein

Ich habe es für die 15er und 16er Serie behoben

import produce from 'immer';

interface IComponentState
{
    numberList: number[];
}

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

Die alte Typdefinition des 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;

..produziert diesen Fehler:

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

Ich habe diesen Vorschlag ausprobiert:

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 kann nun das Eigentum aus dem Zustand von React erkennen. Die erfasste Eigenschaft wird jedoch jetzt als possibly undefined wahrgenommen.

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

Ich müsste den NULL-Assertion-Operator verwenden, obwohl numberList weder undefiniert noch NULL-fähig ist:

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

Ich bleibe einfach bei der alten Typdefinition, bis die Typerkennung verbessert ist. In der Zwischenzeit werde ich nur den Typ des generischen Parameters von immer produzieren explizit angeben. produce<IComponentState> ist einfacher zu begründen als list! .

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

Ihr erster Fehler ist, dass Sie nichts zurückgeben. So funktioniert setState nicht.

Es funktioniert auch, wenn keine Variable in product zurückgegeben wird. Ich bin einfach dem Beispiel hier gefolgt (Nicht-TypeScript):

https://github.com/mweststrate/immer

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

Das einzige, was TypeScript daran hindert, diesen Code auszuführen, ist der falsch abgeleitete Typ aus der React-Typdefinition. Die Typdefinition meldet einen Fehler von numberList does not exist on type Pick<IComponentState, never> . Der Kompilierungsfehler kann einfach behoben werden, indem der Typ explizit an den generischen Parameter von product übergeben wird, dh produce<IComponentState> .

Ich habe sogar versucht, die Variable in product zurückzugeben und zu sehen, ob dies der React-Typdefinition helfen würde, den Typ abzuleiten (allerdings ein Küken-und-Ei-Problem), aber immer noch gibt es für die React-Typdefinition keine Möglichkeit, den richtigen Typ des Zustands zu erkennen. Daher erscheint der Intellisense für Draft nicht:

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

Oder vielleicht habe ich eine falsche Erwartung vom Compiler :) Der Compiler kann keinen Typ für die Entwurfsvariable basierend auf dem Typ von setState erstellen, da der Compiler den Code von innen nach außen verarbeitet. Die vorgeschlagene Typdefinition ließ mich jedoch irgendwie glauben, dass der Compiler den Code von außen nach innen verarbeiten kann, dass er den besten Typ auswählen kann, den er vom äußeren Code ( setState ) an den inneren Code ( 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;

Mit der obigen Typdefinition kann der Compiler erkennen, dass Draft eine numberList Eigenschaft hat. Es erkennt es jedoch als possibly undefined :

image

Nach weiteren Basteleien habe ich den Compiler in die Lage versetzt, den Typ des Zustands an den Entwurf von Produzieren zu übergeben, indem ich S zur Typdefinition von setState hinzugefügt habe:

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;

Der Code wird jetzt kompiliert :)

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

Ihr Fehler liegt nicht bei setState, Ihr Fehler liegt in product. Was ist Ihre Typdefinition für Produkte?

Meine obige PR hat einen Fehler im Testskript von DefinitelyTyped, ich habe es nicht lokal getestet. Also teste ich es jetzt lokal.

Hier ist die Definition des Immer/Produzieren-Typs.

/**
 * 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 könnten Sie mich bitte auf die Diskussion darüber hinweisen, warum Pick anstelle von Partial ? Dies hat mir stundenlangen Kummer bereitet (mit einfachem setState(obj) , nicht der Rückrufversion), und im Moment gehe ich mit this.setState(newState as State) als Workaround vor. Ich möchte nur verstehen, warum es geändert wurde, weil mir etwas fehlt.

Hallo @ericanderson ,

Ich habe ein Problem mit der neuesten Definition.

Mein Anwendungsfall sieht kurz wie folgt aus:

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
}

Die Fehlermeldung lautet wie folgt:

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

Es scheint, dass Partial<AppState> nicht mit der Signatur von setState kompatibel ist. Natürlich kann ich dies durch Typ-Assertion lösen wie

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

aber es ist nicht ideal, denn:
1) diese Syntax ist sehr ausführlich
2) Noch wichtiger ist, dass die Typzusicherung meine tatsächlichen Daten verletzen könnte. newState as Pick<AppState, 'somethingElse'> auch die Prüfung, obwohl sie nicht zu meinen Daten passt.

Ich denke teilweisesollte irgendwie mit Pick kompatibel sein, da teilweisewählt nur eine unsichere Anzahl von Schlüsseln von T. Ich bin mir nicht sicher, ob mein Verständnis richtig ist. Wie auch immer, die ideale Verwendung aus meiner Sicht sollte sein, dass ich die Variable mit dem Typ Partial übergeben kanndirekt in setState.

Könnten Sie freundlicherweise meinen Vorschlag berücksichtigen oder auf mein Missverständnis hinweisen, falls vorhanden? Dankeschön!

Dies ist zunächst einmal eine wirklich alte Änderung. Wenn dies nicht kürzlich von jemand anderem geändert wurde, bellen Sie wahrscheinlich den falschen Baum an.

Das gesagt. Teilweise erlaubt undefinierte Werte.

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

a ist gültig, aber das Ergebnis dieser Aktualisierung Ihres Bundesstaates führt dazu, dass Ihr Bundesstaat mit foo undefiniert ist, obwohl Sie dies für unmöglich erklärt haben.

Daher ist ein Partial nicht einem Pick zuordenbar. Und Pick ist die richtige Antwort, um sicherzustellen, dass Ihre Typen nicht lügen

Ich habe das Gefühl, dass es nicht erlaubt ist:

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

ist super restriktiv.

Der Workaround von

Gibt es etwas, das getan werden kann, um dieses (ich würde sagen) ziemlich häufige Muster zu unterstützen?

Das einzige, was getan werden kann, ist die Typsicherheit zu entfernen

Kann jemand die Gründe für Pick<S, K> | S | null erklären?

        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 ich weiß nicht einmal, warum die obige Signatur sogar für teilweise Statusaktualisierungen funktioniert, da K als keyof S also Pick<S, K> Wesentlichen S ?

sollte Partial<S> | null den Job nicht auch machen?

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

Kann jemand erklären...

Es wurde ein paar Antworten klar erklärt.

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

ist super restriktiv.

Der Workaround von

Ich bin gerade auf genau dieses Problem gestoßen, kann aber im Thread keinen Hinweis auf Kovensky sehen (vielleicht hat jemand seinen Benutzernamen geändert?). Kann mir jemand den aktuell empfohlenen Workaround nennen?

@timrobinson33 Dieser Kommentar erklärt die Problemumgehung https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment -351884578

Und gut, dass wir uns darüber mit Haken keine Gedanken mehr machen müssen 🙂

@timrobinson33 Dieser Kommentar erklärt die Problemumgehung #18365 (Kommentar)

Vielen Dank dafür. Am Ende fand ich, dass mein Code schöner aussah als mehrere kleine setState-Aufrufe mit If Anweisungen außerhalb davon, obwohl dies bedeutet, dass einige Pfade setState mehr als einmal aufrufen.

Ich denke, dies ist tatsächlich ähnlich wie wir mit Hooks arbeiten, da wir den Zustand als mehrere kleine Dinge betrachten, die wir unabhängig voneinander aktualisieren.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen
bleepcoder.com verwendet öffentlich lizenzierte GitHub-Informationen, um Entwicklern auf der ganzen Welt Lösungen für ihre Probleme anzubieten. Wir sind weder mit GitHub, Inc. noch mit anderen Entwicklern affiliiert, die GitHub für ihre Projekte verwenden. Wir hosten keine der Videos oder Bilder auf unseren Servern. Alle Rechte gehören ihren jeweiligen Eigentümern.
Quelle für diese Seite: Quelle

Beliebte Programmiersprachen
Beliebte GitHub Projekte
Mehr GitHub Projekte

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