Typescript: Genaue Typen

Erstellt am 15. Dez. 2016  ·  171Kommentare  ·  Quelle: microsoft/TypeScript

Dies ist ein Vorschlag, um eine Syntax für genaue Typen zu ermöglichen. Ein ähnliches Feature ist in Flow zu sehen (https://flowtype.org/docs/objects.html#exact-object-types), aber ich möchte es als Feature vorschlagen, das für Typliterale und nicht für Schnittstellen verwendet wird. Die spezifische Syntax, die ich vorschlagen würde, ist die Pipe (die fast die Flow-Implementierung widerspiegelt, aber die type-Anweisung umgeben sollte), da sie als mathematische absolute Syntax bekannt ist.

interface User {
  username: string
  email: string
}

const user1: User = { username: 'x', email: 'y', foo: 'z' } //=> Currently errors when `foo` is unknown.
const user2: Exact<User> = { username: 'x', email: 'y', foo: 'z' } //=> Still errors with `foo` unknown.

// Primary use-case is when you're creating a new type from expressions and you'd like the
// language to support you in ensuring no new properties are accidentally being added.
// Especially useful when the assigned together types may come from other parts of the application 
// and the result may be stored somewhere where extra fields are not useful.

const user3: User = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Does not currently error.
const user4: Exact<User> = Object.assign({ username: 'x' }, { email: 'y', foo: 'z' }) //=> Will error as `foo` is unknown.

Diese Syntaxänderung wäre eine neue Funktion und würde sich auf neue Definitionsdateien auswirken, die geschrieben werden, wenn sie als Parameter oder offengelegter Typ verwendet werden. Diese Syntax könnte mit anderen komplexeren Typen kombiniert werden.

type Foo = Exact<X> | Exact<Y>

type Bar = Exact<{ username: string }>

function insertIntoDb (user: Exact<User>) {}

Entschuldigung im Voraus, wenn dies ein Duplikat ist, ich konnte anscheinend nicht die richtigen Schlüsselwörter finden, um Duplikate dieser Funktion zu finden.

Bearbeiten: Dieser Beitrag wurde aktualisiert, um den unter https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -267272371 erwähnten bevorzugten Syntaxvorschlag zu verwenden, der die Verwendung einer einfacheren Syntax mit einem generischen Typ umfasst, um die Verwendung in Ausdrücken zu ermöglichen.

Awaiting More Feedback Suggestion

Hilfreichster Kommentar

Darüber haben wir eine ganze Weile gesprochen. Ich versuche, die Diskussion zusammenzufassen.

Übermäßige Eigentumsprüfung

Exakte Typen sind nur eine Möglichkeit, zusätzliche Eigenschaften zu erkennen. Die Nachfrage nach exakten Typen ging stark zurück, als wir zunächst Exzess-Property-Checking (EPC) implementierten. EPC war wahrscheinlich die größte Breaking Change, die wir vorgenommen haben, aber es hat sich ausgezahlt; Fast sofort bekamen wir Fehler, wenn EPC keine überschüssige Eigenschaft entdeckte.

In den meisten Fällen, in denen die Leute genaue Typen wünschen, würden wir dies lieber beheben, indem wir EPC intelligenter machen. Ein Schlüsselbereich hier ist, wenn der Zieltyp ein Union-Typ ist - wir wollen dies nur als Bugfix betrachten (EPC sollte hier funktionieren, ist aber noch nicht implementiert).

Alle optionalen Typen

Mit EPC verwandt ist das Problem der rein optionalen Typen (die ich "schwache" Typen nenne). Höchstwahrscheinlich möchten alle schwachen Typen genau sein. Wir sollten nur die Erkennung schwacher Typen implementieren (#7485 / #3842); der einzige Blocker hier sind Kreuzungstypen, die eine gewisse zusätzliche Komplexität bei der Implementierung erfordern.

Wessen Typ ist genau?

Das erste große Problem, das wir bei exakten Typen sehen, ist, dass es wirklich unklar ist, welche Typen als exakt markiert werden sollen.

An einem Ende des Spektrums haben Sie Funktionen, die buchstäblich eine Ausnahme auslösen (oder anderweitig schlechte Dinge tun), wenn ein Objekt mit einem eigenen Schlüssel außerhalb einer festen Domäne übergeben wird. Diese sind dünn gesät (ich kann kein Beispiel aus dem Gedächtnis nennen). In der Mitte gibt es Funktionen, die stillschweigend ignorieren
unbekannte Eigenschaften (fast alle). Und am anderen Ende haben Sie Funktionen, die generisch über alle Eigenschaften operieren (zB Object.keys ).

Offensichtlich sollten die Funktionen "wird ausgelöst, wenn zusätzliche Daten angegeben werden" so gekennzeichnet sein, dass sie genaue Typen akzeptieren. Aber was ist mit der Mitte? Die Leute werden wahrscheinlich anderer Meinung sein. Point2D / Point3D ist ein gutes Beispiel - man könnte vernünftigerweise sagen, dass eine magnitude Funktion den Typ (p: exact Point2D) => number , um zu verhindern, dass ein Point3D . Aber warum kann ich mein { x: 3, y: 14, units: 'meters' } Objekt nicht an diese Funktion übergeben? Hier kommt EPC ins Spiel - Sie möchten diese "zusätzliche" units Eigenschaft an Orten erkennen, an denen sie definitiv verworfen wird, aber nicht wirklich Aufrufe blockieren, die Aliasing beinhalten.

Verstöße gegen Annahmen / Instanziierungsprobleme

Wir haben einige grundlegende Grundsätze, die genaue Typen ungültig machen würden. Es wird beispielsweise angenommen, dass ein Typ T & U immer T zuweisbar ist, aber dies schlägt fehl, wenn T ein exakter Typ ist. Dies ist problematisch, da Sie möglicherweise eine generische Funktion haben, die dieses T & U -> T Prinzip verwendet, die Funktion jedoch mit T mit einem genauen Typ instanziiert aufrufen. Es gibt also keine Möglichkeit, diesen Sound zu erzeugen (es ist wirklich nicht in Ordnung, bei der Instanziierung Fehler zu machen) - nicht unbedingt ein Blocker, aber es ist verwirrend, dass eine generische Funktion freizügiger ist als eine manuell instanziierte Version von sich selbst!

Es wird auch angenommen, dass T immer T | U zuweisbar ist, aber es ist nicht offensichtlich, wie diese Regel angewendet wird, wenn U ein exakter Typ ist. Kann { s: "hello", n: 3 } { s: string } | Exact<{ n: number }> zugewiesen werden? "Ja" scheint die falsche Antwort zu sein, denn wer auch immer nach n sucht und es findet, wird nicht glücklich sein, s , aber "Nein" scheint auch falsch zu sein, weil wir die grundlegenden T -> T | U verletzt haben

Sonstiges

Was bedeutet function f<T extends Exact<{ n: number }>(p: T) ? :verwirrt:

Oft werden exakte Typen gewünscht, bei denen man wirklich eine "auto-disjunkte" Union will. Mit anderen Worten, Sie haben möglicherweise eine API, die { type: "name", firstName: "bob", lastName: "bobson" } oder { type: "age", years: 32 } akzeptieren kann, aber { type: "age", years: 32, firstName: 'bob" } nicht akzeptieren möchte, da etwas Unvorhersehbares passieren wird. Der "richtige" Typ ist wohl { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } aber gut, das ist nervig zu tippen. Wir könnten möglicherweise über Zucker nachdenken, um solche Typen zu erstellen.

Zusammenfassung: Benötigte Anwendungsfälle

Unsere hoffnungsvolle Diagnose lautet, dass dies, abgesehen von den relativ wenigen wirklich geschlossenen APIs, eine XY-Problemlösung ist. Wo immer möglich, sollten wir EPC verwenden, um "schlechte" Eigenschaften zu erkennen. Wenn Sie also ein Problem haben und Sie denken, dass genaue Typen die richtige Lösung sind, beschreiben Sie bitte hier das ursprüngliche Problem, damit wir einen Katalog von Mustern zusammenstellen und sehen können, ob es andere Lösungen gibt, die weniger invasiv/verwirrend sind.

Alle 171 Kommentare

Ich würde vorschlagen, dass die Syntax hier streitbar ist. Da TypeScript jetzt führende Pipe für Union-Typen zulässt.

class B {}

type A = | number | 
B

Wird jetzt kompiliert und entspricht type A = number | B , dank automatischer Semikolon-Einfügung.

Ich denke, dies erwarte ich möglicherweise nicht, wenn der genaue Typ eingeführt wird.

Ich bin mir nicht sicher, ob es zutrifft, aber zu Ihrer Information https://github.com/Microsoft/TypeScript/issues/7481

Wenn die Syntax {| ... |} übernommen würde, könnten wir auf zugeordneten Typen aufbauen, damit Sie schreiben können

type Exact<T> = {|
    [P in keyof T]: P[T]
|}

und dann könntest du Exact<User> schreiben.

Dies ist wahrscheinlich das Letzte, was ich bei Flow vermisse, verglichen mit TypeScript.

Das Beispiel Object.assign ist besonders gut. Ich verstehe, warum sich TypeScript heute so verhält, aber meistens hätte ich lieber den genauen Typ.

@HerringtonDarkholme Danke. In meinem ersten Problem wurde das erwähnt, aber ich habe es am Ende weggelassen, da jemand sowieso eine bessere Syntax hätte, wie sich herausstellte

@DanielRosenwasser Das sieht viel vernünftiger aus, danke!

@wallverb Ich glaube nicht, obwohl ich auch gerne sehen würde, dass es diese Funktion gibt

Was ist, wenn ich eine Vereinigung von Typen ausdrücken möchte, von denen einige exakt sind und andere nicht? Die vorgeschlagene Syntax würde es fehleranfällig und schwer zu lesen machen, selbst wenn dem Abstand besondere Aufmerksamkeit geschenkt wird:

|Type1| | |Type2| | Type3 | |Type4| | Type5 | |Type6|

Können Sie schnell erkennen, welche Gewerkschaftsmitglieder nicht genau sind?

Und ohne den sorgfältigen Abstand?

|Type1|||Type2||Type3||Type4||Type5||Type6|

(Antwort: Type3 , Type5 )

@rotemdan Siehe die obige Antwort, stattdessen gibt es den generischen Typ Extact der ein soliderer Vorschlag ist als meiner. Ich denke, dies ist der bevorzugte Ansatz.

Es gibt auch Bedenken, wie es in Editor-Hinweisen, Vorschau-Popups und Compiler-Meldungen aussehen würde. Typaliase werden derzeit nur auf unformatierte Typausdrücke "abgeflacht". Der Alias ​​wird nicht beibehalten, so dass die unverständlichen Ausdrücke immer noch im Editor erscheinen würden, es sei denn, es werden spezielle Maßnahmen ergriffen, um dem entgegenzuwirken.

Ich finde es schwer zu glauben, dass diese Syntax in einer Programmiersprache wie Flow akzeptiert wurde, die Unions mit der gleichen Syntax wie Typescript hat. Für mich erscheint es nicht ratsam, eine fehlerhafte Syntax einzuführen, die grundlegend mit der bestehenden Syntax in Konflikt steht, und dann sehr zu versuchen, sie zu "verdecken".

Eine interessante (amüsante?) Alternative ist die Verwendung eines Modifikators wie only . Ich hatte vor einigen Monaten einen Entwurf für einen Vorschlag dafür, glaube ich, aber ich habe ihn nie eingereicht:

function test(a: only string, b: only User) {};

Das war die beste Syntax, die ich damals finden konnte.

_Edit_: just könnte auch funktionieren?

function test(a: just string, b: just User) {};

_(Bearbeiten: Jetzt, wo ich mich erinnere, war die Syntax ursprünglich für einen Modifikator für nominale Typen gedacht, aber ich denke, das spielt keine Rolle. Die beiden Konzepte liegen nahe genug beieinander, sodass diese Schlüsselwörter auch hier funktionieren könnten)_

Ich habe mich gefragt, vielleicht könnten beide Keywords eingeführt werden, um zwei leicht unterschiedliche Arten von Übereinstimmungen zu beschreiben:

  • just T (bedeutet: "genau T ") für den genauen strukturellen Abgleich, wie hier beschrieben.
  • only T (bedeutet: "eindeutig T ") für nominales Matching.

Nominales Matching könnte als eine noch "strengere" Version des exakten strukturellen Matchings angesehen werden. Dies würde bedeuten, dass nicht nur der Typ strukturell identisch sein muss, sondern der Wert selbst mit genau demselben Typbezeichner wie angegeben verknüpft sein muss. Dies kann Typaliase zusätzlich zu Schnittstellen und Klassen unterstützen oder nicht.

Ich persönlich glaube nicht, dass der subtile Unterschied so viel Verwirrung stiften würde, obwohl ich denke, dass es Sache des Typescript-Teams ist, zu entscheiden, ob das Konzept eines nominalen Modifikators wie only für sie angemessen erscheint. Ich schlage dies nur als Option vor.

_(Bearbeiten: nur eine Anmerkung zu only bei Verwendung mit Klassen: Es gibt hier eine Mehrdeutigkeit, ob es nominale Unterklassen erlaubt, wenn auf eine Basisklasse verwiesen wird - das muss wohl separat besprochen werden in geringerem Maße - das gleiche könnte für Schnittstellen in Betracht gezogen werden - obwohl ich das derzeit nicht für so nützlich halte)_

Dies scheint eine Art von getarnten Subtraktionstypen zu sein. Diese Probleme könnten relevant sein: https://github.com/Microsoft/TypeScript/issues/4183 https://github.com/Microsoft/TypeScript/issues/7993

@ethanresnick Warum glaubst du das?

Dies wäre in der Codebasis, an der ich gerade arbeite, äußerst nützlich. Wenn dies bereits Teil der Sprache wäre, hätte ich heute nicht damit verbracht, einen Fehler aufzuspüren.

(Vielleicht andere Fehler, aber nicht dieser spezielle Fehler 😉)

Ich mag die von Flow inspirierte Pipe-Syntax nicht. Etwas wie das Schlüsselwort exact hinter Schnittstellen wäre einfacher zu lesen.

exact interface Foo {}

@mohsen1 Ich bin mir sicher, dass die meisten Leute den generischen Typ Exact in Ausdruckspositionen verwenden würden, daher sollte es nicht allzu wichtig sein. Ich würde mich jedoch mit einem solchen Vorschlag befassen, da Sie möglicherweise den linken Teil des Interface-Schlüsselworts vorzeitig überladen, das zuvor nur für Exporte reserviert war (in Übereinstimmung mit JavaScript-Werten - z. B. export const foo = {} ). Es zeigt auch an, dass dieses Schlüsselwort möglicherweise auch für Typen verfügbar ist (zB exact type Foo = {} und jetzt export exact interface Foo {} ).

Wie würde extends mit {| |} Syntax funktionieren? wird interface Bar extends Foo {| |} genau sein, wenn Foo nicht genau ist?

Ich denke, das Schlüsselwort exact macht es einfach zu erkennen, ob eine Schnittstelle genau ist. Es kann (sollte?) auch für type funktionieren.

interface Foo {}
type Bar = exact Foo

Äußerst hilfreich für Dinge, die über Datenbanken oder Netzwerkaufrufe an Datenbanken oder SDKs wie AWS SDK funktionieren, die Objekte mit allen optionalen Eigenschaften annehmen, da zusätzliche Daten stillschweigend ignoriert werden und zu schwer bis sehr schwer zu findenden Fehlern führen können :rose:

@mohsen1 Diese Frage scheint für die Syntax irrelevant zu sein, da dieselbe Frage immer noch mit dem Schlüsselwortansatz existiert. Persönlich habe ich keine bevorzugte Antwort und müsste mit bestehenden Erwartungen spielen, um sie zu beantworten - aber meine erste Reaktion ist, dass es egal sein sollte, ob Foo genau ist oder nicht.

Die Verwendung eines exact Schlüsselworts scheint mehrdeutig - Sie sagen, es kann wie exact interface Foo {} oder type Foo = exact {} ? Was bedeutet exact Foo | Bar ? Die Verwendung des generischen Ansatzes und das Arbeiten mit bestehenden Mustern bedeutet, dass keine Neuerfindung oder kein Lernen erforderlich ist. Es ist nur interface Foo {||} (das ist die einzige neue Sache hier), dann type Foo = Exact<{}> und Exact<Foo> | Bar .

Darüber haben wir eine ganze Weile gesprochen. Ich versuche, die Diskussion zusammenzufassen.

Übermäßige Eigentumsprüfung

Exakte Typen sind nur eine Möglichkeit, zusätzliche Eigenschaften zu erkennen. Die Nachfrage nach exakten Typen ging stark zurück, als wir zunächst Exzess-Property-Checking (EPC) implementierten. EPC war wahrscheinlich die größte Breaking Change, die wir vorgenommen haben, aber es hat sich ausgezahlt; Fast sofort bekamen wir Fehler, wenn EPC keine überschüssige Eigenschaft entdeckte.

In den meisten Fällen, in denen die Leute genaue Typen wünschen, würden wir dies lieber beheben, indem wir EPC intelligenter machen. Ein Schlüsselbereich hier ist, wenn der Zieltyp ein Union-Typ ist - wir wollen dies nur als Bugfix betrachten (EPC sollte hier funktionieren, ist aber noch nicht implementiert).

Alle optionalen Typen

Mit EPC verwandt ist das Problem der rein optionalen Typen (die ich "schwache" Typen nenne). Höchstwahrscheinlich möchten alle schwachen Typen genau sein. Wir sollten nur die Erkennung schwacher Typen implementieren (#7485 / #3842); der einzige Blocker hier sind Kreuzungstypen, die eine gewisse zusätzliche Komplexität bei der Implementierung erfordern.

Wessen Typ ist genau?

Das erste große Problem, das wir bei exakten Typen sehen, ist, dass es wirklich unklar ist, welche Typen als exakt markiert werden sollen.

An einem Ende des Spektrums haben Sie Funktionen, die buchstäblich eine Ausnahme auslösen (oder anderweitig schlechte Dinge tun), wenn ein Objekt mit einem eigenen Schlüssel außerhalb einer festen Domäne übergeben wird. Diese sind dünn gesät (ich kann kein Beispiel aus dem Gedächtnis nennen). In der Mitte gibt es Funktionen, die stillschweigend ignorieren
unbekannte Eigenschaften (fast alle). Und am anderen Ende haben Sie Funktionen, die generisch über alle Eigenschaften operieren (zB Object.keys ).

Offensichtlich sollten die Funktionen "wird ausgelöst, wenn zusätzliche Daten angegeben werden" so gekennzeichnet sein, dass sie genaue Typen akzeptieren. Aber was ist mit der Mitte? Die Leute werden wahrscheinlich anderer Meinung sein. Point2D / Point3D ist ein gutes Beispiel - man könnte vernünftigerweise sagen, dass eine magnitude Funktion den Typ (p: exact Point2D) => number , um zu verhindern, dass ein Point3D . Aber warum kann ich mein { x: 3, y: 14, units: 'meters' } Objekt nicht an diese Funktion übergeben? Hier kommt EPC ins Spiel - Sie möchten diese "zusätzliche" units Eigenschaft an Orten erkennen, an denen sie definitiv verworfen wird, aber nicht wirklich Aufrufe blockieren, die Aliasing beinhalten.

Verstöße gegen Annahmen / Instanziierungsprobleme

Wir haben einige grundlegende Grundsätze, die genaue Typen ungültig machen würden. Es wird beispielsweise angenommen, dass ein Typ T & U immer T zuweisbar ist, aber dies schlägt fehl, wenn T ein exakter Typ ist. Dies ist problematisch, da Sie möglicherweise eine generische Funktion haben, die dieses T & U -> T Prinzip verwendet, die Funktion jedoch mit T mit einem genauen Typ instanziiert aufrufen. Es gibt also keine Möglichkeit, diesen Sound zu erzeugen (es ist wirklich nicht in Ordnung, bei der Instanziierung Fehler zu machen) - nicht unbedingt ein Blocker, aber es ist verwirrend, dass eine generische Funktion freizügiger ist als eine manuell instanziierte Version von sich selbst!

Es wird auch angenommen, dass T immer T | U zuweisbar ist, aber es ist nicht offensichtlich, wie diese Regel angewendet wird, wenn U ein exakter Typ ist. Kann { s: "hello", n: 3 } { s: string } | Exact<{ n: number }> zugewiesen werden? "Ja" scheint die falsche Antwort zu sein, denn wer auch immer nach n sucht und es findet, wird nicht glücklich sein, s , aber "Nein" scheint auch falsch zu sein, weil wir die grundlegenden T -> T | U verletzt haben

Sonstiges

Was bedeutet function f<T extends Exact<{ n: number }>(p: T) ? :verwirrt:

Oft werden exakte Typen gewünscht, bei denen man wirklich eine "auto-disjunkte" Union will. Mit anderen Worten, Sie haben möglicherweise eine API, die { type: "name", firstName: "bob", lastName: "bobson" } oder { type: "age", years: 32 } akzeptieren kann, aber { type: "age", years: 32, firstName: 'bob" } nicht akzeptieren möchte, da etwas Unvorhersehbares passieren wird. Der "richtige" Typ ist wohl { type: "name", firstName: string, lastName: string, age: undefined } | { type: "age", years: number, firstName: undefined, lastName: undefined } aber gut, das ist nervig zu tippen. Wir könnten möglicherweise über Zucker nachdenken, um solche Typen zu erstellen.

Zusammenfassung: Benötigte Anwendungsfälle

Unsere hoffnungsvolle Diagnose lautet, dass dies, abgesehen von den relativ wenigen wirklich geschlossenen APIs, eine XY-Problemlösung ist. Wo immer möglich, sollten wir EPC verwenden, um "schlechte" Eigenschaften zu erkennen. Wenn Sie also ein Problem haben und Sie denken, dass genaue Typen die richtige Lösung sind, beschreiben Sie bitte hier das ursprüngliche Problem, damit wir einen Katalog von Mustern zusammenstellen und sehen können, ob es andere Lösungen gibt, die weniger invasiv/verwirrend sind.

Der Hauptpunkt, an dem ich sehe, dass Leute überrascht sind, wenn sie keinen genauen Objekttyp haben, ist das Verhalten von Object.keys und for..in -- sie produzieren immer einen string Typ anstelle von 'a'|'b' für etwas eingegebenes { a: any, b: any } .

Wie ich in https://github.com/Microsoft/TypeScript/issues/14094 erwähnt habe und Sie im Abschnitt Sonstiges beschrieben haben, ist es ärgerlich, dass {first: string, last: string, fullName: string} {first: string; last: string} | {fullName: string} .

Zum Beispiel wird davon ausgegangen, dass ein Typ T & U immer T zuweisbar ist, aber dies schlägt fehl, wenn T ein exakter Typ ist

Wenn T ein exakter Typ ist, dann ist T & U vermutlich never (oder T === U ). Rechts?

Oder U ist eine nicht genaue Teilmenge von T

Mein Anwendungsfall, der mich zu diesem Vorschlag geführt hat, sind Redux Reducer.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

Wie Sie in der Zusammenfassung erwähnt haben, ist mein Problem nicht direkt, dass ich genaue Schnittstellen benötige, ich brauche den Spread-Operator, um präzise zu arbeiten. Da aber das Verhalten des Spread-Operators von JS vorgegeben wird, fällt mir als einzige Lösung ein, den Rückgabetyp bzw. die Schnittstelle genau zu definieren.

Verstehe ich richtig, dass die Zuweisung eines Wertes von T zu Exact<T> ein Fehler wäre?

interface Dog {
    name: string;
    isGoodBoy: boolean;
}
let a: Dog = { name: 'Waldo', isGoodBoy: true };
let b: Exact<Dog> = a;

In diesem Beispiel wäre es nicht sicher, Dog auf Exact<Dog> einzuschränken, oder?
Betrachten Sie dieses Beispiel:

interface PossiblyFlyingDog extends Dog {
    canFly: boolean;
}
let c: PossiblyFlyingDog = { ...a, canFly: true };
let d: Dog = c; // this is okay
let e: Exact<Dog> = d; // but this is not

@leonadler Ja, das wäre die Idee. Sie können Exact<T> nur Exact<T> zuweisen. Mein unmittelbarer Anwendungsfall ist, dass Validierungsfunktionen die Exact Typen behandeln würden (zB Anforderungsnutzlasten als any und gültige Exact<T> ausgeben). Exact<T> wäre jedoch T zuweisbar.

@nerumo

Wie Sie in der Zusammenfassung erwähnt haben, ist mein Problem nicht direkt, dass ich genaue Schnittstellen benötige, ich brauche den Spread-Operator, um präzise zu arbeiten. Da aber das Verhalten des Spread-Operators von JS vorgegeben wird, fällt mir als einzige Lösung ein, den Rückgabetyp bzw. die Schnittstelle genau zu definieren.

Ich bin auf das gleiche Problem gestoßen und habe diese Lösung gefunden, die für mich eine ziemlich elegante Problemumgehung ist :)

export type State = {
  readonly counter: number,
  readonly baseCurrency: string,
};

// BAD
export function badReducer(state: State = initialState, action: Action): State {
  if (action.type === INCREASE_COUNTER) {
    return {
      ...state,
      counterTypoError: state.counter + 1, // OK
    }; // it's a bug! but the compiler will not find it 
  }
}

// GOOD
export function goodReducer(state: State = initialState, action: Action): State {
  let partialState: Partial<State> | undefined;

  if (action.type === INCREASE_COUNTER) {
    partialState = {
      counterTypoError: state.counter + 1, // Error: Object literal may only specify known properties, and 'counterTypoError' does not exist in type 'Partial<State>'. 
    }; // now it's showing a typo error correctly 
  }
  if (action.type === CHANGE_BASE_CURRENCY) {
    partialState = { // Error: Types of property 'baseCurrency' are incompatible. Type 'number' is not assignable to type 'string'.
      baseCurrency: 5,
    }; // type errors also works fine 
  }

  return partialState != null ? { ...state, ...partialState } : state;
}

Weitere Informationen finden Sie in diesem Abschnitt meiner Redux-Anleitung:

Beachten Sie, dass dies im Userland mit meinem Vorschlag für Einschränkungstypen (#13257) gelöst werden könnte:

type Exact<T> = [
    case U in U extends T && T extends U: T,
];

Bearbeiten: Aktualisierte Syntax in Bezug auf das Angebot

@piotrwitek danke, der Partial-Trick funktioniert einwandfrei und habe bereits einen Fehler in meiner Codebasis gefunden ;) das ist den kleinen Boilerplate-Code wert. Aber trotzdem stimme ich @isiahmeadows zu, dass ein Exactwäre noch besser

@piotrwitek , das Partial verwendet, hat _fast_ mein Problem gelöst, aber es ermöglicht immer noch, dass die Eigenschaften undefiniert werden, selbst wenn die State-Schnittstelle behauptet, dass dies nicht der Fall ist (ich gehe von striktNullChecks aus).

Am Ende habe ich etwas komplexeres gefunden, um die Schnittstellentypen beizubehalten:

export function updateWithPartial<S extends object>(current: S, update: Partial<S>): S {
    return Object.assign({}, current, update);
}

export function updateWith<S extends object, K extends keyof S>(current: S, update: {[key in K]: S[key]}): S {
    return Object.assign({}, current, update);
}

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

const f: I = {foo: "a", bar: "b"}
updateWithPartial(f, {"foo": undefined}).foo.replace("a", "x"); // Compiles, but fails at runtime
updateWith(f, {foo: undefined}).foo.replace("a", "x"); // Does not compile
updateWith(f, {foo: "c"}).foo.replace("a", "x"); // Compiles and works

@asmundg das ist richtig, die Lösung akzeptiert undefiniert, aber aus meiner Sicht ist dies akzeptabel, da ich in meinen Lösungen nur Aktionsersteller mit erforderlichen Parametern für die Nutzlast verwende, und dies stellt sicher, dass kein undefinierter Wert jemals sein sollte einer Eigenschaft ohne NULL-Werte zugewiesen.
Praktisch verwende ich diese Lösung seit einiger Zeit in der Produktion und dieses Problem ist nie aufgetreten, aber teilen Sie mir Ihre Bedenken mit.

export const CHANGE_BASE_CURRENCY = 'CHANGE_BASE_CURRENCY';

export const actionCreators = {
  changeBaseCurrency: (payload: string) => ({
    type: CHANGE_BASE_CURRENCY as typeof CHANGE_BASE_CURRENCY, payload,
  }),
}

store.dispatch(actionCreators.changeBaseCurrency()); // Error: Supplied parameters do not match any signature of call target.
store.dispatch(actionCreators.changeBaseCurrency(undefined)); // Argument of type 'undefined' is not assignable to parameter of type 'string'.
store.dispatch(actionCreators.changeBaseCurrency('USD')); // OK => { type: "CHANGE_BASE_CURRENCY", payload: 'USD' }

DEMO - striktNullChecks in den Optionen aktivieren

Sie können bei Bedarf auch eine Nullable Payload erstellen. Weitere Informationen finden Sie in meinem Leitfaden: https://github.com/piotrwitek/react-redux-typescript-guide#actions

Wenn Resttypen zusammengeführt werden, kann diese Funktion leicht zu syntaktischem Zucker gemacht werden.

Vorschlag

Die Typgleichheitslogik sollte streng festgelegt werden – nur Typen mit denselben Eigenschaften oder Typen mit Resteigenschaften, die so instanziiert werden können, dass ihre Elterntypen dieselben Eigenschaften haben, gelten als übereinstimmend. Um die Abwärtskompatibilität zu wahren, wird allen Typen ein synthetischer Resttyp hinzugefügt, es sei denn, es existiert bereits einer. Außerdem wird ein neues Flag --strictTypes hinzugefügt, das das Hinzufügen von synthetischen Pausenparametern unterdrückt.

Gleichheiten unter --strictTypes :

type A = { x: number, y: string };
type B = { x: number, y: string, ...restB: <T>T };
type C = { x: number, y: string, z: boolean, ...restC: <T>T };

declare const a: A;
declare const b: B;
declare const c: C;

a = b; // Error, type B has extra property: "restB"
a = c; // Error, type C has extra properties: "z", "restC"
b = a; // OK, restB inferred as {}
b = c; // OK, restB inferred as { z: boolean, ...restC: <T>T }

c = a; // Error, type A is missing property: "z"
       // restC inferred as {}

c = b; // Error, type B is missing property: "z"
       // restC inferred as restB 

Wenn --strictTypes nicht eingeschaltet ist, wird automatisch eine Eigenschaft ...rest: <T>T zum Typ A hinzugefügt. Auf diese Weise sind die Zeilen a = b; und a = c; keine Fehler mehr, wie dies bei der Variablen b in den beiden folgenden Zeilen der Fall ist.

Ein Wort zu Verstößen gegen Annahmen

Es wird davon ausgegangen, dass ein Typ T & U immer T zuweisbar ist, aber dies schlägt fehl, wenn T ein exakter Typ ist.

Ja, & erlaubt falsche Logik, aber das ist auch bei string & number der Fall. Sowohl string als auch number sind unterschiedliche starre Typen, die nicht gekreuzt werden können, aber das Typsystem lässt dies zu. Genaue Typen sind ebenfalls starr, sodass die Inkonsistenz immer noch konsistent ist. Das Problem liegt im Operator & - er ist nicht korrekt.

Ist { s: "hallo", n: 3 } zuweisbar zu { s: string } | Genau<{ n: Zahl }>.

Dies kann übersetzt werden in:

type Test = { s: string, ...rest: <T>T } | { n: number }
const x: Test = { s: "hello", n: 3 }; // OK, s: string; rest inferred as { n: number }

Die Antwort sollte also "ja" lauten. Es ist unsicher, exakte mit nicht genauen Typen zu vereinigen, da die nicht genauen Typen alle exakten Typen subsumieren, es sei denn, eine Diskriminatoreigenschaft ist vorhanden.

Betreff : die Funktion f<T extends Exact<{ n: number }>(p: T) in @RyanCavanaughs Kommentar oben, in einer meiner Bibliotheken würde ich sehr gerne folgende Funktion implementieren:

const checkType = <T>() => <U extends Exact<T>>(value: U) => value;

Dh eine Funktion, die ihren Parameter mit genau demselben Typ zurückgibt, aber gleichzeitig auch prüft, ob ihr Typ auch genau derselbe Typ wie ein anderer (T) ist.

Hier ist ein etwas künstliches Beispiel mit drei meiner fehlgeschlagenen Versuche, beide Anforderungen zu erfüllen:

  1. Keine zusätzlichen Eigenschaften in Bezug auf CorrectObject
  2. Kann HasX zugewiesen werden, ohne HasX als Objekttyp anzugeben
type AllowedFields = "x" | "y";
type CorrectObject = {[field in AllowedFields]?: number | string};
type HasX = { x: number };

function objectLiteralAssignment() {
  const o: CorrectObject = {
    x: 1,
    y: "y",
    // z: "z" // z is correctly prevented to be defined for o by Excess Properties rules
  };

  const oAsHasX: HasX = o; // error: Types of property 'x' are incompatible.
}

function objectMultipleAssignment() {
  const o = {
    x: 1,
    y: "y",
    z: "z",
  };
  const o2 = o as CorrectObject; // succeeds, but undesirable property z is allowed

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

function genericExtends() {
  const checkType = <T>() => <U extends T>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    z: "z", // undesirable property z is allowed
  }); // o is inferred to be { x: number; y: string; z: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

Hier ist HasX ein stark vereinfachter Typ (der tatsächliche Typ wird einem Schematyp zugeordnet), der in einer anderen Ebene als die Konstante selbst definiert ist, daher kann ich den Typ von o nicht erstellen sein ( CorrectObject & HasX ).

Mit Exact Types wäre die Lösung:

function exactTypes() {
  const checkType = <T>() => <U extends Exact<T>>(value: U) => value;
  const o = checkType<CorrectObject>()({
    x: 1,
    y: "y",
    // z: "z", // undesirable property z is *not* allowed
  }); // o is inferred to be { x: number; y: string; }

  type HasX = { x: number };

  const oAsHasX: HasX = o; // succeeds
}

@andy-ms

Wenn T ein exakter Typ ist, dann ist T & U vermutlich nie (oder T === U). Rechts?

Ich denke, T & U sollte nur never , wenn U nachweislich inkompatibel mit T , zB wenn T Exact<{x: number | string}> und U ist {[field: string]: number} , dann sollte T & U Exact<{x: number}>

Siehe die erste Antwort darauf:

Oder U ist eine nicht exakte Teilmenge von T

Ich würde sagen, wenn U T zuweisbar ist, dann T & U === T . Aber wenn T und U unterschiedliche genaue Typen sind, dann T & U === never .

Warum ist in Ihrem Beispiel eine checkType Funktion erforderlich, die nichts tut? Warum nicht einfach const o: Exact<CorrectObject> = { ... } ?

Weil es die Information verliert, dass x definitiv existiert (optional in CorrectObject) und eine Zahl ist (number | string in CorrectObject). Oder vielleicht habe ich falsch verstanden, was Exakt bedeutet, ich dachte, es würde nur fremde Eigenschaften verhindern, nicht dass es rekursiv bedeuten würde, dass alle Typen genau gleich sein müssen.

Eine weitere Überlegung bei der Unterstützung von Exact Types und gegen die aktuelle EPC ist das Refactoring - wenn das Refactoring von Extract Variable verfügbar wäre, würde man EPC verlieren, es sei denn, die extrahierte Variable würde eine Typannotation einführen, die sehr ausführlich werden könnte.

Um zu verdeutlichen, warum ich Exact Types unterstütze - es ist nicht für diskriminierte Unions, sondern für Rechtschreibfehler und irrtümlicherweise überflüssige Eigenschaften, falls die Typ-Costraint nicht gleichzeitig mit dem Objektliteral angegeben werden kann.

@andy-ms

Ich würde sagen, wenn U T zuweisbar ist, dann T & U === T. Aber wenn T und U verschiedene exakte Typen sind, dann T & U === nie.

Der Operator vom Typ & ist ein Schnittoperator, das Ergebnis ist die gemeinsame Teilmenge beider Seiten, die auch nicht unbedingt gleich ist. Einfachstes Beispiel, das mir einfällt:

type T = Exact<{ x?: any, y: any }>;
type U = { x: any, y? any };

hier sollte T & U Exact<{ x: any, y: any }> , was eine Teilmenge von T und U , aber weder T ist eine Teilmenge von U (fehlendes x) noch U ist eine Teilmenge von T (fehlendes y).

Dies sollte unabhängig davon funktionieren, ob T , U oder T & U exakte Typen sind.

@magnushiie Sie haben einen guten Punkt - genaue Typen können die Zuweisbarkeit von Typen mit einer größeren Breite einschränken, aber dennoch die Zuweisung von Typen mit einer größeren Tiefe zulassen. Sie könnten also Exact<{ x: number | string }> mit Exact<{ x: string | boolean }> schneiden, um Exact<{ x: string }> . Ein Problem ist, dass dies nicht wirklich typsicher ist, wenn x nicht schreibgeschützt ist – wir möchten diesen Fehler möglicherweise für genaue Typen beheben, da sie ein strengeres Verhalten erfordern.

Exakte Typen können auch für Beziehungsprobleme zwischen Typargumenten und Indexsignaturen verwendet werden.

interface T {
    [index: string]: string;
}

interface S {
    a: string;
    b: string;
}

interface P extends S {
    c: number;
}

declare function f(t: T);
declare function f2(): P;
const s: S = f2();

f(s); // Error because an interface can have more fields that is not conforming to an index signature
f({ a: '', b: '' }); // No error because literals is exact by default

Hier ist eine hackige Methode, um den genauen Typ zu überprüfen:

// type we'll be asserting as exact:
interface TextOptions {
  alignment: string;
  color?: string;
  padding?: number;
}

// when used as a return type:
function getDefaultOptions(): ExactReturn<typeof returnValue, TextOptions> {
  const returnValue = { colour: 'blue', alignment: 'right', padding: 1 };
  //             ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.
  return returnValue
}

// when used as a type:
function example(a: TextOptions) {}
const someInput = {padding: 2, colour: '', alignment: 'right'}
example(someInput as Exact<typeof someInput, TextOptions>)
  //          ERROR: ^^ Property 'colour' is missing in type 'TextOptions'.

Leider ist es derzeit nicht möglich, die Exact-Assertion als Typparameter zu erstellen, daher muss sie während des Aufrufs erfolgen (dh Sie müssen sich daran erinnern).

Hier sind die Hilfsprogramme, die erforderlich sind, damit es funktioniert (danke an @tycho01 für einige davon):

type Exact<A, B extends Difference<A, B>> = AssertPassThrough<Difference<A, B>, A, B>
type ExactReturn<A, B extends Difference<A, B>> = B & Exact<A, B>

type AssertPassThrough<Actual, Passthrough, Expected extends Actual> = Passthrough;
type Difference<A, Without> = {
  [P in DiffUnion<keyof A, keyof Without>]: A[P];
}
type DiffUnion<T extends string, U extends string> =
  ({[P in T]: P } &
  { [P in U]: never } &
  { [k: string]: never })[T];

Siehe: Spielplatz .

Schön! @gcanti ( typelevel-ts ) und @pelotom ( type-zoo ) könnten ebenfalls interessiert sein. :)

Für alle Interessierten habe ich eine einfache Möglichkeit gefunden, exakte Typen für Funktionsparameter zu erzwingen. Funktioniert zumindest auf TS 2.7.

function myFn<T extends {[K in keyof U]: any}, U extends DesiredType>(arg: T & U): void;

BEARBEITEN: Ich denke, damit dies funktioniert, müssen Sie ein Objektliteral direkt im Argument angeben; Dies funktioniert nicht, wenn Sie oben eine separate const deklarieren und diese stattdessen übergeben. :/ Aber eine Problemumgehung besteht darin, einfach die Objektverteilung am Aufrufort zu verwenden, dh myFn({...arg}) .

EDIT: Entschuldigung, ich habe nicht gelesen, dass Sie nur TS 2.7 erwähnt haben. Ich werde es dort testen!

@vaskevich Ich kann es anscheinend nicht zum Laufen bringen, dh es erkennt colour als überschüssige Eigenschaft :

Wenn bedingte Typen landen (#21316), können Sie Folgendes tun, um genaue Typen als Funktionsparameter zu verlangen, auch für "nicht frische" Objektliterale:

type Exactify<T, X extends T> = T & {
    [K in keyof X]: K extends keyof T ? X[K] : never
}

type Foo = {a?: string, b: number}

declare function requireExact<X extends Exactify<Foo, X>>(x: X): void;

const exact = {b: 1}; 
requireExact(exact); // okay

const inexact = {a: "hey", b: 3, c: 123}; 
requireExact(inexact);  // error
// Types of property 'c' are incompatible.
// Type 'number' is not assignable to type 'never'.

Wenn Sie den Typ erweitern, funktioniert es natürlich nicht, aber ich glaube nicht, dass Sie wirklich etwas dagegen tun können:

const inexact = {a: "hey", b: 3, c: 123} as Foo;
requireExact(inexact);  // okay

Die Gedanken?

Es sieht so aus, als würden bei den Funktionsparametern Fortschritte gemacht. Hat jemand eine Möglichkeit gefunden, genaue Typen für einen Funktionsrückgabewert zu erzwingen?

@jezzgoodwin nicht wirklich. Siehe #241, was die Hauptursache dafür ist, dass Funktionsrückgaben nicht richtig auf zusätzliche Eigenschaften überprüft werden

Noch ein Anwendungsfall. Ich bin gerade fast auf einen Fehler gestoßen wegen der folgenden Situation, die nicht als Fehler gemeldet wird:

interface A {
    field: string;
}

interface B {
    field2: string;
    field3?: string;
}

type AorB = A | B;

const fixture: AorB[] = [
    {
        field: 'sfasdf',
        field3: 'asd' // ok?!
    },
];

( Spielplatz )

Die naheliegende Lösung hierfür könnte sein:

type AorB = Exact<A> | Exact<B>;

Ich habe in #16679 eine vorgeschlagene Problemumgehung gesehen, aber in meinem Fall ist der Typ AorBorC (kann wachsen) und jedes Objekt hat mehrere Eigenschaften, daher ist es ziemlich schwierig, einen Satz von fieldX?:never Eigenschaften manuell zu berechnen für jeden Typ.

@michalstocki Ist das nicht #20863? Sie möchten, dass die Überprüfung von übermäßigem Eigentum bei Gewerkschaften strenger wird.

Auf jeden Fall können Sie diese fieldX?:never Eigenschaften programmgesteuert statt manuell mit bedingten Typen ausführen, wenn keine genauen Typen und eine strenge übermäßige Eigenschaftsprüfung für Unions vorhanden sind:

type AllKeys<U> = U extends any ? keyof U : never
type ExclusifyUnion<U> = [U] extends [infer V] ?
 V extends any ? 
 (V & {[P in Exclude<AllKeys<U>, keyof V>]?: never}) 
 : never : never

Und dann definiere deine Gewerkschaft als

type AorB = ExclusifyUnion<A | B>;

die sich ausdehnt zu

type AorB = (A & {
    field2?: undefined;
    field3?: undefined;
}) | (B & {
    field?: undefined;
})

automatisch. Es funktioniert auch für alle AorBorC .

Siehe auch https://github.com/Microsoft/TypeScript/issues/14094#issuecomment -373780463 für exklusive oder Implementierung

@jcalz Der erweiterte Typ ExclusifyUnion ist nicht sehr sicher:

const { ...fields } = o as AorB;

fields.field3.toUpperCase(); // it shouldn't be passed

Die Felder von fields sind alle nicht optional.

Ich glaube nicht, dass das viel mit Exact-Typen zu tun hat, aber mit dem, was passiert, wenn Sie ein union-typisiertes Objekt verteilen und dann destrukturieren. Jede Vereinigung wird am Ende zu einem einzigen schnittpunktartigen Typ abgeflacht, da sie ein Objekt in einzelne Eigenschaften zerlegt und sie dann wieder zusammenfügt; jegliche Korrelation oder Einschränkung zwischen den Bestandteilen jeder Union geht verloren. Ich bin mir nicht sicher, wie Sie es vermeiden können ... Wenn es sich um einen Fehler handelt, kann es sich um ein separates Problem handeln.

Offensichtlich werden sich die Dinge besser verhalten, wenn Sie vor der Destrukturierung eine Typüberwachung durchführen:

declare function isA(x: any): x is A;
declare function isB(x: any): x is B;

declare const o: AorB;
if (isA(o)) {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
} else {
  const { ...fields } = o;
  fields.field3.toUpperCase(); // error
  if (fields.field3) {
    fields.field3.toUpperCase(); // okay
  }
}

Nicht, dass dies das Problem "behebt", das Sie sehen, aber so würde ich erwarten, dass jemand mit einer eingeschränkten Gewerkschaft handelt.

Vielleicht behebt https://github.com/Microsoft/TypeScript/pull/24897 das Verbreitungsproblem

Ich komme vielleicht zu spät zur Party, aber so können Sie zumindest sicherstellen, dass Ihre Typen genau übereinstimmen:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
const sureIsTrue: (fact: true) => void = () => {};
const sureIsFalse: (fact: false) => void = () => {};



declare const x: string;
declare const y: number;
declare const xAndYAreOfTheSameType: AreSame<typeof x, typeof y>;
sureIsFalse(xAndYAreOfTheSameType);  // <-- no problem, as expected
sureIsTrue(xAndYAreOfTheSameType);  // <-- problem, as expected

wünschte ich könnte das machen:

type Exact<A, B> = A extends B ? B extends A ? B : never : never;
declare function needExactA<X extends Exact<A, X>>(value: X): void;

Würde die in dieser Ausgabe beschriebene Funktion in einem Fall helfen, in dem eine leere/indizierte Schnittstelle mit objektähnlichen Typen wie Funktionen oder Klassen übereinstimmt?

interface MyType
{
    [propName: string]: any;
}

function test(value: MyType) {}

test({});           // OK
test(1);            // Fails, OK!
test('');           // Fails, OK!
test(() => {});     // Does not fail, not OK!
test(console.log);  // Does not fail, not OK!
test(console);      // Does not fail, not OK!

Die Schnittstelle MyType definiert nur eine Indexsignatur und wird als Typ des einzigen Parameters der Funktion test . Parameter, der an die Funktion vom Typ übergeben wird:

  • Objektliteral {} , passiert. Erwartetes Verhalten.
  • Numerische Konstante 1 geht nicht durch. Erwartetes Verhalten (_Argument vom Typ '1' kann Parameter vom Typ 'MyType' nicht zugewiesen werden._)
  • String-Literal '' wird nicht übergeben. Erwartetes Verhalten (_`Argument vom Typ '""' kann Parameter vom Typ 'MyType' nicht zugewiesen werden._)
  • Deklaration der Pfeilfunktion () => {} : Passiert. Nicht erwartetes Verhalten. Passiert wahrscheinlich, weil Funktionen Objekte sind?
  • Klassenmethode console.log Passiert. Nicht erwartetes Verhalten. Ähnlich der Pfeilfunktion.
  • Klasse console Pässe. Nicht erwartetes Verhalten. Wahrscheinlich, weil Klassen Objekte sind?

Der Punkt ist, nur Variablen zuzulassen, die genau der Schnittstelle MyType indem sie bereits von diesem Typ sind (und nicht implizit in sie konvertiert werden). TypeScript scheint viele implizite Konvertierungen basierend auf Signaturen durchzuführen, daher könnte dies etwas sein, das nicht unterstützt werden kann.

Entschuldigung, wenn dies nicht zum Thema gehört. Bisher entspricht dieses Problem am ehesten dem Problem, das ich oben erläutert habe.

@ Janne252 Dieser Vorschlag könnte Ihnen indirekt helfen. Angenommen, Sie haben das offensichtliche Exact<{[key: string]: any}> ausprobiert, und hier ist der Grund, warum es funktionieren würde:

  • Objektliterale passieren wie erwartet, wie sie es bereits bei {[key: string]: any} tun.
  • Numerische Konstanten schlagen wie erwartet fehl, da Literale {[key: string]: any} nicht zuweisbar sind.
  • Zeichenfolgenliterale schlagen wie erwartet fehl, da sie {[key: string]: any} nicht zuweisbar sind.
  • Funktionen und Klassenkonstruktoren schlagen aufgrund ihrer call Signatur fehl (es ist keine Zeichenfolgeneigenschaft).
  • Das console Objekt wird übergeben, weil es genau das ist, ein Objekt (keine Klasse). JS macht keine Trennung zwischen Objekten und Schlüssel/Wert-Wörterbüchern, und TS unterscheidet sich hier nicht, abgesehen von der hinzugefügten zeilenpolymorphen Typisierung. Außerdem unterstützt TS keine wertabhängigen Typen, und typeof ist einfach Zucker, um ein paar zusätzliche Parameter und/oder Typaliase hinzuzufügen - es ist nicht annähernd so magisch, wie es aussieht.

@blakeembrey @michalstocki @aleksey-bykov
So mache ich genaue Typen:

type Exact<A extends object> = A & {__kind: keyof A};

type Foo = Exact<{foo: number}>;
type FooGoo = Exact<{foo: number, goo: number}>;

const takeFoo = (foo: Foo): Foo => foo;

const foo = {foo: 1} as Foo;
const fooGoo = {foo: 1, goo: 2} as FooGoo;

takeFoo(foo)
takeFoo(fooGoo) // error "[ts]
//Argument of type 'Exact<{ foo: number; goo: number; }>' is not assignable to parameter of type 'Exact<{ //foo: number; }>'.
//  Type 'Exact<{ foo: number; goo: number; }>' is not assignable to type '{ __kind: "foo"; }'.
//    Types of property '__kind' are incompatible.
//      Type '"foo" | "goo"' is not assignable to type '"foo"'.
//        Type '"goo"' is not assignable to type '"foo"'."

const takeFooGoo = (fooGoo: FooGoo): FooGoo => fooGoo;

takeFooGoo(fooGoo);
takeFooGoo(foo); // error "[ts]
// Argument of type 'Exact<{ foo: number; }>' is not assignable to parameter of type 'Exact<{ foo: number; // goo: number; }>'.
//  Type 'Exact<{ foo: number; }>' is not assignable to type '{ foo: number; goo: number; }'.
//    Property 'goo' is missing in type 'Exact<{ foo: number; }>'.

Es funktioniert für Funktionsparameter, Rückgaben und sogar für Zuweisungen.
const foo: Foo = fooGoo; // Fehler
Kein Laufzeit-Overhead. Das einzige Problem ist, dass Sie jedes Mal, wenn Sie ein neues exaktes Objekt erstellen, es gegen seinen Typ umwandeln müssen, aber das ist wirklich keine große Sache.

Ich glaube, das ursprüngliche Beispiel hat das richtige Verhalten: Ich erwarte, dass interface s geöffnet sind. Im Gegensatz dazu erwarte ich, dass type s geschlossen sind (und sie werden nur manchmal geschlossen). Hier ist ein Beispiel für ein überraschendes Verhalten beim Schreiben eines Typs MappedOmit :
https://gist.github.com/donabrams/b849927f5a0160081db913e3d52cc7b3

Der Typ MappedOmit im Beispiel funktioniert nur wie vorgesehen für diskriminierte Gewerkschaften. Bei nicht diskriminierten Unions wird Typescript 3.2 übergeben, wenn eine beliebige Schnittmenge der Typen in der Union übergeben wird.

Die obigen Problemumgehungen, die as TypeX oder as any zum Casten verwenden, haben den Nebeneffekt, dass Fehler in der Konstruktion ausgeblendet werden!. Wir möchten, dass unser Typechecker uns hilft, auch Konstruktionsfehler zu erkennen! Darüber hinaus gibt es verschiedene Dinge, die wir statisch aus wohldefinierten Typen generieren können. Workarounds wie die oben genannten (oder die hier beschriebenen nominalen Workarounds: https://gist.github.com/donabrams/74075e89d10db446005abe7b1e7d9481) verhindern, dass diese Generatoren funktionieren (obwohl wir _ führende Felder filtern können, ist dies eine schmerzhafte Konvention das ist absolut vermeidbar).

@aleksey-bykov fyi Ich denke, Ihre Implementierung ist zu 99% auf dem Weg dorthin, das hat für mich funktioniert:

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

const value1 = {};
const value2 = {a:1};

// works
const exactValue1: Exact<{}, typeof value1> = value1;
const exactValue1WithTypeof: Exact<typeof value1, typeof value1> = value1;

// cannot assign {a:number} to never
const exactValue1Fail: Exact<{}, typeof value2> = value2;
const exactValue1FailWithTypeof: Exact<typeof value1, typeof value2> = value2;

// cannot assign {} to never
const exactValue2Fail: Exact<{a: number}, typeof value1> = value1;
const exactValue2FailWithTypeof: Exact<typeof value2, typeof value1> = value1;

// works
const exactValue2: Exact<{a: number}, typeof value2> = value2;
const exactValue2WithTypeof: Exact<typeof value2, typeof value2> = value2;

Wow, bitte lass die Blumen hier drüben, Geschenke gehören in den Mülleimer

Eine kleine Verbesserung, die hier gemacht werden kann:
Durch die Verwendung der folgenden Definition von Exact effektiv eine Subtraktion von B von A als A & never Typen auf allen B eindeutigen Schlüsseln von

type Omit<T, K> = Pick<T, Exclude<keyof T, keyof K>>;
type Exact<A, B = {}> = A & Record<keyof Omit<B, A>, never>;

Schließlich wollte ich dies tun können, ohne die explizite Template-Verwendung des zweiten B Template-Arguments hinzufügen zu müssen. Ich konnte dies zum Laufen bringen, indem ich mit einer Methode umschloss - nicht ideal, da sie sich auf die Laufzeit auswirkt, aber es ist nützlich, wenn Sie es wirklich brauchen:

function makeExactVerifyFn<T>() {
  return <C>(x: C & Exact<T, C>): C => x;
}

Beispielverwendung:

interface Task {
  title: string;
  due?: Date;
}

const isOnlyTask = makeExactVerifyFn<Task>();

const validTask_1 = isOnlyTask({
    title: 'Get milk',
    due: new Date()  
});

const validTask_2 = isOnlyTask({
    title: 'Get milk'
});

const invalidTask_1 = isOnlyTask({
    title: 5 // [ts] Type 'number' is not assignable to type 'string'.
});

const invalidTask_2 = isOnlyTask({
    title: 'Get milk',
    procrastinate: true // [ts] Type 'true' is not assignable to type 'never'.
});

@danielnmsft Es scheint seltsam, B in Exact<A, B> in Ihrem Beispiel optional zu lassen, insbesondere wenn dies für eine ordnungsgemäße Validierung erforderlich ist. Ansonsten sieht es für mich ganz gut aus. Es sieht jedoch besser aus mit dem Namen Equal .

@drabinowitz Ihr Typ Exact repräsentiert nicht das, was hier vorgeschlagen wurde und sollte wahrscheinlich in etwas wie AreExact . Ich meine, Sie können das mit Ihrem Typ nicht tun:

function takesExactFoo<T extends Exact<Foo>>(foo: T) {}

Ihr Typ ist jedoch praktisch, um den genauen Parametertyp zu implementieren!

type AreSame<A, B> = A extends B
    ? B extends A ? true : false
    : false;
type Exact<A, B> = AreSame<A, B> extends true ? B : never;

interface Foo {
    bar: any
}

function takesExactFoo <T>(foo: T & Exact<Foo, T>) {
                    //  ^ or `T extends Foo` to type-check `foo` inside the function
}

let foo = {bar: 123}
let foo2 = {bar: 123, baz: 123}

takesExactFoo(foo) // ok
takesExactFoo(foo2) // error

UPD1 Dadurch wird keine +1 Laufzeitfunktion erstellt wie in der Lösung von @danielnmsft und ist natürlich viel flexibler.

UPD2 Mir ist gerade aufgefallen, dass Daniel im Grunde den gleichen Typ Exact wie @drabinowitz gemacht hat, aber einen kompakteren und wahrscheinlich besseren. Mir wurde auch klar, dass ich dasselbe tat wie Daniel. Aber ich werde meinen Kommentar hinterlassen, falls es jemand nützlich findet.

Diese Definition von AreSame / Exact scheint für den Gewerkschaftstyp nicht zu funktionieren.
Beispiel: Exact<'a' | 'b', 'a' | 'b'> ergibt never .
Dies kann anscheinend behoben werden, indem type AreSame<A, B> = A|B extends A&B ? true : false;

@nerumo hat dies definitiv für die gleiche Art von Reduzierfunktion gefunden, die Sie gezeigt haben.

Paar zusätzliche Optionen von dem, was Sie hatten:

1 Sie können den Rückgabetyp mit typeof gleich dem Eingabetyp festlegen. Nützlicher, wenn es sich um einen sehr komplizierten Typ handelt. Für mich ist es klarer, dass die Absicht darin besteht, zusätzliche Eigenschaften zu verhindern.

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): typeof state {
   return {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   };
}

2 Weisen Sie bei Reduzierern anstelle einer temporären Variablen diese vor der Rückgabe selbst zu:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {
   return (state = {
       ...state,
       fullName: action.payload        // THIS IS REPORTED AS AN ERROR
   });
}

3 Wenn Sie wirklich eine temporäre Variable wollen, geben Sie ihr keinen expliziten Typ, sondern verwenden Sie erneut typeof state

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: typeof state = {
       ...state,
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

3b Wenn Ihr Reduzierstück ...state nicht enthält, können Sie Partial<typeof state> für den Typ verwenden:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>) {

   const newState: Partial<typeof state> = {
       name: 'Simon',
       fullName: action.payload         // THIS IS REPORTED AS AN ERROR
   };

   return newState;
}

Ich habe das Gefühl, dass diese ganze Konversation (und ich habe gerade den ganzen Thread gelesen) den Kern des Problems für die meisten Leute übersehen hat, und das heißt, um Fehler zu vermeiden, wollen wir nur eine Typzusicherung, um zu verhindern, dass ein "breiterer" Typ nicht zugelassen wird:

Dies ist, was die Leute zuerst versuchen können, was 'fullName' nicht verbietet:

 return <State> {
       ...state,
       fullName: action.payload         // compiles ok :-(
   };

Dies liegt daran , <Dog> cat wird man die Compiler zu sagen - ja , ich weiß , was ich tue, es ist ein Dog ! Sie fragen nicht um Erlaubnis.

Was mir also am nützlichsten wäre, wäre eine strengere Version von <Dog> cat , die überflüssige Eigenschaften verhindern würde:

 return <strict State> {
       ...state,
       fullName: action.payload     // compiles ok :-(
   };

Die ganze Sache vom Typ Exact<T> hat viele Konsequenzen (das ist ein langer Thread!). Es erinnert mich an die ganze Debatte über 'geprüfte Ausnahmen', in der es etwas ist, von dem Sie glauben, dass Sie es wollen, aber es stellt sich heraus, dass es viele Probleme hat (wie plötzlich fünf Minuten später ein Unexact<T> ).

Auf der anderen Seite würde <strict T> eher wie eine Barriere wirken, um zu verhindern, dass 'unmögliche' Typen 'durchkommen'. Es ist im Wesentlichen ein Typfilter, der den Typ durchläuft (wie oben mit Laufzeitfunktionen).

Für Neuankömmlinge wäre es jedoch leicht anzunehmen, dass dadurch „schlechte Daten“ verhindert wurden, wenn dies unmöglich wäre.

Wenn ich also eine Vorschlagssyntax machen müsste, wäre es diese:

/// This syntax is ONLY permitted directly in front of an object declaration:
return <strict State> { ...state, foooooooooooooo: 'who' };

Zurück zum OP: Theoretisch [1] mit negierten Typen könnten Sie type Exact<T> = T & not Record<not keyof T, any> schreiben. Dann würde ein Exact<{x: string}> allen Typen mit anderen Schlüsseln als x verbieten, ihm zugewiesen zu werden. Ich bin mir nicht sicher, ob das ausreicht, um die Anforderungen aller hier zu erfüllen, aber es scheint perfekt zum OP zu passen.

[1] Ich sage theoretisch, weil das auch auf besseren Indexsignaturen beruht

Bin gespannt, ob ich das hier beschriebene Problem habe. Ich habe Code wie:

const Layers = {
  foo: 'foo'
  bar: 'bar'
  baz: 'baz'
}

type Groups = {
  [key in keyof Pick<Layers, 'foo' | 'bar'>]: number
}

const groups = {} as Groups

Dann kann ich unbekannte Eigenschaften festlegen, was ich nicht möchte:

groups.foo = 1
groups.bar = 2
groups.anything = 2 // NO ERROR :(

Die Einstellung von anything funktioniert immer noch und der Schlüsselwerttyp ist any . Ich hatte gehofft, es wäre ein Fehler.

Wird das durch dieses Problem gelöst?

Es stellte sich heraus, dass ich es hätte tun sollen

type Groups = {
  [key in keyof Pick<typeof Layers, 'foo' | 'bar'>]: number
}

Beachten Sie die zusätzliche Verwendung von typeof .

Das Atom-Plugin atom-typescript versuchte, nicht zu scheitern, und stürzte schließlich ab. Als ich typeof hinzufügte, liefen die Dinge wieder normal und unbekannte Requisiten waren nicht mehr erlaubt, was ich erwartet hatte.

Mit anderen Worten, als ich typeof , hat atom-typescript versucht, den Typ an anderen Stellen des Codes zu ermitteln, an denen ich die Objekte vom Typ Groups , und es erlaubte mir, unbekannte Requisiten hinzuzufügen und mir einen Typhinweis von any für sie zu zeigen.

Also ich glaube nicht, dass ich das Problem dieses Threads habe.

Eine weitere Komplikation könnte der Umgang mit optionalen Eigenschaften sein.

Wenn Sie einen Typ mit optionalen Eigenschaften haben, was würde Exact<T> für diese Eigenschaften bedeuten:

export type PlaceOrderResponse = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharged?: number
};

Bedeutet Exact<T> dass jede optionale Eigenschaft definiert werden muss? Als was würden Sie es angeben? Nicht 'undefined' oder 'null', da dies einen Laufzeiteffekt hat.

Erfordert dies jetzt eine neue Möglichkeit, einen "erforderlichen optionalen Parameter" anzugeben?

Was müssen wir beispielsweise amountCharged im folgenden Codebeispiel zuweisen, damit es die 'Genauigkeit' des Typs erfüllt? Wir sind nicht sehr 'exakt', wenn wir nicht erzwingen, dass diese Eigenschaft zumindest irgendwie 'bestätigt' wird. Ist es <never> ? Es darf nicht undefined oder null .

const exactOrderResponse: Exact<PlaceOrderResponse> = 
{
   status: 'paymentFailed',
   orderNumber: '1001',
   amountCharged: ????      
};

Sie denken also vielleicht - es ist immer noch optional, und es ist jetzt genau optional, was einfach in optional übersetzt wird. Und sicherlich müsste es zur Laufzeit nicht gesetzt werden, aber für mich sieht es so aus, als hätten wir gerade Exact<T> 'gebrochen', indem wir ein Fragezeichen gesetzt haben.

Vielleicht muss diese Prüfung nur bei der Wertzuweisung zwischen zwei Typen durchgeführt werden? (Um durchzusetzen, dass beide amountCharged?: number )

Lassen Sie uns hier einen neuen Typ für die Eingabedaten eines Dialogfelds einführen:

export type OrderDialogBoxData = { 
   status: 'success' | 'paymentFailed', 
   orderNumber: string
   amountCharge?: number      // note the typo here!
};

Probieren wir es also aus:

// run the API call and then assign it to a dialog box.
const serverResponse: Exact<PlaceOrderResponse> = await placeOrder();
const dialogBoxData: Exact<OrderDialogBoxData> = serverResponse;    // SHOULD FAIL

Ich würde erwarten, dass dies natürlich aufgrund des Tippfehlers fehlschlägt - obwohl diese Eigenschaft in beiden optional ist.

Also kam ich zurück zu 'Warum wollen wir das überhaupt?' .
Ich denke, es wäre aus diesen Gründen (oder einer Teilmenge je nach Situation):

  • Vermeiden Sie Tippfehler bei Eigenschaftsnamen
  • Wenn wir einer 'Komponente' eine Eigenschaft hinzufügen, möchten wir sicherstellen, dass alle Benutzer, die sie verwenden, auch diese Eigenschaft hinzufügen müssen
  • Wenn wir eine Eigenschaft von einer "Komponente" entfernen, müssen wir sie überall entfernen.
  • Stellen Sie sicher, dass wir nicht unnötig zusätzliche Eigenschaften bereitstellen (vielleicht senden wir sie an eine API und möchten die Nutzlast schlank halten).

Wenn 'exakte optionale Eigenschaften' nicht richtig gehandhabt werden, sind einige dieser Vorteile gebrochen oder stark verwirrt!

Auch im obigen Beispiel haben wir gerade Exact reingelegt, um Tippfehler zu vermeiden, aber es ist uns nur gelungen, ein riesiges Durcheinander zu verursachen! Und es ist jetzt noch spröder als je zuvor.

Ich denke, was ich oft brauche, ist überhaupt kein Exact<T> Typ, sondern einer dieser beiden:

NothingMoreThan<T> oder
NothingLessThan<T>

Wo 'erforderlich optional' ist jetzt eine Sache. Die erste erlaubt, dass nichts Extras durch die RHS der Zuweisung definiert wird, und die zweite stellt sicher, dass alles (einschließlich optionaler Eigenschaften) auf der RHS einer Zuweisung angegeben ist.

NothingMoreThan wäre nützlich für Nutzlasten, die über die Leitung gesendet werden, oder JSON.stringify() und wenn Sie einen Fehler erhalten, weil Sie zu viele Eigenschaften auf RHS haben, müssen Sie Laufzeitcode schreiben, um nur auszuwählen die benötigten Eigenschaften. Und das ist die richtige Lösung - denn so funktioniert Javascript.

NothingLessThan ist so etwas wie das, was wir bereits in Typoskript haben - für alle normalen Aufgaben - außer dass es optionale (optional?: number) Eigenschaften berücksichtigen müsste.

Ich erwarte nicht, dass diese Namen Wirkung zeigen, aber ich denke, das Konzept ist klarer und granularer als Exact<T> ...

Dann vielleicht (wenn wir es wirklich brauchen):

Exact<T> = NothingMoreThan<NothingLessThan<T>>;

oder wäre es:

Exact<T> = NothingLessThan<NothingMoreThan<T>>;   // !!

Dieser Beitrag ist das Ergebnis eines echten Problems, das ich heute habe, bei dem ich einen 'Dialogfeld-Datentyp' habe, der einige optionale Eigenschaften enthält und ich sicherstellen möchte, dass das, was vom Server kommt, diesem zuordbar ist.

Abschließende Anmerkung: NothingLessThan / NothingMoreThan haben ein ähnliches 'Gefühl' wie einige der obigen Kommentare, wo Typ A von Typ B oder B von A erweitert wird. Die Einschränkung besteht darin, dass sie würde optionale Eigenschaften nicht ansprechen (zumindest glaube ich nicht, dass sie es heute könnten).

@simeyla Du

  • "Nichts weniger als" sind nur normale Typen. TS tut dies implizit, und jeder Typ wird als äquivalent zu einem for all T extends X: T .
  • "Nichts mehr als" ist im Grunde das Gegenteil: es ist ein implizites for all T super X: T

Eine Möglichkeit, eine oder beide explizit auszuwählen, wäre ausreichend. Als Nebeneffekt könnten Sie T super C Java als Ihr vorgeschlagenes T extends NothingMoreThan<C> angeben. Ich bin also ziemlich überzeugt, dass dies wahrscheinlich besser ist als die standardmäßigen exakten Typen.

Ich denke, dies sollte jedoch Syntax sein. Vielleicht das?

  • extends T - Die Vereinigung aller Typen, die T zuweisbar sind, dh äquivalent zu einfach T .
  • super T - Die Vereinigung aller Typen T ist zuweisbar.
  • extends super T , super extends T - Die Vereinigung aller Typen äquivalent zu T. Dies fällt einfach aus dem Raster, da nur der Typ sowohl zuweisbar als auch sich selbst zugewiesen werden kann.
  • type Exact<T> = extends super T - Sugar eingebaut für den oben genannten allgemeinen Fall, um die Lesbarkeit zu verbessern.
  • Da dies nur die Zuweisbarkeit umschaltet, könnten Sie immer noch Dinge wie Vereinigungen haben, die exakte oder Supertypen sind.

Dies macht es auch möglich, #14094 im Userland zu implementieren, indem man einfach jede Variante zu Exact<T> , wie Exact<{a: number}> | Exact<{b: number}> .


Ich frage mich, ob dies auch negierte Typen im Userland ermöglicht. Ich glaube, das tut es, aber ich müsste zuerst eine komplizierte Typarithmetik durchführen, um das zu bestätigen, und es ist nicht gerade offensichtlich, dies zu beweisen.

Ich frage mich, ob dies auch negierte Typen im Userland ermöglicht, da (super T) | (erweitert T) ist äquivalent zu unbekannt. Ich glaube, das ist es, aber ich müsste zuerst eine komplizierte Typarithmetik durchführen, um das zu bestätigen, und es ist nicht gerade offensichtlich, dies zu beweisen.

Damit (super T) | (extends T) === unknown die Zuweisung festhält, müsste es sich um eine Gesamtbestellung handeln.

@jack-williams Guter Fang und behoben (durch Entfernen des Anspruchs). Ich habe mich gefragt, warum die Dinge anfangs nicht geklappt haben, als ich ein bisschen herumgespielt habe.

@jack-williams

"Nichts weniger als" sind nur normale Typen. TS tut dies implizit und jeder Typ wird als gleichwertig behandelt

Ja und nein. Aber meistens ja... ...aber nur im strict Modus!

Ich hatte also viele Situationen, in denen ich eine Eigenschaft brauchte, um logisch "optional" zu sein, aber ich wollte, dass der Compiler mir sagte, ob ich sie "vergessen" oder falsch geschrieben hatte.

Nun, genau das bekommt man mit lastName: string | undefined während ich meistens lastName?: string , und natürlich wirst du ohne strict Modus nicht vor all den Unstimmigkeiten gewarnt.

Ich habe den strikten Modus schon immer gewusst und kann für mein Leben keinen guten Grund finden, warum ich ihn bis gestern nicht eingeschaltet habe - aber jetzt, wo ich es habe (und ich wate immer noch durch Hunderte von Fixes ) ist es viel einfacher, das gewünschte Verhalten "out of the box" zu erreichen.

Ich hatte alle möglichen Dinge ausprobiert, um das zu bekommen, was ich wollte – einschließlich des Spielens mit Required<A> extends Required<B> und dem Versuch, optionale ? Eigenschaftsflags zu entfernen. Das hat mich in einen ganz anderen Kaninchenbau geschickt - (und das war alles, bevor ich den strict Modus aktiviert habe).

Der Punkt ist , dass , wenn Sie versuchen , heute etwas in der Nähe ‚genau‘ Typen zu bekommen , dann müssen Sie mit der Aktivierung beginnen strict - Modus (oder was auch immer Kombination von Flags gibt die richtigen Kontrollen). Und wenn ich später middleName: string | undefined hinzufügen musste, dann Boom - ich würde plötzlich überall finden, wo ich es "überdenken" musste :-)

PS. danke für deine Kommentare - war sehr hilfreich. Mir wird klar, dass ich VIEL Code gesehen habe, der eindeutig nicht den strict -Modus verwendet - und dann laufen die Leute wie ich gegen Wände. Ich frage mich, was getan werden kann, um seine Verwendung mehr zu fördern?

@simeyla Ich denke, Ihr Feedback und Ihr Dank sollten an @isiahmeadows gerichtet werden!

Ich dachte, ich würde meine Erfahrungen mit Exact-Typen nach der Implementierung eines einfachen Prototyps aufschreiben. Meine allgemeine Meinung ist, dass das Team mit seiner Einschätzung richtig lag:

Unsere hoffnungsvolle Diagnose ist, dass dies, abgesehen von den relativ wenigen wirklich geschlossenen APIs, eine XY-Problemlösung ist.

Ich glaube nicht, dass die Kosten für die Einführung eines weiteren Objekttyps durch das Auffangen weiterer Fehler oder das Aktivieren neuer Typbeziehungen bezahlt werden. Letztendlich ließen mich exakte Typen mehr _sagen_, aber sie ließen mich nicht mehr _tun_.

Untersuchung einiger potenzieller Anwendungsfälle von genauen Typen:

Starkes Tippen für keys und for ... in .

Präzisere Typen beim Aufzählen von Schlüsseln zu haben scheint verlockend, aber in der Praxis habe ich nie Schlüssel für Dinge aufgezählt, die konzeptionell genau waren. Wenn Sie die Schlüssel genau kennen, warum sprechen Sie sie nicht einfach direkt an?

Härten optional Eigenschaftserweiterung.

Die Zuweisungsregel { ... } <: { ...; x?: T } ist falsch, da der linke Typ eine inkompatible x Eigenschaft enthalten kann, die mit einem Alias ​​versehen wurde. Bei der Zuweisung von einem genauen Typ wird diese Regel richtig. In der Praxis verwende ich diese Regel nie; es scheint eher für Legacy-Systeme geeignet zu sein, die von vornherein keine genauen Typen haben.

Reagieren und HOC

Ich hatte meine letzte Hoffnung auf exakte Typen gesetzt, die das Passieren von Requisiten und die Vereinfachung der Verbreitungstypen verbesserten. Die Realität ist, dass exakte Typen das Gegenteil von beschränktem Polymorphismus sind und im Grunde nicht kompositorisch sind.

Mit einem beschränkten generischen können Sie Requisiten angeben, die Ihnen wichtig sind, und den Rest durchreichen. Sobald die Grenze exakt wird, verlieren Sie die Breiten-Subtypisierung vollständig und das Generische wird deutlich weniger nützlich. Ein weiteres Problem besteht darin, dass eines der wichtigsten Kompositionswerkzeuge in TypeScript die Schnittmenge ist, aber Schnittmengentypen sind nicht mit exakten Typen kompatibel. Jeder nicht-triviale Schnitttyp mit einer exakten Komponente wird leer sein: _exakte Typen setzen sich nicht zusammen_. Für Reagieren und Requisiten möchten Sie wahrscheinlich Zeilentypen und Zeilenpolymorphismus, aber das ist für einen anderen Tag.

Fast alle interessanten Fehler, die durch exakte Typen gelöst werden könnten, werden durch eine übermäßige Eigenschaftsprüfung gelöst; Das größte Problem besteht darin, dass die übermäßige Überprüfung von Eigentum bei Gewerkschaften ohne diskriminierende Eigenschaft nicht funktioniert; lösen dies und fast alle interessanten Probleme, die für genaue Typen relevant sind, verschwinden, IMO.

@jack-williams Ich stimme zu, dass es im Allgemeinen nicht sehr nützlich ist, genaue Typen zu haben. Das Konzept der übermäßigen Eigenschaftsprüfung wird tatsächlich von meinem super T Operatorvorschlag abgedeckt, nur indirekt, weil die Vereinigung aller Typen, denen T zuweisbar ist, insbesondere keine richtigen Untertypen von T enthält.

Ich persönlich unterstütze dies nicht besonders, abgesehen von vielleicht einem T super U *, da der einzige Anwendungsfall, den ich jemals für übermäßige Eigenschaftsprüfungen erlebt habe, der Umgang mit defekten Servern war, etwas, mit dem Sie normalerweise umgehen können Verwenden einer Wrapper-Funktion, um die Anforderungen manuell zu generieren und den überschüssigen Müll zu entfernen. Jedes andere Problem, das ich bisher in diesem Thread gefunden habe, könnte einfach durch die Verwendung einer einfachen diskriminierten Union gelöst werden.

* Dies wäre im Grunde T extends super U mit meinem Vorschlag - untere Grenzen sind manchmal nützlich, um kontravariante generische Typen einzuschränken, und Problemumgehungen führen meiner Erfahrung nach normalerweise dazu, dass viele zusätzliche Typen-Boilerplates eingeführt werden.

@isiahmeadows Ich stimme sicherlich zu, dass die Typen mit niedrigerer

@jack-williams Ich glaube, Sie haben meine Nuance übersehen, dass ich mich hauptsächlich auf die genauen Typen und den damit verbundenen Teil der Überprüfung von überschüssigem Eigentum bezog. Das bisschen über Typen mit niedriger Grenze war aus gutem Grund eine Fußnote - es war ein Exkurs, der nur tangential zusammenhängt.

Ich habe es geschafft, eine Implementierung dafür zu schreiben, die für Funktionsargumente funktioniert, die unterschiedliche Genauigkeit erfordern:

// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
   [P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}

// This can be used to implement partially strict typing e.g.:
// ('b?:' is where the behaviour differs with optional b)
type BaseOptions = { a: string, b: number }

// Checks there are no extra properties (Not More, Less fine)
const noMore = <T extends Subset<BaseOptions, T>>(options: T) => { }
noMore({ a: "hi", b: 4 })        //Fine
noMore({ a: 5, b: 4 })           //Error 
noMore({ a: "o", b: "hello" })   //Error
noMore({ a: "o" })               //Fine
noMore({ b: 4 })                 //Fine
noMore({ a: "o", b: 4, c: 5 })   //Error

// Checks there are not less properties (More fine, Not Less)
const noLess = <T extends Subset<T, BaseOptions>>(options: T) => { }
noLess({ a: "hi", b: 4 })        //Fine
noLess({ a: 5, b: 4 })           //Error
noLess({ a: "o", b: "hello" })   //Error
noLess({ a: "o" })               //Error  |b?: Fine
noLess({ b: 4 })                 //Error
noLess({ a: "o", b: 4, c: 5 })   //Fine

// We can use these together to get a fully strict type (Not More, Not Less)
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 })        //Fine
strict({ a: 5, b: 4 })           //Error
strict({ a: "o", b: "hello" })   //Error
strict({ a: "o" })               //Error  |b?: Fine
strict({ b: 4 })                 //Error
strict({ a: "o", b: 4, c: 5 })   //Error

// Or a fully permissive type (More Fine, Less Fine)
type Permissive<A extends {}, B extends {}> = Subset<A, B> | Subset<B, A>;
const permissive = <T extends Permissive<BaseOptions, T>>(options: T) => { }
permissive({ a: "hi", b: 4 })        //Fine
permissive({ a: 5, b: 4 })           //Error
permissive({ a: "o", b: "hello" })   //Error
permissive({ a: "o" })               //Fine
permissive({ b: 4 })                 //Fine
permissive({ a: "o", b: 4, c: 5 })   //Fine


Genauer Typ für die Variablenzuweisung, von dem ich festgestellt habe, dass er nichts tut ...

// This is a little unweildy, there's also a shortform that works in many cases:
type Exact<A extends {}> = Subset<A, A>
// The simpler Exact type works for variable typing
const options0: Exact<BaseOptions> = { a: "hi", b: 4 }        //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 }           //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" }   //Error
const options3: Exact<BaseOptions> = { a: "o" }               //Error |b?: Fine
const options4: Exact<BaseOptions> = { b: 4 }                 //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 }   //Error

// It also works for function typing when using an inline value
const exact = (options: Exact<BaseOptions>) => { }
exact({ a: "hi", b: 4 })        //Fine
exact({ a: 5, b: 4 })           //Error
exact({ a: "o", b: "hello" })   //Error
exact({ a: "o" })               //Error  |b?: Fine
exact({ b: 4 })                 //Error
exact({ a: "o", b: 4, c: 5 })   //Error

// But not when using a variable as an argument even of the same type
const options6 = { a: "hi", b: 4 }
const options7 = { a: 5, b: 4 }
const options8 = { a: "o", b: "hello" }
const options9 = { a: "o" }
const options10 = { b: 4 }
const options11 = { a: "o", b: 4, c: 5 }
exact(options6)                 //Fine
exact(options7)                 //Error
exact(options8)                 //Error
exact(options9)                 //Error |b?: Fine
exact(options10)                //Error
exact(options11)                //Fine  -- Should not be Fine

// However using strict does work for that
// const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict(options6)                //Fine
strict(options7)                //Error
strict(options8)                //Error
strict(options9)                //Error |b?: Fine
strict(options10)               //Error
strict(options11)               //Error -- Is correctly Error

Sehen

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

Ich habe das Gefühl, dass ich einen Anwendungsfall dafür habe, wenn ich React-Komponenten einpacke, bei denen ich Requisiten "durchreichen" muss: https://github.com/Microsoft/TypeScript/issues/29883. @jack-williams Irgendwelche Gedanken dazu?

@OliverJAsh Sieht relevant aus, aber ich muss zugeben, dass ich React nicht so gut kenne wie die meisten. Ich denke, es wäre hilfreich, durchzuarbeiten, wie genaue Typen hier genau helfen können.

type MyComponentProps = { foo: 1 };
declare const MyComponent: ComponentType<MyComponentProps>;

type MyWrapperComponent = MyComponentProps & { myWrapperProp: 1 };
const MyWrapperComponent: ComponentType<MyWrapperComponent> = props => (
    <MyComponent
        // We're passing too many props here, but no error!
        {...props}
    />
);

Bitte korrigiert mich jederzeit wenn ich etwas falsches sage.

Ich vermute, der Anfang wäre, MyComponent anzugeben, um einen genauen Typ zu akzeptieren?

declare const MyComponent: ComponentType<Exact<MyComponentProps>>;

In diesem Fall würden wir einen Fehler erhalten, aber wie können Sie den Fehler beheben? Ich gehe hier davon aus, dass die Wrapper-Komponenten nicht nur den gleichen Prop-Typ haben, und Sie müssen irgendwann wirklich eine Prop-Teilmenge dynamisch extrahieren. Ist das eine vernünftige Annahme?

Wenn MyWrapperComponent props auch genau ist, dann würde es meiner Meinung nach ausreichen, eine destrukturierende Bindung durchzuführen. Im generischen Fall würde dies einen Omit Typ gegenüber einem exakten Typ erfordern, und ich kenne die Semantik dort wirklich nicht. Ich vermute, es könnte wie ein homomorpher zugeordneter Typ funktionieren und die Genauigkeit beibehalten, aber ich denke, dies würde mehr Nachdenken erfordern.

Wenn MyWrapperComponent nicht genau ist, ist eine Laufzeitprüfung erforderlich, um die Genauigkeit des neuen Typs zu beweisen Ihre OP). Ich bin mir nicht sicher, wie viel Sie in diesem Fall gewinnen.

Dinge, die ich nicht behandelt habe, weil ich nicht weiß, wie wahrscheinlich sie sind, ist der generische Fall, bei dem props ein allgemeiner Typ ist und wo Sie Requisiten wie { ...props1, ...props2 } kombinieren müssen. Ist das üblich?

@Kotarski Hast du es zufällig in der NPM-Registrierung veröffentlicht?

@gitowiec

@Kotarski Hast du es zufällig in der NPM-Registrierung veröffentlicht?

https://www.npmjs.com/package/ts-strictargs
https://github.com/Kotarski/ts-strictargs

Ich habe diesen Anwendungsfall:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

// I want this to error, because the 'c' should mean it prevents either AB or ABCD from being satisfied.
const foo: AB | ABCD = { a, b, c };

// I presume that I would need to do this:
const foo: Exact<AB> | Exact<ABCD> = { a, b, c };

@ryami333 Das erfordert keine genauen Typen; das braucht nur eine Korrektur der übermäßigen Eigenschaftsprüfung: #13813.

@ryami333 Wenn Sie bereit sind, einen zusätzlichen Typ zu verwenden, habe ich einen Typ , der das tut, was Sie wollen, nämlich eine strengere Version von Unions erzwingen:

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD


type UnionKeys<T> = T extends any ? keyof T : never;
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>

// Error now.
const foo: StrictUnion<AB | ABCD> = { a: "", b: "", c: "" };

@dragomirtitian Faszinierend. Es interessiert mich warum

type KeyofV1<T extends object> = keyof T

ergibt ein anderes Ergebnis als

type KeyofV2<T> = T extends object ? keyof T : never

Könnte mir das jemand erklären?

type AB = { a: string, b: string }
type CD = { c: string, d: string }
type ABCD = AB & CD

KeyofV1< AB | ABCD > // 'a' | 'b'
KeyofV2< AB | ABCD > // 'a' | 'b' | 'c' | 'e'

V1 erhält die gemeinsamen Schlüssel der Union, V2 erhält die Schlüssel jedes Unionsmitglieds und vereinigt das Ergebnis.

@weswigham Gibt es einen Grund, warum sie unterschiedliche Ergebnisse zurückgeben sollten?

Jawohl? Wie gesagt - V1 bekommt die _gemeinsamen Schlüssel_ an jedes Gewerkschaftsmitglied, weil das Argument für keyof am Ende keyof (AB | ABCD) , was nur "A" | "B" , während Die Version innerhalb der Bedingung erhält dank der bedingten Verteilung über ihre Eingabe nur jeweils ein Unionsmitglied, also im Wesentlichen keyof AB | keyof ABCD .

@weswigham Die Bedingung wertet es also eher so aus, wie über eine implizite Schleife?

type Union =
    (AB extends object ? keyof AB : never) |
    (ABCD extends object ? keyof ABCD : never)

Wenn ich diesen Code lese, würde ich normalerweise erwarten, dass die Überprüfung von (AB | ABCD) extends object als eine einzige Einheit funktioniert, indem überprüft wird, dass (AB | ABCD) object zuweisbar ist, und dann keyof (AB | ABCD) als Einheit, 'a' | 'b' . Das implizite Mapping kommt mir wirklich seltsam vor.

@isiahmeadows Sie können distributive bedingte Typen als Foreach für Unions betrachten. Sie wenden den bedingten Typ der Reihe nach auf jedes Mitglied der Vereinigung an, und das Ergebnis ist die Vereinigung jedes Teilergebnisses.

Also UnionKeys<A | B> = UnionKeys<A> | UnionKeys<B> =(keyof A) | (keyof B)

Aber nur, wenn der bedingte Typ verteilt, und er verteilt nur, wenn der getestete Typ ein nackter Typparameter ist. So:

type A<T> = T extends object ? keyof T : never // distributive
type B<T> = [T] extends [object] ? keyof T : never // non distributive the type parameter is not naked
type B<T> = object extends T ? keyof T : never // non distributive the type parameter is not the tested type

Danke Leute, ich glaube ich habe es verstanden. Ich habe es für mein Verständnis neu geordnet; Ich glaube, dass das NegativeUncommonKeys alleine nützlich ist. Hier ist es für den Fall, dass es auch für andere nützlich ist.

type UnionKeys<T> = T extends any ? keyof T : never;
type NegateUncommonKeys<T, TAll> = (
    Partial<
        Record<
            Exclude<
                UnionKeys<TAll>,
                keyof T
            >,
            never
        >
    >
) 

type StrictUnion<T, TAll = T> = T extends any 
  ? T & NegateUncommonKeys<T, TAll>
  : never;

Ich verstehe auch, warum T und TAll beide da sind. Der "Schleifeneffekt", bei dem T getestet und nackt ist, bedeutet, dass jedes Element in der Vereinigung für T angewendet wird, während das ungetestete TAll die ursprüngliche und vollständige Vereinigung aller Elemente enthält.

@weswigham Ja .. außer ich habe das Gefühl, dass sich dieser Abschnitt so liest, als wäre er von einem Compiler-Ingenieur für einen anderen Compiler-Ingenieur geschrieben worden.

Bedingte Typen, bei denen der geprüfte Typ ein nackter Typparameter ist, werden als distributive bedingte Typen bezeichnet.

Was sind nackte Typparameter? (und warum ziehen sie sich nicht ein paar Klamotten an 😄)

dh T bezieht sich auf die einzelnen Konstituenten, nachdem der bedingte Typ über den Unionstyp verteilt wurde)

Erst gestern hatte ich eine Diskussion darüber, was dieser spezielle Satz bedeutet und warum das Wort 'nachher' betont wurde.

Ich denke, die Dokumentation ist unter der Annahme von Vorkenntnissen und Terminologie geschrieben, die Benutzer möglicherweise nicht immer haben.

Der Handbuchteil macht für mich Sinn und erklärt es viel besser, aber ich bin immer noch skeptisch gegenüber der Designwahl dort. Es macht für mich einfach keinen Sinn, wie sich dieses Verhalten aus einer mengentheoretischen und typentheoretischen Perspektive natürlich ergeben würde. Es kommt nur ein wenig zu hackisch rüber.

folgen natürlich aus einer mengentheoretischen und typentheoretischen Perspektive

Nehmen Sie jedes Element einer Menge und teilen Sie es nach einem Prädikat auf.

Das ist eine Verteilungsoperation!

Nehmen Sie jedes Element einer Menge und teilen Sie es nach einem Prädikat auf.

Obwohl dies nur Sinn macht, wenn Sie über Mengen von Mengen sprechen (dh einen Vereinigungstyp), was sehr viel mehr nach Kategorientheorie klingt.

@RyanCavanaugh Okay, lassen Sie mich klarstellen: Ich habe T extends U ? F<T> : G<T> intuitiv als T <: U ⊢ F(T), (T <: U ⊢ ⊥) ⊢ G(T) gelesen, wobei der Vergleich nicht stückweise, sondern als vollständiger Schritt erfolgt. Das unterscheidet sich deutlich von "der Vereinigung von für alle {if t ∈ U then F({t}) else G({t}) | t ∈ T} , was derzeit die Semantik ist.

(Verzeihen Sie, wenn meine Syntax etwas abweicht - mein Wissen über die Typtheorie ist vollständig autodidaktisch, daher weiß ich, dass ich nicht alle syntaktischen Formalismen kenne.)

Welche Operation intuitiver ist, lässt sich endlos diskutieren, aber mit den aktuellen Regeln ist es einfach, einen distributiven Typ mit [T] extends [C] nicht-distributiv zu machen. Wenn die Standardeinstellung nicht-verteilend wäre, bräuchte man eine neue Beschwörung auf einer anderen Ebene, um die Verteilung zu bewirken. Das ist auch eine andere Frage, von der Verhalten häufiger bevorzugt wird; IME Ich möchte fast nie einen nicht verteilenden Typ.

Ja, es gibt keine starke theoretische Grundlage für die Verteilung, da es sich um eine syntaktische Operation handelt.

Die Realität ist, dass es sehr nützlich ist und der Versuch, es auf andere Weise zu codieren, schmerzhaft wäre.

So wie es aussieht, werde ich weitermachen und abbrechen, bevor ich das Gespräch zu weit vom Thema abtreibe.

Es gibt bereits so viele Probleme mit der Verteilungsfähigkeit, warum werden wir uns nicht damit auseinandersetzen, dass eine neue Syntax erforderlich ist?

30572

Hier ist ein Beispielproblem:

Ich möchte angeben, dass der API-Endpunkt/-Service meines Benutzers KEINE zusätzlichen Eigenschaften (wie z. B. ein Kennwort) zurückgeben darf, als die in der Serviceschnittstelle angegebenen. Wenn ich versehentlich ein Objekt mit zusätzlichen Eigenschaften zurückgebe, möchte ich einen Kompilierzeitfehler, unabhängig davon, ob das Ergebnisobjekt von einem Objektliteral oder auf andere Weise erzeugt wurde.

Eine Laufzeitprüfung jedes zurückgegebenen Objekts kann kostspielig sein, insbesondere bei Arrays.

Eine übermäßige Eigenschaftsprüfung hilft in diesem Fall nicht. Ehrlich gesagt denke ich, dass es eine skurrile One-Trick-Pony-Lösung ist. Theoretisch hätte es eine "es funktioniert einfach"-Erfahrung bieten sollen - in der Praxis ist es auch eine Quelle der Verwirrung. Stattdessen hätten genaue Objekttypen implementiert werden sollen, sie hätten beide Anwendungsfälle gut abgedeckt.

@babakness Dein Typ NoExcessiveProps ist ein No-Op. Ich glaube, sie meinen etwa so:

interface API {
    username: () => { username: string }
}

const api: API = {
    username: (): { username: string } => {
        return { username: 'foobar', password: 'secret'} // error, ok
    }
}

const api2: API = {
    username: (): { username: string } => {
        const id: <X>(x: X) => X = x => x;
        const value = id({ username: 'foobar', password: 'secret' });
        return value  // no error, bad?
    }
}

Als Autor des API-Typs möchten Sie erzwingen, dass username nur den Benutzernamen zurückgibt, aber jeder Implementierer kann das umgehen, da Objekttypen keine Breitenbeschränkung haben. Dies kann nur bei der Initialisierung eines Literals angewendet werden, was der Implementierer tun kann oder nicht. Ich würde jedoch jedem davon abraten, exakte Typen als sprachbasierte Sicherheit zu verwenden.

@spion

Eine übermäßige Eigenschaftsprüfung hilft in diesem Fall nicht. Ehrlich gesagt denke ich, dass es eine skurrile One-Trick-Pony-Lösung ist. Theoretisch hätten sie eine "Es funktioniert einfach"-Erfahrung bieten sollen

EPC ist eine einigermaßen vernünftige und leichte Konstruktionsoption, bei der Abdeckungen eine Vielzahl von Problemen darstellen. Die Realität ist, dass Exact-Typen nicht „einfach funktionieren“. Eine solide Implementierung, die die Erweiterbarkeit unterstützt, erfordert ein völlig anderes Typensystem.

@jack-williams Natürlich gibt es auch andere Möglichkeiten, die Gegenwart zu überprüfen (Laufzeitüberprüfungen, bei denen die Leistung kein Problem darstellt, Tests usw.), aber eine zusätzliche Kompilierzeit ist für schnelles Feedback von unschätzbarem Wert.

Außerdem meinte ich nicht, dass exakte Typen "einfach funktionieren". Ich meinte, dass EPC "einfach funktionieren" soll, aber in der Praxis ist es nur begrenzt, verwirrend und unsicher. Vor allem, weil Sie sich im Allgemeinen selbst in den Fuß schießen, wenn Sie versuchen, es "absichtlich" zu verwenden.

Bearbeiten: Ja, ich habe "sie" durch "es" ersetzt, da ich erkannte, dass es verwirrend ist.

@spion

Außerdem meinte ich nicht, dass exakte Typen "einfach funktionieren". Ich meinte, dass EPC "einfach funktionieren" soll, aber in der Praxis ist es nur begrenzt, verwirrend und unsicher. Vor allem, weil Sie sich im Allgemeinen selbst in den Fuß schießen, wenn Sie versuchen, es "absichtlich" zu verwenden.

Mein Fehler. Lesen Sie den Originalkommentar als

Theoretisch hätten sie eine "es funktioniert einfach"-Erfahrung bieten sollen [die exakte Typen anstelle von EPC gewesen wären]

Kommentar in [] ist meine Lektüre.

Die überarbeitete Aussage:

In der Theorie sollte es ein „es funktioniert einfach“ habe bereitgestellt Art von Erfahrung

ist viel klarer. Sorry für meine Fehlinterpretation!

type NoExcessiveProps<O> = {
  [K in keyof O]: K extends keyof O ? O[K] : never 
}

// no error
const getUser1 = (): {username: string} => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
} 

// Compile-time error, OK
const foo: NoExcessiveProps<{username: string}>  = {username: 'a', password: 'b' }

// No error? 🤔
const getUser2 = (): NoExcessiveProps<{username: string}> => {
  const foo = {username: 'foo', password: 'bar' }
  return foo 
}


Das Ergebnis für getUser2 ist überraschend, es fühlt sich inkonsistent an und sollte einen Kompilierungsfehler erzeugen. Was ist die Erkenntnis, warum das nicht der Fall ist?

@babakness Ihr NoExcessiveProps einfach zu T ausgewertet (nun ein Typ mit den gleichen Schlüsseln wie T ). In [K in keyof O]: K extends keyof O ? O[K] : never ist K immer ein Schlüssel von O da Sie über keyof O . Ihr const Beispielfehler, weil es EPC genauso auslöst, als ob Sie es als {username: string} eingegeben hätten.

Wenn es Ihnen nichts ausmacht, eine zusätzliche Funktion aufzurufen, können wir den tatsächlichen Typ des übergebenen Objekts erfassen und eine benutzerdefinierte Form von übermäßigen Eigenschaftsprüfungen durchführen. (Mir ist klar, dass der springende Punkt darin besteht, diese Art von Fehler automatisch abzufangen, daher kann dies von begrenztem Wert sein):

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked(foo) //ok
}

@dragomirtitian Ah ... richtig ... guter Punkt! Ich versuche also, Ihre checked Funktion zu verstehen. ich bin besonders ratlos

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    const bar = checked(foo) // error
    return checked(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    const bar = checked(foo) // error!?
    return checked(foo) //ok
}

Die bar Zuweisung in getUser3 schlägt fehl. Der Fehler scheint foo
image

Details zum Fehler

image

Der Typ für bar hier {} , was so aussieht, als ob es an checked

function checked<T extends E, E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
    return o;
}

E ist nirgendwo zugewiesen. Wenn wir jedoch typeof E durch typeof {} ersetzen, funktioniert es nicht.

Was ist der Typ für E? Gibt es eine Art kontextbezogenes Geschehen?

@babakness Wenn kein anderer Ort zum Ableiten eines Typparameters vorhanden ist, leitet Typescript ihn vom Rückgabetyp ab. Wenn wir also das Ergebnis von checked der Rückgabe von getUser* zuweisen, ist E der Rückgabetyp der Funktion und T ist der den tatsächlichen Typ des Werts, den Sie zurückgeben möchten. Wenn es keinen Ort gibt, um E daraus abzuleiten, wird standardmäßig {} und Sie erhalten immer eine Fehlermeldung.

Der Grund, warum ich es so gemacht habe, war, explizite Typparameter zu vermeiden, Sie könnten eine explizitere Version davon erstellen:

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checked<{ username: string }>()(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checked<{ username: string }>()(foo) //ok
}

Hinweis: Der Ansatz der Curry-Funktion ist erforderlich, da wir noch keine partielle Argumentinferenz (https://github.com/Microsoft/TypeScript/pull/26349) haben, sodass wir einige Typparameter nicht angeben können und andere in der gleicher Anruf. Um dies zu umgehen, geben wir beim ersten Aufruf E an und lassen beim zweiten Aufruf T ableiten. Sie könnten auch die cache Funktion für einen bestimmten Typ zwischenspeichern und die zwischengespeicherte Version verwenden

function checked<E>() {
    return function <T extends E>(o: T & Record<Exclude<keyof T, keyof E>, never>): E {
        return o;
    }
}
const checkUser = checked<{ username: string }>()

const getUser2 = (): { username: string } => {
    const foo = { username: 'foo', password: 'bar' }
    return checkUser(foo) //error
}
const getUser3 = (): { username: string } => {
    const foo = { username: 'foo' }
    return checkUser(foo) //ok
}

FWIW Dies ist eine WIP / Sketch-Tslint-Regel, die das spezifische Problem löst, nicht versehentlich zusätzliche Eigenschaften von "exponierten" Methoden zurückzugeben.

https://gist.github.com/spion/b89d1d2958f3d3142b2fe64fea5e4c32

Für den Anwendungsfall Verbreitung – siehe https://github.com/Microsoft/TypeScript/issues/12936#issuecomment -300382189 – könnte ein Linter ein solches Muster erkennen und warnen, dass es nicht typsicher ist?

Codebeispiel aus dem oben genannten Kommentar kopieren:

interface State {
   name: string;
}
function nameReducer(state: State, action: Action<string>): State {
   return {
       ...state,
       fullName: action.payload // compiles, but it's an programming mistake
   }
}

cc @JamesHenry / @armano2

Würde das sehr gerne sehen. Wir verwenden generierte TypeScript-Definitionen für GraphQL-Endpunkte und es ist ein Problem, dass TypeScript keinen Fehler auslöst, wenn ich ein Objekt mit mehr Feldern als nötig an eine Abfrage übergebe, da GraphQL eine solche Abfrage zur Laufzeit nicht ausführen kann.

Wie viel davon wird jetzt mit dem 3.5.1-Update behoben, mit einer besseren Überprüfung auf zusätzliche Eigenschaften während der Zuweisung? Wir haben eine Reihe bekannter Problembereiche als Fehler gekennzeichnet, so wie wir sie nach dem Upgrade auf 3.5.1 haben wollten

Wenn Sie ein Problem haben und denken, dass genaue Typen die richtige Lösung sind, beschreiben Sie bitte das ursprüngliche Problem hier

https://github.com/microsoft/TypeScript/issues/12936#issuecomment -284590083

Hier ist eine mit React-Refs: https://github.com/microsoft/TypeScript/issues/31798

/cc @RyanCavanaugh

Ein Anwendungsfall für mich ist

export const mapValues =
  <T extends Exact<T>, V>(object: T, mapper: (value: T[keyof T], key: keyof T) => V) => {
    type TResult = Exact<{ [K in keyof T]: V }>;
    const result: Partial<TResult> = { };
    for (const [key, value] of Object.entries(object)) {
      result[key] = mapper(value, key);
    }
    return result as TResult;
  };

Dies ist nicht korrekt, wenn wir keine genauen Typen verwenden, denn wenn object zusätzliche Eigenschaften hat, ist es nicht sicher, mapper für diese zusätzlichen Schlüssel und Werte aufzurufen.

Die wahre Motivation hier ist, dass ich die Werte für eine Aufzählung irgendwo haben möchte, die ich im Code wiederverwenden kann:

const choices = { choice0: true, choice1: true, choice2: true };
const callbacksForChoices = mapValues(choices, (_, choice) => () => this.props.callback(choice));

wobei this.props.callback den Typ (keyof typeof choices) => void .

Es geht also wirklich darum, dass das Typsystem die Tatsache darstellen kann, dass ich eine Liste von Schlüsseln im Codeland habe, die genau einer Menge (zB einer Vereinigung) von Schlüsseln im Typland entspricht, damit wir Funktionen schreiben können, die darauf operieren Liste der Schlüssel und machen gültige Typzusicherungen über das Ergebnis. Wir können kein Objekt verwenden ( choices in meinem vorherigen Beispiel), da das Codeland-Objekt, soweit das Typsystem weiß, zusätzliche Eigenschaften haben könnte, die über den verwendeten Objekttyp hinausgehen. Wir können kein Array ( ['choice0', 'choice1', 'choice2'] as const , da das Array, soweit das Typsystem weiß, möglicherweise nicht alle vom Arraytyp zulässigen Schlüssel enthält.

Vielleicht sollte exact kein Typ sein, sondern nur ein Modifikator für die Eingaben und/oder Ausgaben der Funktion? Etwas wie der Varianz-Modifikator von Flow ( + / - )

Ich möchte ergänzen, was @phaux gerade gesagt hat. Die eigentliche Verwendung von Exact besteht darin, dass der Compiler die Form der Funktionen garantiert. Wenn ich ein Framework habe, möchte ich vielleicht eines der folgenden: (T, S): AtMost<T> , (T, S): AtLeast<T> oder (T, S): Exact<T> wo der Compiler überprüfen kann, ob die von einem Benutzer definierten Funktionen genau passen.

Einige nützliche Beispiele:
AtMost ist nützlich für die Konfiguration (damit wir zusätzliche Parameter/Tippfehler nicht ignorieren und früh fehlschlagen).
AtLeast eignet sich hervorragend für Dinge wie Reaktionskomponenten und Middleware, bei denen ein Benutzer beliebige Extras auf ein Objekt schieben kann.
Exact ist nützlich für die Serialisierung/Deserialisierung (wir können garantieren, dass wir keine Daten verwerfen und diese isomorph sind).

Würde dies helfen, dies zu verhindern?

interface IDate {
  year: number;
  month: number;
  day: number;
}

type TBasicField = string | number | boolean | IDate;

 // how to make this generic stricter?
function doThingWithOnlyCorrectValues<T extends TBasicField>(basic: T): void {
  // ... do things with basic field of only the exactly correct structures
}

const notADate = {
  year: 2019,
  month: 8,
  day: 30,
  name: "James",
};

doThingWithOnlyCorrectValues(notADate); // <- this should not work! I want stricter type checking

Wir brauchen wirklich eine Möglichkeit in TS, T extends exactly { something: boolean; } ? xxx : yyy zu sagen.

Oder sonst so etwas wie:

const notExact = {
  something: true,
  name: "fred",
};

Werde dort immer noch xxx .

Vielleicht kann das Schlüsselwort const verwendet werden? zB T extends const { something: boolean }

@pleerock es könnte etwas mehrdeutig sein, da wir in JavaScript / TypeScript eine Variable als const aber trotzdem Objekteigenschaften hinzufügen / entfernen. Ich denke, das Schlüsselwort exact ist ziemlich auf den Punkt gebracht.

Ich bin mir nicht sicher, ob es genau zusammenhängt, aber ich würde in diesem Fall mindestens zwei Fehler erwarten:
Spielplatz
Screen Shot 2019-08-08 at 10 15 34

@mityok Ich denke, das hängt zusammen. Ich vermute, Sie möchten etwas in der Art tun:

class Animal {
  makeSound(): exact Foo {
     return { a: 5 };
  }
}

Wenn exact den Typ strenger gemacht hat - dann sollte er nicht mit einer zusätzlichen Eigenschaft erweiterbar sein, wie Sie es in Dog getan haben.

Nutzen Sie die Vorteile von const ( as const ) und verwenden Sie Vor-Schnittstellen und -Typen, wie

const type WillAcceptThisOnly = number

function f(accept: WillAcceptThisOnly) {
}

f(1) // error
f(1 as WillAcceptThisOnly) // ok, explicit typecast

const n: WillAcceptThisOnly = 1
f(n) // ok

Es wäre wirklich ausführlich, konstanten Variablen zuweisen zu müssen, würde aber viele Randfälle vermeiden, wenn Sie einen Typalias übergeben, der nicht genau Ihren Erwartungen entspricht

Ich habe eine reine TypeScript-Lösung für das Exact<T> Problem entwickelt, die sich meiner Meinung nach genau so verhält, wie im Hauptbeitrag gefordert:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

Der Grund, warum ExactInner nicht in den Exact enthalten sein darf, liegt daran, dass der Fix #32824 noch nicht veröffentlicht wurde (aber bereits in !32924 zusammengeführt wurde ).

Es ist nur möglich, dem Variablen- oder Funktionsargument vom Typ Exact<T> einen Wert zuzuweisen, wenn der rechte Ausdruck auch Exact<T> , wobei T in beiden Teilen genau der gleiche Typ ist der Aufgabe.

Ich habe keine automatische Hochstufung von Werten in Exact-Typen erreicht, also ist die exact() Hilfsfunktion dafür da. Jeder Wert kann zu einem exakten Typ heraufgestuft werden, die Zuweisung ist jedoch nur erfolgreich, wenn TypeScript beweisen kann, dass die zugrunde liegenden Typen beider Teile des Ausdrucks nicht nur erweiterbar, sondern genau gleich sind.

Es funktioniert, indem es die Tatsache ausnutzt, dass TypeScript die extend Relationsprüfung verwendet, um festzustellen, ob der Typ der rechten Hand dem Typ der linken Hand zugewiesen werden kann — dies ist nur möglich, wenn der Typ der rechten Hand (Quelle) _erweitert_ den Typ der linken Hand (Ziel). .

Zitat von checker.ts ,

// Zwei bedingte Typen 'T1 erweitert U1? X1 : Y1' und 'T2 erweitert U2 ? X2 : Y2' sind verwandt, wenn
// eines von T1 und T2 ist mit dem anderen verwandt, U1 und U2 sind identische Typen, X1 ist mit X2 verwandt,
// und Y1 ist mit Y2 verwandt.

ExactInner<T> generisch verwendet den beschriebenen Ansatz und ersetzt U1 und U2 durch zugrunde liegende Typen, die Genauigkeitsprüfungen erfordern. Exact<T> fügt eine Schnittmenge mit einem einfachen zugrunde liegenden Typ hinzu, wodurch TypeScript den exakten Typ lockern kann, wenn seine Zielvariable oder sein Funktionsargument kein exakter Typ sind.

Aus der Sicht des Programmierers verhält sich Exact<T> als würde es ein exact Flag auf T , ohne T oder zu ändern und ohne einen unabhängigen Typ zu erstellen.

Hier sind Spielplatz-Link und Gist-Link .

Eine mögliche zukünftige Verbesserung wäre die automatische Hochstufung von nicht genauen Typen in exakte Typen, wodurch die Funktion exact() vollständig überflüssig würde.

Tolle Arbeit @toriningen!

Wenn jemand einen Weg finden kann, dies zum Laufen zu bringen, ohne Ihren Wert in einen Aufruf von exact einschließen zu müssen, wäre es perfekt.

Ich bin mir nicht sicher, ob dies das richtige Problem ist, aber hier ist ein Beispiel für etwas, an dem ich gerne arbeiten würde.

https://www.typescriptlang.org/play/#code/KYOwrgtgBAyg9gJwC4BECWDgGMlriKAbwCgooBBAZyygF4oByAQ2oYBpSoVhq7GATHlgbEAvsWIAzMCBx4CTfvwDyCQQgBCATwAU -DNlz4AXFABE5GAGEzUAD7mUAUWtmAlEQnjiilWuCauvDI6Jhy + AB0VFgRSHAAqgAOiQFWLMA6bm4A3EA

enum SortDirection {
  Asc = 'asc',
  Desc = 'desc'
}
function addOrderBy(direction: "ASC" | "DESC") {}
addOrderBy(SortDirection.Asc.toUpperCase());

@lookfirst Das ist anders. Dies erfordert eine Funktion für Typen, die keine zusätzlichen Eigenschaften zulassen, wie etwa den Typ exact {foo: number} dem {foo: 1, bar: 2} nicht zuweisbar ist. Das verlangt nur nach Texttransformationen, die auf Enumerationswerte angewendet werden, die wahrscheinlich nicht vorhanden sind.

Ich bin mir nicht sicher, ob dies das richtige Thema ist, aber [...]

Nach meiner Erfahrung als Betreuer an anderer Stelle, wenn Sie Zweifel haben und kein klares bestehendes Problem finden konnten, melden Sie einen neuen Fehler und im schlimmsten Fall wird es als Duplikat geschlossen, das Sie nicht gefunden haben. Dies ist in den meisten großen Open-Source-JS-Projekten so ziemlich der Fall. (Die meisten von uns größeren Betreuern in der JS-Community sind eigentlich anständige Leute, nur Leute, die sich wirklich mit Fehlerberichten und dergleichen verzetteln können und daher ist es schwer, manchmal nicht wirklich knapp zu sein.)

@isiahmeadows Danke für die Antwort. Ich habe kein neues Problem eingereicht, weil ich zuerst nach doppelten Problemen gesucht habe, was die richtige Vorgehensweise ist. Ich habe versucht, die Leute nicht zu verzetteln, weil ich nicht sicher war, ob dies das richtige Thema war oder nicht oder wie ich das, worüber ich sprach, kategorisieren sollte.

BEARBEITET: Überprüfen Sie die @aigoncharov- Lösung unten , da ich denke, dass es noch schneller ist.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Weiß nicht ob das noch verbessert werden kann.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Ohne Kommentare

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

Weiß nicht ob das noch verbessert werden kann.

type Exact<T, Shape> =
    // Check if `T` is matching `Shape`
    T extends Shape
        // Does match
        // Check if `T` has same keys as `Shape`
        ? Exclude<keyof T, keyof Shape> extends never
            // `T` has same keys as `Shape`
            ? T
            // `T` has more keys than `Shape`
            : never
        // Does not match at all
        : never;

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)

Ohne Kommentare

type ExactKeys<T1, T2> = Exclude<keyof T1, keyof T2> extends never
    ? T1
    : never

type Exact<T, Shape> = T extends Shape
    ? ExactKeys<T, Shape>
    : never;

Liebe diese Idee!

Ein weiterer Trick, der die Aufgabe erfüllen könnte, besteht darin, die Zuweisbarkeit in beide Richtungen zu überprüfen.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: string
}
type B = {
  prop1: string
  prop2: string
}
type C = {
  prop1: string
}

type ShouldBeNever = Exact<A, B>
type ShouldBeA = Exact<A, C>

http://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAEEiUAN58w0gPZgAjKLrBpASyoBzRgF9l4aACFNOlnsMmoZyzd0GYABMpuZWtg4q0ADCbgFeoX4RjI6qAMoAFvoArgA2NM4QAHKSMprwyGhq2M54qdCZOfmFGsQVKKjVUNF1QkA

Ein weiterer Spielplatz von @iamandrewluca https://www.typescriptlang.org/play/?ssl=7&ssc=6&pln=7&pc=17#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEoglyyc4DyqAogrACYK0Q1yRt02-RdlfuAist8-NoB6ZagAEnhIFBhpaVNZQuAhxZagA

Eine Nuance ist hier, ob Exact<{ prop1: 'a' }> Exact<{ prop1: string }> zuweisbar sein soll. In meinen Anwendungsfällen sollte es.

@jeremybparagon Ihr Fall ist abgedeckt. Hier sind einige weitere Fälle.

type InexactType = {
    foo: 'foo'
}

const obj = {
    // here foo is infered as `string`
    // and will error because `string` is not assignable to `"foo"`
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj) // $ExpectError
type InexactType = {
    foo: 'foo'
}

const obj = {
    // here we cast to `"foo"` type
    // and will not error
    foo: 'foo' as 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}
function test2(t: InexactType) {}

test1(obj) // $ExpectError
test2(obj)
type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

Ich denke, jeder, der diesen Trick anwendet (und ich sage nicht, dass es keine gültigen Verwendungen dafür gibt), sollte sich bewusst sein, dass es sehr, sehr einfach ist, mehr Requisiten in der "genauen" Form zu erhalten. Da InexactType Exact<T, InexactType> zuweisbar ist, wenn Sie so etwas haben, brechen Sie aus der Genauigkeit aus, ohne es zu merken:

function test1<T>(t: Exact<T, InexactType>) {}

function test2(t: InexactType) {
  test1(t); // inexactType assigned to exact type
}
test2(obj) // but 

Spielplatz-Link

Dies ist der Grund (zumindest einer von ihnen), dass TS keine exakten Typen hat, da es eine vollständige Verzweigung von Objekttypen in exakte vs zum Nennwert sind sie kompatibel. Der ungenaue Typ kann immer mehr Eigenschaften enthalten. (Zumindest war dies einer der Gründe, warum @ahejlsberg als tsconf erwähnt wurde).

Wenn asExact eine syntaktische Möglichkeit wäre, ein so genaues Objekt zu markieren, könnte eine solche Lösung wie folgt aussehen:

declare const exactMarker: unique symbol 
type IsExact = { [exactMarker]: undefined }
type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

type InexactType = {
    foo: string
}
function asExact<T>(o: T): T & IsExact { 
  return o as T & IsExact;
}

const obj = asExact({
  foo: 'foo',
});


function test1<T extends IsExact & InexactType>(t: Exact<T, InexactType>) {

}

function test2(t: InexactType) {
  test1(t); // error now
}
test2(obj) 
test1(obj);  // ok 

const obj2 = asExact({
  foo: 'foo',
  bar: ""
});
test1(obj2);

const objOpt = asExact < { foo: string, bar?: string }>({
  foo: 'foo',
  bar: ""
});
test1(objOpt);

Spielplatz-Link

@dragomirtitian deshalb habe ich etwas früher die Lösung gefunden https://github.com/microsoft/TypeScript/issues/12936#issuecomment -524631270 die nicht darunter leidet.

@dragomirtitian es kommt darauf an, wie Sie Ihre Funktionen
Wenn Sie es etwas anders machen, funktioniert es.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type InexactType = {
    foo: string
}

const obj = {
    foo: 'foo',
    bar: 'bar'
}

function test1<T>(t: Exact<T, InexactType>) {}

function test2<T extends InexactType>(t: T) {
  test1(t); // fails
}
test2(obj)

https://www.typescriptlang.org/play/#code/C4TwDgpgBAogHgQwMbADwBUA0UBKA + KAXinSgjmAgDsATAZ1wCgooB + XMi6 + k5lt3vygAuKFQgA3CACc + o8VNmNQkKAElxiFOnDRiAbz4sAZgHtTousGkBLKgHNGAX0aMkpqlaimARgCsiKEMhMwsoAHJQ8MwjKB8EaVFw + Olw51djAFcqFBsPKEorAEYMPAAKYFF4ZDQsdU0anUg8AEogl0YsnOA8qgKIKwAmDE5KWgYNckbdcsqSNuD + 4oqWgG4oAHoNqGMEGwAbOnTC4EGy3z82oA

@jeremybparagon Ihr Fall ist abgedeckt.

@iamandrewluca Ich denke, die Lösungen hier und hier unterscheiden sich darin, wie sie mein Beispiel behandeln.

type Exact<T, R> = T extends R
  ? R extends T
    ? T
    : never
  : never

type A = {
  prop1: 'a'
}
type C = {
  prop1: string
}

type ShouldBeA = Exact<A, C> // This evaluates to never.

const ob...

Spielplatz-Link

@aigoncharov Das Problem ist, dass Sie sich dessen bewusst sein müssen, damit man dies leicht nicht tun kann und test1 immer noch mit zusätzlichen Eigenschaften aufgerufen werden könnte. IMO ist jede Lösung, die so leicht eine versehentliche ungenaue Zuordnung zulassen kann, bereits gescheitert, da es darum geht, Genauigkeit im Typsystem durchzusetzen.

@toriningen ja, deine Lösung scheint besser zu sein, ich habe mich nur auf die zuletzt gepostete Lösung bezogen. Ihre Lösung spricht dafür, dass Sie den zusätzlichen Funktionstypparameter nicht benötigen, jedoch scheint er für optionale Eigenschaften nicht gut zu funktionieren:

// (these two types MUST NOT be merged into a single declaration)
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
type Unexact<T> = T extends Exact<infer R> ? R : T;

function exact<T>(obj: Exact<T> | T): Exact<T> {
    return obj as Exact<T>;
};

////////////////////////////////
// Fixtures:
type Wide = { foo: string, bar?: string };
type Narrow = { foo: string };
type ExactWide = Exact<Wide>;
type ExactNarrow = Exact<Narrow>;

const ew: ExactWide = exact<Wide>({ foo: "", bar: ""});
const assign_en_ew: ExactNarrow = ew; // Ok ? 

Spielplatz-Link

@jeremybparagon Ich bin mir nicht sicher, ob die Lösung von T extends S und S extends T basiert, leidet unter der einfachen Tatsache, dass

type A = { prop1: string }
type C = { prop1: string,  prop2?: string }
type CextendsA = C extends A ? "Y" : "N" // Y 
type AextendsC = A extends C ? "Y" : "N" // also Y 

Spielplatz-Link

Ich denke, @iamandrewluca mit Exclude<keyof T, keyof Shape> extends never ist gut, mein Typ ist ziemlich ähnlich (ich habe meine ursprüngliche Antwort bearbeitet, um die &R hinzuzufügen, um sicherzustellen, dass T extends R ohne zusätzliche Überprüfungen erforderlich ist).

type Exact<T extends IsExact & R, R> =
  Exclude<keyof T, typeof exactMarker> extends keyof R? T : never;

Ich würde meinen Ruf nicht aufs Spiel setzen, dass meine Lösung keine Löcher hat, ich habe nicht so genau danach gesucht, aber solche Erkenntnisse begrüße ich sehr

Wir sollten ein Flag haben, bei dem dies global aktiviert ist. Auf diese Weise kann jeder, der Typ verlieren möchte, dasselbe tun. Viel zu viele Fehler, die durch dieses Problem verursacht wurden. Jetzt versuche ich, den Spread-Operator zu vermeiden und verwende pickKeysFromObject(shipDataRequest, ['a', 'b','c'])

Hier ist ein Anwendungsfall für genaue Typen, über die ich kürzlich gestolpert bin:

type PossibleKeys = 'x' | 'y' | 'z';
type ImmutableMap = Readonly<{ [K in PossibleKeys]?: string }>;

const getFriendlyNameForKey = (key: PossibleKeys) => {
    switch (key) {
        case 'x':
            return 'Ecks';
        case 'y':
            return 'Why';
        case 'z':
            return 'Zee';
    }
};

const myMap: ImmutableMap = { x: 'foo', y: 'bar' };

const renderMap = (map: ImmutableMap) =>
    Object.keys(map).map(key => {
        // Argument of type 'string' is not assignable to parameter of type 'PossibleKeys'
        const friendlyName = getFriendlyNameForKey(key);
        // No index signature with a parameter of type 'string' was found on type 'Readonly<{ x?: string | undefined; y?: string | undefined; z?: string | undefined; }>'.    
        return [friendlyName, map[key]];
    });
;

Da Typen standardmäßig ungenau sind, muss Object.keys ein string[] (siehe https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208), aber in diesem Fall , wenn ImmutableMap genau war, gibt es keinen Grund, warum PossibleKeys[] .

@dallonf Beachten Sie, dass dieses Beispiel neben genauen Typen zusätzliche Funktionen erfordert -- Object.keys ist nur eine Funktion und es müsste einen Mechanismus geben, um eine Funktion zu beschreiben, die keyof T für genaue Typen zurückgibt und string für andere Typen. Die einfache Möglichkeit, einen genauen Typ zu deklarieren, würde nicht ausreichen.

@RyanCavanaugh Ich denke, das war die Implikation, genaue Typen + die Fähigkeit, sie zu erkennen.

Anwendungsfall für die Reaktionstypisierungen:

forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P> .

Es ist verlockend, eine reguläre Komponente an forwardRef , weshalb React Laufzeitwarnungen ausgibt, wenn propTypes oder defaultProps im render Argument erkannt wird. Wir möchten dies auf Typebene ausdrücken, müssen aber auf never zurückgreifen:

- forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string }) => ComponentType<P>
+ forwardRef<T, P>(render: (props: P, ref: Ref<T>) => ReactElement<P> & { displayName?: string, propTypes?: never, defaultProps?: never }) => ComponentType<P>

Die Fehlermeldung mit never ist nicht hilfreich ("{} ist nicht zuordenbar zu undefined").

Kann mir jemand helfen, wie die Lösung von @toriningen mit einer Vereinigung verschiedener Ereignisobjektformen aussehen würde? Ich möchte meine Ereignisformen in Redux-Dispatch-Aufrufen einschränken, zB:

type StoreEvent =
  | { type: 'STORE_LOADING' }
  | { type: 'STORE_LOADED'; data: unknown[] }

Es ist unklar, wie ich eine typisierte Dispatch()-Funktion erstellen könnte, die nur die genaue Form eines Ereignisses akzeptiert.

(UPDATE: Ich habe es herausgefunden: https://gist.github.com/sarimarton/d5d539f8029c01ca1c357aba27139010)

Anwendungsfall:

Fehlende Exact<> Unterstützung führt zu Laufzeitproblemen mit GraphQL-Mutationen. GraphQL akzeptiert eine genaue Liste der zulässigen Eigenschaften. Wenn Sie übermäßig viele Requisiten bereitstellen, wird ein Fehler ausgegeben.

Wenn wir also einige Daten aus dem Formular abrufen, kann Typescript überzählige (zusätzliche) Eigenschaften nicht validieren. Und wir erhalten einen Fehler zur Laufzeit.

Das folgende Beispiel veranschaulicht imaginäre Sicherheit

  • im ersten Fall alle Eingabeparameter verfolgt
  • aber im wirklichen Leben (wie im zweiten Fall, als wir Daten aus dem Formular erhalten und in einer Variablen speichern) Typskript

Im Playground ausprobieren

Screen Shot 2020-03-05 at 13 04 38

Gemäß dem Artikel https://fettblog.eu/typescript-match-the-exact-object-shape/ und ähnlichen oben bereitgestellten Lösungen können wir die folgende hässliche Lösung verwenden:

Screen Shot 2020-03-05 at 12 26 57

Warum ist diese savePerson<T>(person: ValidateShape<T, Person>) Lösung hässlich?

Angenommen, Sie haben einen tief verschachtelten Eingabetyp, z. B.:

// Assume we are in the ideal world where implemented Exact<>

type Person {
  name: string;
  address: Exact<Address>;
}

type Address {
   city: string
   location: Exact<Location>
}

type Location {
   lon: number;
   lat: number; 
}

savePerson(person: Exact<Person>)

Ich kann mir nicht vorstellen, welche Spaghetti wir schreiben sollten, um mit der derzeit verfügbaren Lösung das gleiche Verhalten zu erzielen:

savePerson<T, TT, TTT>(person: 
  ValidateShape<T, Person keyof ...🤯...
     ValidateShape<TT, Address keyof ...💩... 
         ValidateShape<TTT, Location keyof ...🤬... 
> > >)

Im Moment haben wir also große Lücken in der statischen Analyse in unserem Code, die mit komplexen verschachtelten Eingabedaten arbeitet.

Der im ersten Bild beschriebene Fall, in dem TS überschüssige Eigenschaften nicht validiert, weil "Frische" verloren geht, war für uns auch ein kleiner Schmerzpunkt.

Schreiben

doSomething({
  /* large object of options */
})

fühlt sich oft viel weniger lesbar an als

const options = {
  /* large object of options */
}
doSomething(options)

Das explizite Kommentieren von const options: DoSomethingOptions = { hilft, aber es ist etwas umständlich und bei Code-Reviews schwer zu erkennen und durchzusetzen.

Dies ist eine Offtopic-Idee und würde die meisten der hier beschriebenen Anwendungsfälle für die Genauigkeit nicht lösen, aber wäre es möglich, ein Objektliteral frisch zu halten, wenn es nur einmal innerhalb des umschließenden Bereichs verwendet wird?

@RyanCavanaugh vielen Dank für die Erklärung von EPC ... wird der Unterschied zwischen EPC und genauen Typen irgendwo ausführlicher diskutiert? Jetzt habe ich das Gefühl, dass ich besser verstehen sollte, warum EPC einige Fälle zulässt, die bei genauen Typen nicht möglich sind.

Hallo @noppa , das wäre eine tolle Idee. Ich bin gerade darüber gestolpert, als ich den Unterschied zwischen der direkten Zuweisung und der Zuweisung einer Variablen zuerst bemerkt habe - sogar eine Frage zu SO gestellt , die mich hierher gebracht hat. Das aktuelle Verhalten überrascht zumindest für mich...

Ich glaube, ich habe das gleiche Problem wie das Beispiel der GraphQL-Mutationen (genaue verschachtelte Typisierung, keine zusätzlichen Eigenschaften sollten erlaubt sein). In meinem Fall denke ich daran, API-Antworten in ein gemeinsames Modul einzugeben (das zwischen Frontend und Backend geteilt wird):

export type ProductsSlashResponse = {
  products: Array<{
    id: number;
    description: string;
  }>,
  total: number;
};

Auf der Serverseite möchte ich sicherstellen, dass die Antwort diese Typsignatur respektiert:

router.get("products/", async () =>
  assertType<ProductsSlashResponse>(getProducts())));

Ich habe Lösungen von hier versucht. Eine, die zu funktionieren scheint, ist T extends U ? U extends T ? T : never : never , zusammen mit einer Curry-Funktion, die nicht ideal ist. Das Hauptproblem dabei ist, dass Sie kein Feedback zu fehlenden oder zusätzlichen Eigenschaften erhalten (vielleicht könnten wir das verbessern, aber es wird schwierig, wenn wir in verschachtelte Eigenschaften geraten). Andere Lösungen funktionieren nicht mit tief verschachtelten Objekten.

Natürlich stürzt das Frontend normalerweise nicht ab, wenn ich mehr Informationen sende als angegeben. Dies kann jedoch zu einem Informationsleck führen, wenn die API mehr Informationen sendet, als sie sollte (und aufgrund der nebligen Natur des Lesens von Daten aus einer Datenbank) welche Typen nicht unbedingt immer mit dem Code synchron sind, kann dies passieren).

@fer22f GraphQL sendet keine Felder, die der Client nicht angefordert hat ... es sei denn, Sie verwenden einen JSON-Skalartyp für products oder für die Array-Elemente, worüber Sie sich keine Sorgen machen müssen.

Entschuldigung, ich habe mich falsch verstanden, ich dachte, du meinst, du verwendest GraphQL

Jemand hat GraphQL bereits erwähnt, aber nur in Bezug auf das "Sammeln von Anwendungsfällen" ( @DanielRosenwasser erwähnte vor einigen Jahren im Thread :-) von "keine Anwendungsfälle aus der Hand zu haben"), zwei Anwendungsfälle, bei denen ich das wollte verwenden Exact sind:

  1. Übergabe von Daten an Datenspeicher/Datenbanken/ORMs--alle zusätzlichen Felder, die übergeben werden, werden stillschweigend verworfen/nicht gespeichert.

  2. Weitergabe von Daten an Drahtanrufe / RPCs / REST / GraphQL - auch hier werden alle zusätzlichen Felder, die übergeben werden, stillschweigend verworfen / nicht gesendet.

(Nun, vielleicht nicht stillschweigend, sie können Laufzeitfehler sein.)

In beiden Fällen möchte ich dem Programmierer/mir selbst (über einen Kompilierungsfehler) sagen: "...Sie sollten mir diese zusätzliche Eigenschaft wirklich nicht geben, b/c, wenn Sie erwarten, dass sie 'gespeichert' wird oder ' gesendet', wird es nicht sein".

Dies wird insbesondere in APIs im Stil von "partiellen Aktualisierungen" benötigt, dh schwachen Typen:

type Data = { firstName:? string; lastName?: string; children?: [{ ... }] };
const data = { firstName: "a", lastNmeTypo: "b" };
await saveDataToDbOrWireCall(data);

Besteht die Prüfung auf schwachen Typ b/c mit mindestens einem übereinstimmenden Parameter, firstName , ist also nicht zu 100% unzusammenhängend, aber es gibt immer noch einen "offensichtlichen" Tippfehler von lsatNmeTypo , der nicht erwischt wird.

Zugegeben, EPC funktioniert, wenn ich Folgendes tue:

await saveDataToDbOrWireCall({ firstName, lastNmeTypo });

Aber jedes Feld destrukturieren + neu eingeben zu müssen, ist ziemlich mühsam.

Lösungen wie Exactify @jcalz funktionieren auf der ersten Ebene, aber der rekursive Fall (dh children ist ein Array und die Array-Elemente sollten genau sein) habe ich Probleme, sobald es zutrifft Anwendungsfälle aus der "realen Welt" mit Generika / wie Exact<Foo<Bar<T>> .

Es wäre großartig, dies integriert zu haben, und wollte nur diese expliziten Anwendungsfälle (im Grunde Drahtaufrufe mit partiellen / schwachen Typen) beachten, wenn dies bei der Priorisierung / Roadmapping hilft.

(FWIW https://github.com/stephenh/joist-ts/pull/35/files hat meinen aktuellen Versuch eines tiefen Exact und auch ein Exact.test.ts , das triviale Fälle passiert, aber die PR selbst hat Kompilierungsfehler bei den eher esoterischen Verwendungen.Haftungsausschluss Ich erwarte nicht wirklich, dass sich jemand mit dieser speziellen PR befasst, sondern gebe sie nur als "Hier wäre Exact nützlich" + "AFAICT Dies ist im Datenpunkt "Benutzerland" schwer zu tun.)

Hey,

Haben Sie sich gefragt, was das TS-Team zu den Vorschlägen für genaue Typendatensätze und Tupel denkt? https://github.com/tc39/proposal-record-tuple

Ist es sinnvoll, für diese neuen Primitive exakte Typen einzuführen?

@slorber Nicht TS, aber das ist orthogonal. Dieser Vorschlag betrifft die Unveränderlichkeit, und die Bedenken sind bei Bibliotheken wie Immutable.js fast identisch.

Ich habe die rekursive Version von

export type Exact<Expected, Actual> = Expected &
  Actual & // Needed to infer `Actual`
  (null extends Actual
    ? null extends Expected
      ? Actual extends null // If only null stop here, because NonNullable<null> = never
        ? null
        : CheckUndefined<Expected, Actual>
      : never // Actual can be null but not Expected: forbid the field
    : CheckUndefined<Expected, Actual>);

type CheckUndefined<Expected, Actual> = undefined extends Actual
  ? undefined extends Expected
    ? Actual extends undefined // If only undefined stop here, because NonNullable<undefined> = never
      ? undefined
      : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>
    : never // Actual can be undefined but not Expected: forbid the field
  : NonNullableExact<NonNullable<Expected>, NonNullable<Actual>>;

type NonNullableExact<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Actual[K] extends (infer ActualElement)[]
      ? Expected[K] extends (infer ExpectedElement)[] | undefined | null
        ? Exact<ExpectedElement, ActualElement>[]
        : never // Not both array
      : Exact<Expected[K], Actual[K]>
    : never; // Forbid extra properties
};

Spielplatz

Exact wäre für uns bei der Rückgabe von API-Antworten sehr nützlich. Derzeit beschließen wir Folgendes:

const response = { companies };

res.json(exact<GetCompaniesResponse, typeof response>(response));
export function exact<S, T>(object: Exact<S, T>) {
  return object;
}

Hier ist der Typ Exact was @ArnaudBarre oben angegeben hat.

Danke @ArnaudBarre, dass du mich entsperrt und mir einige ts beigebracht hast.
Riffing an deiner Lösung:

export type Exact<Expected, Actual> =
  keyof Expected extends keyof Actual
    ? keyof Actual extends keyof Expected
      ? Expected extends ExactElements<Expected, Actual>
        ? Expected
        : never
      : never
    : never;

type ExactElements<Expected, Actual> = {
  [K in keyof Actual]: K extends keyof Expected
    ? Expected[K] extends Actual[K]
      ? Actual[K] extends Expected[K]
        ? Expected[K]
        : never
      : never
    : never
};

// should succeed (produce exactly the Expected type)
let s1: Exact< { a: number; b: string }, { a: number; b: string } >;
let s2: Exact< { a?: number; b: string }, { a?: number; b: string } >;
let s3: Exact< { a?: number[]; b: string }, { a?: number[]; b: string } >;
let s4: Exact< string, string >;
let s5: Exact< string[], string[] >;
let s6: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string }[] >;

// should fail (produce never)
let f1: Exact< { a: string; b: string }, { a: number; b: string } >;
let f2: Exact< { a: number; b: string }, { a?: number; b: string } >;
let f3: Exact< { a?: number; b: string }, { a: number; b: string } >;
let f4: Exact< { a: number[]; b: string }, { a: string[]; b: string } >;
let f5: Exact< { a?: number[]; b: string }, { a: number[]; b: string } >;
let f6: Exact< { a?: number; b: string; c: string }, { a?: number; b: string } >;
let f7: Exact< { a?: number; b: string }, { a?: number; b: string; c: string } >;
let f8: Exact< { a?: number; b: string; c?: string }, { a?: number; b: string } >;
let f9: Exact< { a?: number; b: string }, { a?: number; b: string; c?: string } >;
let f10: Exact< never, string >;
let f11: Exact< string, never >;
let f12: Exact< string, number >;
let f13: Exact< string[], string >;
let f14: Exact< string, string[] >;
let f15: Exact< string[], number[] >;
let f16: Exact< { a?: number[]; b: string }[], { a?: number[]; b: string } >;

Die vorherige Lösung war für f6, f8 und f9 'erfolgreich'.
Diese Lösung liefert auch „sauberere“ Ergebnisse; bei Übereinstimmung erhalten Sie den Typ 'Erwartet' zurück.
Wie bei @ ArnaudBarres Kommentar ... nicht sicher, ob alle Randfälle behandelt werden, also ymmv ...

@heystewart Dein Exact ergibt kein symmetrisches Ergebnis:

let a: Exact< { foo: number }[], { foo: number, bar?: string }[] >;
let b: Exact< { foo: number, bar?: string }[], { foo: number }[] >;

a = [{ foo: 123, bar: 'bar' }]; // error
b = [{ foo: 123, bar: 'bar' }]; // no error

Bearbeiten: Die Version von @ ArnaudBarre hat auch das gleiche Problem

@papb Ja, effektiv funktioniert meine Eingabe nicht, variables immer ein Objekt ist.

Um es zu lösen, müssen Sie ExactObject und ExactArray isolieren und einen Einstiegspunkt haben, der in das eine oder andere geht.

Was also ist der beste Weg, um sicherzustellen, dass ein Objekt genaue Eigenschaften hat, nicht weniger und nicht mehr?

@captain-yossarian überzeugt das TypeScript-Team, dies zu implementieren. Keine hier vorgestellte Lösung funktioniert für alle erwarteten Fälle, und fast allen fehlt es an Klarheit.

@toriningen kann sich nicht vorstellen, wie viele Probleme geschlossen werden, wenn das TS-Team diese Funktion implementiert

@RyanCavanaugh
Derzeit habe ich einen Anwendungsfall, der mich hierher geführt hat und der direkt in Ihr Thema „Sonstiges“ passt. Ich möchte eine Funktion, die:

  1. nimmt einen Parameter, der eine Schnittstelle mit optionalen Parametern implementiert
  2. gibt ein Objekt zurück, das in die engere tatsächliche Schnittstelle des angegebenen Parameters typisiert ist, so dass

Diese unmittelbaren Ziele dienen diesen Zwecken:

  1. Ich erhalte eine übermäßige Eigenschaftsprüfung für die Eingabe
  2. Ich erhalte Autovervollständigung und Eigenschaftstypsicherheit für die Ausgabe

Beispiel

Ich habe meinen Fall auf folgendes reduziert:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function y<
    Y extends X
>(
    y: (X extends Y ? Y : X)
) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

Dieses Setup funktioniert und erreicht alle gewünschten Ziele, also aus reiner Machbarkeitssicht und soweit es geht, bin ich gut. EPC wird jedoch durch die Parametereingabe (X extends Y ? Y : X) . Ich bin im Grunde zufällig darüber gestolpert und war etwas überrascht, dass es funktioniert hat.

Vorschlag

Und deshalb möchte ich ein implements Schlüsselwort haben, das anstelle von extends werden kann, um die Absicht zu kennzeichnen, dass der Typ hier keine überflüssigen Eigenschaften haben soll. Wie so:

type X = {
    red?: number,
    green?: number,
    blue?: number,
}

function x<
    Y implements X
>( y: Y ) {
    if ((y as any).purple) throw Error('bla')

    return y as Y
}

const z = y({
    blue: 1,
    red: 3,
    purple: 4, // error
})
z.green // error

type Z = typeof z

Dies scheint mir viel klarer zu sein als mein derzeitiger Workaround. Abgesehen davon, dass es prägnanter ist, lokalisiert es die gesamte Einschränkung mit der Generics-Deklaration im Gegensatz zu meiner aktuellen Aufteilung zwischen den Generics und den Parametern.

Das kann auch weitere Anwendungsfälle ermöglichen, die derzeit unmöglich oder unpraktisch sind, aber das ist derzeit nur ein Bauchgefühl.

Schwache Typerkennung als Alternative

Insbesondere sollte die Schwache Typerkennung gemäß #3842 dies ebenfalls beheben und könnte vorteilhaft sein, da keine zusätzliche Syntax erforderlich ist, wenn sie in Verbindung mit extends funktioniert, wie in meinem Anwendungsfall.

Bezüglich Exact<Type> usw.

Schließlich sollte implements , wie ich es mir vorstelle, in Bezug auf Ihren Standpunkt zu function f<T extends Exact<{ n: number }>(p: T) ziemlich einfach sein, da es nicht versucht, den allgemeineren Fall von Exact<Type> zu lösen.

Im Allgemeinen scheint Exact<Type> neben EPC von eher geringem Nutzen zu sein, und ich kann mir keinen gültigen allgemein nützlichen Fall vorstellen, der außerhalb dieser Gruppen liegt:

  • Funktionsaufrufe: diese können jetzt wie in meinem Beispiel leicht gehandhabt werden und würden von implements profitieren
  • Zuweisungen: nur Literale verwenden, damit EPC gilt
  • Daten von außerhalb Ihres Kontrollbereichs: Typprüfung kann Sie nicht davor schützen, Sie müssen das zur Laufzeit handhaben, und an diesem Punkt sind Sie wieder bei sicheren Umwandlungen

Natürlich wird es Fälle geben, in denen Sie keine Literale zuweisen können, aber diese sollten ebenfalls von einer endlichen Menge sein:

  • Wenn Sie die Zuweisungsdaten in einer Funktion erhalten, führen Sie die Typprüfung in der Rufsignatur durch
  • Wenn Sie gemäß OP mehrere Objekte zusammenführen, bestätigen Sie den Typ jedes Quellobjekts richtig und Sie können as DesiredType sicher umsetzen

Zusammenfassung: implements wäre schön aber sonst sind wir gut

Zusammenfassend bin ich zuversichtlich, dass mit implements und der Behebung von EPC (falls Probleme auftreten) wirklich mit genauen Typen umgegangen werden sollte.

Frage an alle Interessierten: Ist hier eigentlich was offen?

Nachdem ich die Anwendungsfälle hier durchgesehen habe, denke ich, dass mittlerweile fast alle Repros richtig gehandhabt werden, und der Rest kann mit meinem kleinen Beispiel oben zum Laufen gebracht werden. Da stellt sich die Frage: Hat das heute noch jemand mit aktuellen TS?

Ich habe eine unausgereifte Idee zu Typanmerkungen. Die Übereinstimmung eines Objekts ist in Elemente unterteilt kann genau gleich sein, nicht mehr und nicht weniger, mehr oder weniger, nicht mehr aber weniger, mehr aber nicht weniger. Für jeden der oben genannten Fälle sollte es einen Ausdruck geben.

genau gleich, also nicht mehr und nicht weniger:

function foo(p:{|x:any,y:any|})

//it matched 
foo({x,y})
//no match
foo({x})
foo({y})
foo({x,y,z})
foo({})

mehr aber nicht weniger:

function foo(p:{|x:any,y:any, ...|})

//it matched 
foo({x,y})
foo({x,y,z})

//no matched
foo({x})
foo({y})
foo({x,z})

nicht mehr aber weniger:

function foo(p:{x:any,y:any})

//it matched 
foo({x,y})
foo({x})
foo({y})

//no match
foo({x,z})
foo({x,y,z})

mehr oder weniger:

function foo(p:{x:any,y:any, ...})

//it matched 
foo({x,y})
foo({x})
foo({y})
foo({x,z})
foo({x,y,z})

Fazit:

Mit einer vertikalen Linie bedeutet, dass es nicht weniger geben kann, ohne eine vertikale Linie bedeutet, dass es weniger geben kann. Mit einem Auslassungszeichen bedeutet, dass es mehr geben kann, ohne ein Auslassungszeichen bedeutet, dass es nicht mehr geben kann. Arrays match ist die gleiche Idee.

function foo(p:[|x,y|]) // p.length === 2
function foo(p:[|x,y, ... |]) // p.length >= 2
function foo(p:[x,y]) // p.length >= 0
function foo(p:[x,y,...]) // p.length >= 0

@rasenplanscher anhand Ihres Beispiels kompiliert dies:

const x = { blue: 1, red: 3, purple: 4 };
const z = y(x);

Bei genauen Typen sollte dies jedoch nicht der Fall sein. Dh die Bitte hier ist, nicht von EPC abhängig zu sein.

@xp44mm "mehr, aber nicht weniger" ist bereits das Verhalten und "mehr oder weniger" ist das Verhalten, wenn Sie alle Eigenschaften als optional markieren

function foo(p:{x?: any, y?: any}) {}
const x = 1, y = 1, z = 1
// all pass
foo({x,y})
foo({x})
foo({y})
const p1 = {x,z}
foo(p1)
const p2 = {x,y,z}
foo(p2)

Wenn wir exakte Typen hätten, wäre exakter Typ + alle optionalen Eigenschaften im Wesentlichen "nicht mehr, aber weniger" .

Ein weiteres Beispiel zu diesem Thema. Eine gute Demonstration für diesen Vorschlag, denke ich. In diesem Fall verwende ich rxjs , um mit dem Subjekt zu arbeiten, möchte aber ein ("gesperrtes") Observable zurückgeben (das keine Methode next , error usw. hat, um die Wert.)

someMethod(): Observable<MyType> {
  const subject = new Subject<MyType>();

  // This works, but should not. (if this proposal is implemented.)
  return subject;

  // Only Observable should be allowed as return type.
  return subject.asObservable();
}

Ich möchte immer nur den genauen Typ Observable und nicht Subject das ihn erweitert.

Vorschlag:

// Adding exclamation mark `!` (or something else) to match exact type. (or some other position `method(): !Foo`, ...)
someMethod()!: Observable<MyType> {
  // ...
}

Aber du hast sicher bessere Ideen. Vor allem, weil dies nicht nur Rückgabewerte betrifft, oder? Wie auch immer, nur eine Pseudocode-Demo. Ich denke, das wäre ein nettes Feature, um Fehler und Mängel zu vermeiden. Wie im oben beschriebenen Fall. Eine andere Lösung könnte darin bestehen, einen neuen Dienstprogrammtyp hinzuzufügen.
Oder habe ich etwas übersehen? Funktioniert das schon? Ich verwende TypeScript 4.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen