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:
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 }
).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
.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.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.
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.
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.
Beispiel:
interface State {
foo: string;
bar: string;
}
setState(prevState => {foo: "hi"}); // OKAY
setState(prevState => {foo: undefined}); // OKAY BUT BAD!
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
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}
.
(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& PartialType '{ 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:
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.
Ich müsste den NULL-Assertion-Operator verwenden, obwohl numberList weder undefiniert noch NULL-fähig ist:
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!
.
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:
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
:
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 :)
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 teilweise
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.
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:
Compilerfehler: