Typescript: Vorschlag: Typ Eigenschaftstyp

Erstellt am 28. Nov. 2014  ·  76Kommentare  ·  Quelle: microsoft/TypeScript

Motivationen

Viele JavaScript-Bibliotheken / Frameworks / Muster umfassen Berechnungen basierend auf dem Eigenschaftsnamen eines Objekts. Zum Beispiel basieren das Backbone- Modell, die funktionale Transformation pluck und ImmutableJS alle auf einem solchen Mechanismus.

//backbone
var Contact = Backbone.Model.extend({})
var contact = new Contact();
contact.get('name');
contact.set('age', 21);

// ImmutableJS
var map = Immutable.Map({ name: 'François', age: 20 });
map = map.set('age', 21);
map.get('age'); // 21

//pluck
var arr = [{ name: 'François' }, { name: 'Fabien' }];
_.pluck(arr, 'name') // ['François', 'Fabien'];

In diesen Beispielen können wir die Beziehung zwischen der API und der zugrunde liegenden Typbeschränkung leicht verstehen.
Im Fall des Backbone-Modells ist es nur eine Art _proxy_ für ein Objekt vom Typ:

interface Contact {
  name: string;
  age: number;
}

Für den Fall von pluck ist es eine Transformation

T[] => U[]

Dabei ist U der Typ einer Eigenschaft von T prop .

Wir haben jedoch keine Möglichkeit, eine solche Beziehung in TypeScript auszudrücken, und erhalten einen dynamischen Typ.

Vorgeschlagene Lösung

Die vorgeschlagene Lösung besteht darin, eine neue Syntax für den Typ T[prop] einzuführen, wobei prop ein Argument der Funktion ist, das einen Typ wie einen Rückgabewert oder einen Typparameter verwendet.
Mit dieser neuen Typensyntax könnten wir die folgende Definition schreiben:

declare module Backbone {

  class Model<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): void;
  }
}

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[prop];
    set(prop: string, value: T[prop]): Map<T>;
  }
}

declare function pluck<T>(arr: T[], prop: string): Array<T[prop]>  // or T[prop][] 

Auf diese Weise kann TypeScript bei Verwendung unseres Backbone-Modells den Aufruf von get und set korrekt tippen.

interface Contact {
  name: string;
  age: number;
}
var contact: Backbone.Model<Contact>;

var age = contact.get('age');
contact.set('name', 3) /// error

Die Konstante prop

Zwang

Offensichtlich muss die Konstante von einem Typ sein, der als Indextyp verwendet werden kann ( string , number , Symbol ).

Fall von indexierbar

Werfen wir einen Blick auf unsere Map Definition:

declare module ImmutableJS {
  class Map<T> {
    get(prop: string): T[string];
    set(prop: string, value: T[string]): Map<T>;
  }
}

Wenn T indizierbar ist, erbt unsere Karte dieses Verhalten:

var map = new ImmutableJS.Map<{ [index: string]: number}>;

Jetzt hat get für Typ get(prop: string): number .

Verhör

Nun, es gibt einige Fälle, in denen ich Schmerzen habe, wenn ich an ein _korrektes_ Verhalten denke. Beginnen wir noch einmal mit unserer Definition von Map .
Wenn wir nicht { [index: string]: number } als Typparameter übergeben würden, hätten wir angegeben
{ [index: number]: number } Sollte der Compiler einen Fehler auslösen?

Wenn wir pluck mit einem dynamischen Ausdruck für prop anstelle einer Konstante verwenden:

var contactArray: Contact[] = []
function pluckContactArray(prop: string) {
  return _.pluck(myArray, prop);
}

oder mit einer Konstante, die keine Eigenschaft des als Parameter übergebenen Typs ist.
Sollte der Aufruf von pluck einen Fehler auslösen, da der Compiler nicht auf den Typ T[prop] , sollte T[prop] in {} oder any , wenn ja, sollte der Compiler mit --noImplicitAny einen Fehler auslösen?

Fixed Suggestion help wanted

Hilfreichster Kommentar

@weswigham @mhegazy , und ich habe dies kürzlich diskutiert; Wir werden Sie über alle Entwicklungen informieren, auf die wir stoßen, und bedenken, dass dies nur ein Prototyp der Idee ist.

Aktuelle Ideen:

  • Ein keysof Foo -Operator zum Abrufen der Vereinigung von Eigenschaftsnamen aus Foo als String-Literal-Typen.
  • Ein Foo[K] -Typ, der angibt, dass es sich bei einem Typ K um einen String-Literal-Typ oder eine Vereinigung von String-Literal-Typen handelt.

Wenn Sie aus diesen Basisblöcken ein Zeichenfolgenliteral als geeigneten Typ ableiten müssen, können Sie schreiben

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

Hier ist K ein Subtyp von keysof T was bedeutet, dass es sich um einen String-Literal-Typ oder eine Vereinigung von String-Literal-Typen handelt. Alles, was Sie für den Parameter key , sollte kontextbezogen durch dieses Literal / diese Vereinigung von Literalen eingegeben und als Singleton-String-Literal-Typ abgeleitet werden.

Zum Beispiel

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

Das größte Problem ist, dass keysof häufig den Betrieb "verzögern" muss. Es ist zu eifrig, wie ein Typ bewertet wird, was ein Problem für Typparameter ist, wie im ersten Beispiel, das ich gepostet habe (dh der Fall, den wir wirklich lösen wollen, ist eigentlich der schwierige Teil: Lächeln :).

Hoffe das gibt euch allen ein Update.

Alle 76 Kommentare

Mögliches Duplikat von # 394

Siehe auch https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

@NoelAbrahams Ich glaube wirklich nicht, dass es sich um ein Duplikat von # 394 handelt. Im Gegenteil, beide Funktionen ergänzen sich ziemlich gut wie:

 class Model<T> {
    get(prop: memberof T): T[prop];
    set(prop:  memberof T, value: T[prop]): void;
  }

Wäre ideal

@fdecampredon

contact.set(Math.random() >= 0.5 ? 'age' : 'name', 13)

Was ist in diesem Fall zu tun?

Es ist mehr oder weniger der gleiche Fall wie im letzten Absatz meiner Ausgabe. Wie ich bereits sagte, haben wir Multiple-Choice. Wir können einen Fehler melden oder any für T[prop] ableiten. Ich denke, die zweite Lösung ist logischer

Toller Vorschlag. Stimmen Sie zu, es wäre eine nützliche Funktion.

@fdecampredon , ich glaube, das ist ein Duplikat. Siehe den Kommentar von Dan und die entsprechende Antwort, die den Vorschlag für membertypeof .

IMO all dies ist eine Menge neuer Syntax für einen ziemlich engen Anwendungsfall.

@ NoelAbrahams es ist nicht dasselbe.

  • memberof T gibt den Typ zurück, dessen Instanz nur eine Zeichenfolge mit dem gültigen Eigenschaftsnamen T Instanz sein kann.
  • T[prop] gibt den Typ der Eigenschaft von T die mit einem String benannt ist, der durch das Argument / die Variable prop wird.

Es gibt eine Abkürzung für memberof , diese Art von prop -Parameter sollte memberof T .

Eigentlich hätte ich gerne ein umfangreicheres System für die Typinferenz basierend auf Typmetadaten. Aber ein solcher Operator ist ein guter Anfang sowie memberof .

Das ist interessant und wünschenswert. TypeScript funktioniert noch nicht gut mit stringlastigen Frameworks, und dies würde offensichtlich sehr hilfreich sein.

TypeScript eignet sich nicht für stringlastige Frameworks

Wahr. Ändert immer noch nichts an der Tatsache, dass dies ein doppelter Vorschlag ist.

dennoch und dies würde offensichtlich viel helfen [um stringlastige Frameworks zu tippen]

Nicht sicher, dass. Scheint eher stückweise und etwas spezifisch für das oben beschriebene Proxy-Objekt-Muster zu sein. Ich würde einen ganzheitlicheren Ansatz für das Problem der magischen Saiten nach dem Vorbild von # 1003 sehr bevorzugen.

1003 schlägt any als Rückgabetyp eines Getters vor. Dieser Vorschlag fügt hinzu, dass durch Hinzufügen einer Möglichkeit zum Nachschlagen des Wertetyps auch die Mischung zu etwa folgendem Ergebnis führen würde:

declare module ImmutableJS {
  class Map<T> {
    get(prop: memberof T): T[prop];
    set(prop: memberof T, value: T[prop]): Map<T>;
  }
}

@spion ,

Ich dachte über den Rückgabetyp nach, ließ ihn aber weg, um den Gesamtvorschlag nicht zu groß zu machen.

Dies war mein erster Gedanke, hat aber Probleme. Was ist, wenn es mehrere Argumente vom Typ memberof T , auf die sich membertypeof T bezieht?

get(property: memberof T): membertypeof T;
set(property: memberof T, value: membertypeof T);

Dies löst das Problem "Auf welches Argument beziehe ich mich?", Aber der Name membertypeof scheint falsch zu sein und kein Fan des Operators, der auf den Eigenschaftsnamen abzielt.

get(property: memberof T): membertypeof property;
set(property: memberof T, value: membertypeof property);

Ich denke das funktioniert besser.

get(property: memberof T is A): A;
set(property: memberof T is A, value: A)

Leider nicht sicher, ob ich eine großartige Lösung habe, obwohl ich glaube, dass der letzte Vorschlag ein anständiges Potenzial hat.

OK @NoelAbrahams Es gab einen Kommentar in # 394, der versuchte, mehr oder weniger dasselbe zu beschreiben wie dieser.
Jetzt denke ich, dass T[prop] vielleicht etwas eleganter ist als die verschiedenen Sätze dieses Kommentars, und dass der Satz in dieser Ausgabe in der Reflexion vielleicht etwas weiter geht.
Aus diesem Grund denke ich nicht, dass es als Duplikat geschlossen werden sollte.
Aber ich denke, ich bin voreingenommen, da ich derjenige bin, der die Ausgabe geschrieben hat;).

@fdecampredon , mehr desto besser: smiley:

@ NoelAbrahams oops, ich habe diesen Teil verpasst. Sicher, diese sind ziemlich gleichwertig (dieser scheint keinen anderen generischen Parameter einzuführen, was ein Problem sein kann oder nicht).

Wenn ich mir Flow anschaue, denke ich, dass es mit einem etwas stärkeren Typensystem und speziellen Typen eleganter wäre als mit einer Ad-hoc-Typverengung.

Was wir zum Beispiel mit get(prop: string): Contact[prop] meinen, ist nur eine Reihe möglicher Überladungen:

interface Map {
  get(prop : string) : Contact[prop];
}

// is morally equivalent to 

interface Map {
  get(prop : "name") : string;
  get(prop : "age") : number;
}

Unter der Annahme, dass der Operator & (Schnittpunkttypen) vorhanden ist, entspricht dieser Typ

interface Map {
   get : (prop : "name") => string & (prop : "age") => number;
}

Nachdem wir unseren nicht generischen Fall nur in Typausdrücke ohne besondere Behandlung (kein [prop] ) übersetzt haben, können wir die Frage der Parameter beantworten.

Die Idee ist, diesen Typ etwas aus einem Typparameter zu generieren. Wir könnten einige spezielle generische Dummy-Typen $MapProperties , $Name und $Value , um unseren generierten Typ nur als Typausdruck (keine spezielle Syntax) auszudrücken, während wir weiterhin auf die Typprüfung hinweisen dass etwas getan werden sollte.

class Map<T> {
   get : $MapProperties<T, (prop : $Name) => $Value>
   set : $MapProperties<T, (prop : $Name, val : $Value) => void>
}

Es mag kompliziert und beinahe als Vorlage erscheinen oder als menschenabhängige Typen der Armen, aber es kann nicht vermieden werden, wenn jemand möchte, dass Typen von Werten abhängen.

Ein weiterer Bereich, in dem dies nützlich wäre, ist das Durchlaufen der Eigenschaften eines typisierten Objekts:

interface Env {
 // pretend this is an actually interesting type
};

var actions = {
  action1: function (env: Env, x: number) : void {},
  action2: function (env: Env, y: string) : void {}
};

// actions has type { action1: (Env, number) => void; action2: (Env, string) => void; }
var env : Env = {};
var boundActions = {};
for (var action in actions) {
  boundActions[action] = actions[action].bind(null, env);
}

// boundActions should have type { action1: (number) => void; action2: (string) => void; }

Diese Typen sollten zumindest theoretisch ableitbar sein (es gibt genügend Typinformationen, um auf das Ergebnis der for -Schleife schließen zu können), aber es ist wahrscheinlich auch eine ziemliche Strecke.

Beachten Sie, dass die nächste Version von react von diesem Ansatz stark profitieren würde (siehe https://github.com/facebook/react/issues/3398)

Wie bei https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856 bricht diese Funktion aufgrund des Halteproblems schnell zusammen, wenn die Zeichenfolge von einem anderen Ausdruck als einem Zeichenfolgenliteral geliefert wird. Kann eine Basisversion davon dennoch mithilfe des Problems des Lernens aus dem ES6-Symbol (# 2012) implementiert werden?

Genehmigt.

Wir werden dies in einem experimentellen Zweig ausprobieren wollen, um ein Gefühl für die Syntax zu bekommen.

Sie fragen sich nur, welche Version des Vorschlags umgesetzt wird? Dh wird T[prop] an der Anrufstelle ausgewertet und eifrig durch einen konkreten Typ ersetzt, oder wird es eine neue Form der Typvariablen?

Ich denke, wir sollten uns auf eine allgemeinere und weniger ausführliche Syntax verlassen, wie sie in # 3779 definiert ist.

interface Map<T> {
  get<A>(prop: $Member<T,A>): A;
  set<A>(prop: $Member<T,A>, value: A): Map<T>;
}

Oder ist es nicht möglich, auf die Art von A zu schließen?

Ich möchte nur sagen, dass ich ein kleines Codegen-Tool erstellt habe, um die TS-Integration mit ImmutableJS zu vereinfachen, während wir auf die normale Lösung warten: https://www.npmjs.com/package/tsimmutable. Es ist ganz einfach, aber ich denke, es wird für die meisten Anwendungsfälle funktionieren. Vielleicht hilft es jemandem.

Außerdem möchte ich darauf hinweisen, dass die Lösung mit einem Mitgliedstyp möglicherweise nicht mit ImmutableJS funktioniert:

interface Profile {
  firstName: string 
}

interface User {
  profile: Profile  
}

let a: Map<User> = fromJS(/* ... */);
a.get('profile') // Type will be Profile, but the real type is Map<Profile>!

@ s-panferov So etwas könnte funktionieren:

interface ImmutableMap<T> {
    get<A extends boolean | number | string>(key : string) : A;
    get<A extends {}>(key : string) : ImmutableMap<A>;
    get<E, A extends Array<any>>(key : string) : ImmutableList<E>;
}

interface Profile {

}

interface User {
    name : string;
    profile : Profile;
}

var map : ImmutableMap<User>;

var name = map.get<string>('name'); // string
var profile = map.get<Profile>('profile'); // ImmutableMap<Profile>

Dies schließt DOM-Knoten oder Datumsobjekte nicht aus, sollte jedoch überhaupt nicht in unveränderlichen Strukturen verwendet werden dürfen. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Allmählich von der besten Problemumgehung aus arbeiten

Ich denke, es ist nützlich, zuerst mit der derzeit besten verfügbaren Problemumgehung zu beginnen und sie dann schrittweise von dort aus zu erarbeiten.

Angenommen, wir benötigen eine Funktion, die den Wert einer Eigenschaft für jedes nicht primitive Objekt zurückgibt:

function getProperty<T extends object>(container: T; propertyName: string) {
    return container[propertyName];
}

Jetzt möchten wir, dass der Rückgabewert den Typ der Zieleigenschaft hat, sodass ein weiterer generischer Typparameter für seinen Typ hinzugefügt werden kann:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

Ein Anwendungsfall mit einer Klasse würde also so aussehen:

class C {
    member: number;
    static member: string;
}

let instance = new C();
let result = getProperty<C, typeof instance.member>(instance, "member");

Und result würde den Typ number korrekt erhalten.

Es scheint jedoch einen doppelten Verweis auf 'member' im Aufruf zu geben: Einer befindet sich im Typparameter und der andere ist eine _literal_ Zeichenfolge, die immer die Zeichenfolgendarstellung des Namen der Zieleigenschaft erhält. Die Idee dieses Vorschlags ist, dass diese beiden zu einem einzigen Typparameter vereinheitlicht werden können, der nur die Zeichenfolgendarstellung erhalten würde.

Daher muss hier beachtet werden, dass die Zeichenfolge auch als _generischer Parameter_ fungiert und als Literal übergeben werden muss, damit dies funktioniert (andere Fälle können ihren Wert stillschweigend ignorieren, sofern keine Form der Laufzeitreflexion verfügbar ist). Um die Semantik klar zu machen, muss es eine Möglichkeit geben, eine Beziehung zwischen dem generischen Parameter und der Zeichenfolge zu kennzeichnen:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <T[PName]> container[propertyName];
}

Und jetzt würden die Anrufe so aussehen:

let instance = new C();
let result = getProperty<C>(instance, "member");

Was sich intern auflösen würde:

let result = getProperty<C, "member">(instance, "member");

Da C sowohl eine Instanz als auch eine statische Eigenschaft mit dem Namen member , ist der generische Ausdruck T[PName] höherer Art nicht eindeutig. Da die Absicht hier hauptsächlich darin besteht, sie auf Eigenschaften von Instanzen anzuwenden, könnte eine Lösung wie der vorgeschlagene Operator typeon intern verwendet werden, was auch die Semantik verbessern kann, da T[PName] von einigen als repräsentativ interpretiert wird eine Wertreferenz, nicht unbedingt ein Typ:

function getProperty<T extends object, PName: string = propertyName>(container: T; propertyName: string) {
    return <typeon T[PName]> container[propertyName];
}

(Dies würde auch funktionieren, wenn T ein kompatibler Schnittstellentyp ist, da typeon auch Schnittstellen unterstützt.)

Um die statische Eigenschaft zu erhalten, wird die Funktion mit dem Konstruktor selbst aufgerufen und der Konstruktortyp sollte als typeof C :

let result = getProperty<typeof C>(C, "member"); // Note it is called with the constructor object

(Intern wird typeon (typeof C) in typeof C damit dies funktioniert.)
result erhält jetzt korrekt den Typ string .

Um zusätzliche Arten von Eigenschaftskennungen zu unterstützen, kann dies wie folgt umgeschrieben werden:

type PropertyIdentifier = string|number|Symbol;

function getProperty<T extends object, PName: PropertyIdentifier = propertyName>(container: T; propertyName: PropertyIdentifier) {
    return <typeon T[PName]> container[propertyName];
}

Es sind also verschiedene Funktionen erforderlich, um dies zu unterstützen:

  • Ermöglichen die Übergabe von Literalwerten als generische Parameter.
  • Lassen Sie diese Literale den Wert von Funktionsargumenten erhalten.
  • Erlaube eine Form von Generika höherer Art.
  • Ermöglichen Sie das Referenzieren eines Eigenschaftstyps über einen generischen Literalparameter auf einen generischen Typ über Generika höherer Art.

Das Problem überdenken

Kehren wir nun zur ursprünglichen Problemumgehung zurück:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

Ein Vorteil dieser Problemumgehung besteht darin, dass sie problemlos erweitert werden kann, um komplexere Pfadnamen zu unterstützen, die nicht nur auf direkte Eigenschaften, sondern auch auf Eigenschaften verschachtelter Objekte verweisen:

function getPath<T extends object, P>(container: T; path: string) {
    ... more complex code here ...

    return <P> resultValue;
}

und nun:

class C {
    a: {
        b: {
            c: string[];
        }
    }
}
let instance = new C();
let result = getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

result würde hier korrekt den Typ string[] .

Die Frage ist, ob auch verschachtelte Pfade unterstützt werden sollen. Wird die Funktion wirklich so nützlich sein, wenn beim Eingeben keine automatische Vervollständigung verfügbar ist?

Dies deutet darauf hin, dass möglicherweise eine andere Lösung möglich ist, die in die andere Richtung funktioniert. Zurück zur ursprünglichen Problemumgehung:

function getProperty<T extends object, P>(container: T; propertyName: string) {
    return <P> container[propertyName];
}

Wenn Sie dies aus der anderen Richtung betrachten, ist es möglich, die Typreferenz selbst in eine Zeichenfolge zu konvertieren:

function getProperty<T extends object, P>(container: T; propertyName: string = @typeReferencePathOf(P)) {
    return <P> container[propertyName];
}

@typeReferencePathOf ähnelt im Konzept nameOf , gilt jedoch für generische Parameter. Es würde die empfangene Typreferenz typeof instance.member (oder alternativ typeon C.member ) nehmen, den Eigenschaftspfad member extrahieren und ihn beim Kompilieren in die Literalzeichenfolge "member" konvertieren Zeit. Ein weniger spezialisiertes komplementäres @typeReferenceOf würde in die vollständige Zeichenfolge "typeof instance.member" .

Also jetzt:

getProperty<C, typeof instance.subObject>(instance);

Würde sich entschließen zu:

getProperty<C, typeof instance.subObject>(instance, "subObject");

Und eine ähnliche Implementierung für getPath :

getPath<C, typeof instance.a.b.c>(instance);

Würde sich entschließen zu:

getPath<C, typeof instance.a.b.c>(instance, "a.b.c");

Da @typeReferencePathOf(P) nur als Standardwert festgelegt ist. Das Argument kann bei Bedarf manuell angegeben werden:

getPath<C, SomeTypeWhichIsNotAPath>(instance, "member.someSubMember.AnotherSubmember.data");

Wenn der zweite Typparameter nicht angegeben würde, könnte @typeReferencePathOf() in undefined oder alternativ in die leere Zeichenfolge "" . Wenn ein Typ angegeben wurde, aber keinen internen Pfad hatte, wurde er in "" .

Vorteile dieses Ansatzes:

  • Benötigt keine höherwertigen Generika.
  • Es ist nicht erforderlich, Literale als generische Parameter zuzulassen.
  • Erfordert keine Erweiterung von Generika in irgendeiner Weise.
  • Ermöglicht die automatische Vervollständigung beim Eingeben des Ausdrucks typeof und verwendet einen vorhandenen Mechanismus zur Typprüfung, um ihn zu validieren, anstatt ihn zur Kompilierungszeit aus einer Zeichenfolgendarstellung konvertieren zu müssen.
  • Kann auch in generischen Klassen verwendet werden.
  • Benötigt nicht typeon (könnte es aber unterstützen).
  • Kann in anderen Szenarien angewendet werden.

+1

Nett! Ich habe bereits begonnen, einen großen Vorschlag zu schreiben, um das gleiche Problem anzugehen, habe aber diese Diskussion gefunden. Lassen Sie uns hier diskutieren.

Hier ist ein Auszug aus meiner nicht eingereichten Ausgabe:

Frage : Wie sollte die folgende Funktion in .d.ts deklariert werden, damit sie in Kontexten verwendet werden kann, in denen sie verwendet werden soll?

function mapValues(obj, fn) {
      return Object.keys(obj)
          .map(key => ({key, value: fn(obj[key], key)}))
          .reduce((res, {key, value}) => (res[key] = value, res), {})
}

Diese Funktion (mit geringfügigen Abweichungen) ist in fast jeder generischen "Utils" -Bibliothek zu finden.

Es gibt zwei verschiedene Anwendungsfälle für mapValues in freier Wildbahn:

ein. _Object-as-a-_ _Dictionary _ bei dem Eigenschaftsnamen dynamisch sind, Werte denselben Typ haben und die Funktion im Allgemeinen monomorph ist (Kenntnis dieses Typs)

var obj = {'a': 1, 'b': 2, 'c': 3};
var res = mapValues(x, val => val * 5); // {a: 5, b: 10, c: 15}
console.log(res['a']) // 5

b. _Object-as-a-_ _Record _ wobei jede Eigenschaft ihren eigenen Typ hat, während die Funktion parametrisch polymorph ist (durch Implementierung)

var obj = {a: 123, b: "Hello", c: true};
var res = mapValues(p, val => [val]); // {a: [123], b: ["Hello"], c: [true]}
console.log(res.a[0].toFixed(2)) // "123.00"

Die beste verfügbare Instrumentierung für mapValues mit aktuellem TypeScript ist:

declare function mapValues<T1, T2>(
    obj: {[key: string]: T1},
    fn: (arg: T1, key: string) => T2
): {[key: string]: T2};

Es ist nicht schwer zu sehen , dass diese Signatur paßt perfekt zu dem _object-as-a-_ _Dictionary _ Anwendungsfall, während erzeugt ein etwas überraschend 1 Ergebnis der Typinferenz wenn in dem Fall , wo angewandt wird _object-as-a-_ Aufzeichnung war die Absicht.

Der Typ {p1: T1, p2: T2, ...pn: Tn} wird zu {[key: string]: T1 | T2 | ... | Tn } gezwungen, der alle Typen zusammenführt und alle Informationen über die Struktur eines Datensatzes effektiv verwirft:

  • Die korrekte und erwartete 2 ist in Ordnung. console.log(res.a[0].toFixed(2)) wird vom Compiler abgelehnt.
  • Das akzeptierte console.log((<number>res['a'][0]).toFixed(2)) hat zwei unkontrollierte und fehleranfällige Stellen: die Typumwandlung und den Namen einer beliebigen Eigenschaft.

1, 2 - für die Programmierer, die von der ES zur TS migrieren


Mögliche Schritte, um dieses Problem anzugehen (und auch andere zu lösen)

1. Führen Sie mithilfe von String-Literal-Typen verschiedene Datensätze ein

type Numbers<p extends string> =  { ...p: number };
type NumbersOpt<p extends string> = {...p?: number };
type ABC = "a" | "b" | "c";
type abc = Numbers<ABC> // abc =  {a: number, b: number, c: number} 
type abcOpt = NumbersOpt<ABC> // abcOpt =  {a?: number, b?:number, c?: number}

function toFixedAll<p extends string>(obj: {...p: number}, precision):{...p: string}  {
      var result: {...p: string} = {} as any;
      Object.keys(obj).forEach((p:p) => {
           result[p] = obj[p].toFixed(precision);
      });
      return result;
}

var test = toFixedAll({x:5, y:7}, 3); // { x: "5.00", y: "6.00" },  p inferred as "x"|"y"
console.log(test.y.length) // 4   test.y: string
2. Erlauben Sie, dass formale Typen mit anderen formalen Typen subskribiert werden, die gezwungen sind, 'string' zu erweitern.

Beispiel:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. Ermöglichen Sie die Destrukturierung formaler Typen

Beispiel:

class C<Array<T>> {
     x: T;
}   

var v1: C<string[]>;   // v1.x: string

Mit allen drei Schritten zusammen konnten wir schreiben

declare module Backbone {

  class Model<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): void;
  }
}

declare module ImmutableJS {
  class Map<{...p: T[p]}> {
    get(prop: p): T[p];
    set(prop: p, value: T[p]): this;
  }
}

declare function pluck<p extends string, T[p]>(
    arr: Array<{...p:T[p]}>, prop: p
): Array<T[p]> 

Ich weiß, dass die Syntax ausführlicher ist als die von @fdecampredon vorgeschlagene
aber ich kann mir nicht vorstellen, wie man den Typ des mapValues oder des combineReducers mit dem ursprünglichen Vorschlag ausdrückt.

function combineReducers(reducers) {
  return (state, action) => mapValues(reducers, (reducer, key) => reducer(state[key], action))
}

mit meinem Vorschlag wird seine Erklärung wie folgt aussehen:

declare function combineReducers<p extends string, S[p]>(
    reducers: { ...p: (state: S[p], action: Action) => S[p] }
): (state: { ...p: S[p] }, action: Action) => { ...p: S[p] };

Ist es mit dem ursprünglichen Vorschlag ausdrückbar?

@spion , @fdecampredon ?

@ Artazor Auf

Angenommen, user.id und user.name sind Datenbankspaltentypen, die entsprechend number und string dh Column<number> und Column<string>

Schreiben Sie den Typ einer Funktion select , die benötigt wird:

select({id: user.id, name: user.name})

und gibt Query<{id: number; name: string}>

select<T>({...p: Column<T[p]>}):Query<T>

Ich bin mir nicht sicher, wie das überprüft und abgeleitet werden soll. Es scheint ein Problem zu geben, da der Typ T nicht existiert. Müsste der Rückgabetyp in einer destrukturierten Form ausgedrückt werden? dh

select<T>({...p: Column<T[p]>}):Query<{...p:T[p]}>

@Artazor hat vergessen zu fragen, ist der Parameter p extends string wirklich notwendig?

Wäre so etwas nicht genug?

select<T[p]>({...p: Column<T[p]>}):Query<{...p:T[p]}>

dh für alle Typvariablen T [p] in {...p:Column<T[p]>}

bearbeiten: eine andere Frage, wie wird dies auf Richtigkeit überprüft?

@spion Ich habe Ihren Standpunkt - Sie haben mich missverstanden (aber es ist meine Schuld), mit T[p] habe ich etwas ganz anderes gemeint, und Sie werden sehen, dass p extends string ist hier eine entscheidende Sache. Lassen Sie mich meine Gedanken ausführlich erklären.

Betrachten wir vier Anwendungsfälle für Datenstrukturen: List , Tuple , Dictionary und Record . Wir werden sie als konzeptionelle Datenstrukturen bezeichnen und sie mit den folgenden Eigenschaften beschreiben:

Konzeptionelle DatenstrukturenZugang durch
ein. Position
(Nummer)
b. Schlüssel
(Zeichenfolge)
Graf von
Artikel
1. Variable
(die gleiche Rolle für jeden Artikel)
ListeWörterbuch
2. behoben
(Jeder Gegenstand spielt seine eigene Rolle)
TupelAufzeichnung

In JavaScript werden diese vier Fälle durch zwei Speicherformulare unterstützt: ein Array - für a1 und a2 und ein Object - für b1 und b2 .

_Hinweis 1 _. Um ehrlich zu sein, ist es nicht richtig zu sagen, dass das Tuple durch das Array , da der einzige "wahre" Tupeltyp der Typ des arguments Objekts ist, das ist kein Array . Trotzdem sind sie mehr oder weniger austauschbar, insbesondere im Zusammenhang mit der Destrukturierung und dem Function.prototype.apply das die Tür für die Metaprogrammierung öffnet. Das TypeScript nutzt auch Arrays zum Modellieren von Tupeln.

_Hinweis 2 _. Während das vollständige Konzept des Wörterbuchs mit beliebigen Schlüsseln Jahrzehnte später in Form des ES6 Map , entschied sich Brendan Eich zunächst, ein Record und ein limitiertes Dictionary (mit) zu kombinieren Nur String-Schlüssel) unter der gleichen Haube von Object (wobei obj.prop dem obj["prop"] ) wurde (meiner Meinung nach) das umstrittenste Element der gesamten Sprache.
Es ist sowohl der Fluch als auch der Segen der JavaScript-Semantik. Es macht Reflexion trivial und ermutigt Programmierer, frei zwischen Programmier- und Metaprogrammierstufe zu wechseln, und zwar zu nahezu null mentalen Kosten (auch ohne es zu merken!). Ich glaube, es ist der wesentliche Teil des JavaScript-Erfolgs als Skriptsprache.

Jetzt ist es an der Zeit, dass das TypeScript die Möglichkeit bietet, Typen für diese seltsame Semantik auszudrücken. Wenn wir auf der Programmierebene denken, ist alles in Ordnung:

Typen auf ProgrammierebeneZugang durch
ein. Position
(Nummer)
b. Schlüssel
(Zeichenfolge)
Graf von
Artikel
1. Variable
(die gleiche Rolle für jeden Artikel)
T []{[key: string]: T}
2. behoben
(Jeder Gegenstand spielt seine eigene Rolle)
[T1, T2, ...]{key1: T1, key2: T2, ...}

Wenn wir jedoch zur Meta-Programmier-Ebene wechseln, werden Dinge, die auf der Programmier-Ebene festgelegt wurden, plötzlich variabel! Und hier erkennen wir plötzlich die Beziehungen zwischen Tupeln und Funktionssignaturen und schlagen Dinge wie # 5453 (Variadic Kinds) vor, die tatsächlich nur einen kleinen (aber ziemlich wichtigen) Teil des Metaprogrammierungsbedarfs abdecken - die Verkettung von Signaturen durch die Fähigkeit, eine formale Struktur zu zerstören Parameter in Typen der Rest-Argumente:

     function f<T>(a: number, ...args:T) { ... }

    f<[string,boolean]>(1, "A", true);

Falls # 6018 ebenfalls implementiert wird, könnte die Funktionsklasse so aussehen

declare class Function<This, TArgs, TRes> {
        This::(...args: TArgs): TRes;
        call(self: This, ...args: TArgs): TRes;
        apply(self: This, args: TArgs): TRes;
        // bind needs also formal pattern matching:
        bind<[...TPartial, ...TCurried] = TArgs>(
           self: This, ...args: TPartial): Function<{}, TCurried, TRes> 
}

Es ist toll, aber trotzdem unvollständig.

Stellen Sie sich zum Beispiel vor, wir möchten der folgenden Funktion einen korrekten Typ zuweisen:

function extractAndWrapAll(...args) {
     return args.map(x => [x]);
}

// wrapAll(1,"A",true) === [[1],["A"],[true]]

Mit vorgeschlagenen variadischen Arten können wir keine Signaturtypen transformieren. Hier brauchen wir etwas Stärkeres. Tatsächlich ist es wünschenswert, Funktionen zur Kompilierungszeit zu haben, die über Typen hinweg arbeiten können (wie in Facebooks Flow). Ich bin mir jedoch sicher, dass dies nur möglich sein wird, wenn (und wenn) das Typsystem des TypeScript stabil genug ist, um es den Endprogrammierern zugänglich zu machen, ohne dass ein erhebliches Risiko besteht, ein Benutzerland bei einem nächsten kleinen Update zu beschädigen. Wir brauchen also etwas weniger Radikales als die vollständige Unterstützung der Metaprogrammierung, aber dennoch in der Lage, nachgewiesene Probleme zu lösen.

Um dieses Problem anzugehen, möchte ich den Begriff der Objektsignatur einführen. Etwa ist es auch nicht

  1. die Menge der Eigenschaftsnamen des Objekts oder
  2. den Satz der Schlüssel des Wörterbuchs oder
  3. Die Menge der numerischen Indizes eines Arrays
  4. die Menge der numerischen Positionen eines Tupels.

Im Idealfall wäre es schön, ganzzahlige Literaltypen sowie String-Literaltypen zu haben, um Signaturen der Arrays oder Tupel wie darzustellen

type ZeroToFive = 0 | 1 | 2 | 3 | 4 | 5;
// or
type ZeroToFive = 0 .. 5;   // ZeroToFive extends number (!)

Die Regeln für ganzzahlige Literale stimmen mit den Regeln für String-Literale überein.

Dann können wir die Syntax der Signaturabstraktion einführen, die genau mit der Syntax für Objektrest / Spread übereinstimmt:

{...Props: T }

mit einer signifikanten Ausnahme: hier ist Props der Bezeichner, der unter dem universellen Quantifizierer gebunden ist:

<Props extends string> {...Props: T }  // every property has type T
<Index extends number> {...Index: T }  // every item has type T  
// the same as T[]

Daher wird es im lexikalischen Bereich des Typs eingeführt und kann an einer beliebigen Stelle in diesem Bereich als Name des Typs verwendet werden. Es wird jedoch doppelt verwendet: Wenn es anstelle des Namens der Rest- / Spread-Eigenschaft verwendet wird, repräsentiert es die Abstraktion über die Objektsignatur, wenn es als eigenständiger Typ verwendet wird, repräsentiert es einen Subtyp der Zeichenfolge (bzw. der Zahl).

declare class Object {
      static keys<p extends string, q extends p>(object{...q: {}}): p[];
}

Und hier ist der raffinierteste Teil: _Key Dependent Types_

Wir führen ein spezielles Typkonstrukt ein: T for Prop (ich möchte diese Syntax anstelle von T [Prop] verwenden, das Sie verwirrt hat), wobei Prop der Name der Typvariablen ist, die die Signaturabstraktion des Objekts enthält. Zum Beispiel führt <Prop extends string, T for Prop> zwei formale Typen in den lexikalischen Bereich ein, den Prop und den T wobei bekannt ist, dass für jeden bestimmten Wert p von Prop es wird einen eigenen Typ geben T .

Wir sagen nicht, dass irgendwo ein Objekt ist, das Eigenschaften Props und deren Typen T ! Wir führen nur eine funktionale Abhängigkeit zwischen zwei Typen ein. Typ T ist mit Mitgliedern vom Typ Requisiten korreliert, und das ist alles!

Es gibt uns die Möglichkeit, solche Dinge zu schreiben wie

function unwrap<P extends string, T for P>(obj:{...P: Maybe<T>}): Maybe<{...P: T}> {
  ...
}

unwrap({a:{value:1}, b:{value:"A"}, c:{value: true}}) === { a: 1, b: "A", c: true }
// here actual parameters will be inferred as 
unwrap<"a"|"b"|"c", {a: number, b: string, c: boolean}>

Der zweite Parameter wird jedoch nicht als Objekt, sondern als abstrakte Zuordnung von Bezeichnern zu Typen behandelt. In dieser Perspektive kann T for P verwendet werden, um abstrakte Folgen von Typen für Tupel darzustellen, wenn P ein Subtyp der Zahl ist (

Wenn T irgendwo in {...P: .... T .... } , repräsentiert es genau einen bestimmten Typ dieser Karte.

Es ist meine Hauptidee.
Mit Spannung auf Fragen, Gedanken, Kritik warten -)

Richtig, also extends string soll in einem Fall Arrays (und verschiedene Argumente) und im anderen Fall String-Konstanten (als Typen) berücksichtigen. Süss!

Wir sagen nicht, dass irgendwo ein Objekt ist, das Eigenschaften Requisiten hat und deren Typen T sind! Wir führen nur eine funktionale Abhängigkeit zwischen zwei Typen ein. Typ T ist mit Mitgliedern vom Typ Requisiten korreliert, und das ist alles!

Ich habe das nicht so gemeint, ich habe es eher so gemeint, da ihre Typen T [p] sind, ein Wörterbuch der durch p indizierten Typen. Wenn das eine gute Intuition ist, würde ich das behalten.

Insgesamt erfordert die Syntax zwar etwas mehr Arbeit, aber die allgemeine Idee sieht fantastisch aus.

Ist es möglich, unwrap für verschiedene Argumente zu schreiben?

edit: egal, ich habe gerade festgestellt, dass Ihre vorgeschlagene Erweiterung auf verschiedene Arten dies anspricht.

Hallo zusammen,
Ich bin verwirrt, wie ich eines meiner Probleme lösen kann, und habe diese Diskussion gefunden.
Das Problem ist:
Ich habe die Methode RpcManager.call(command:Command):Promise<T> und die Verwendung wäre so:

RpcManager.call(new GetBalance(123)).then((result) => {
 // here I want that result would have a type.
});

Lösung Ich denke könnte sein:

interface Command<T> {
    responseType:T;
}

class GetBalance implements Command<number> {
    responseType: number; // somehow this should be avoided. maybe Command should be abstract class.
    constructor(userId:number) {}
}

class RpcManager {
    static call(command:Command):Promise<typeof command.responseType> {
    }
}

or:

class RpcManager {
    static call<T>(command:Command<T>):Promise<T> {
    }
}

Irgendwelche Gedanken dazu?

@ antanas-arvasevicius Der letzte Codeblock in diesem Beispiel sollte tun, was Sie wollen.

Es hört sich so an, als hätten Sie eher die Frage, wie Sie eine bestimmte Aufgabe erfüllen können. Verwenden Sie den Stapelüberlauf oder reichen Sie ein Problem ein, wenn Sie glauben, einen Compiler-Fehler gefunden zu haben.

Hallo Ryan, danke für deine Antwort.
Ich habe diesen letzten Codeblock ausprobiert, aber er funktioniert nicht.

Schnelle Demo:

interface Command<T> { }
class MyCommand implements Command<{status:string}> { }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

//output: error TS2339: Property 'status' does not exist on type '{}'.
//tested with: Version 1.9.0-dev.20160222

Es tut mir leid, dass ich Stack Overflow nicht verwendet habe, aber ich dachte, dass es mit diesem Problem zusammenhängt :)
Soll ich eine neue Ausgabe zu diesem Thema eröffnen?

Ein nicht konsumierter generischer Typparameter verhindert, dass Inferenz funktioniert. Im Allgemeinen sollten Sie niemals nicht verwendete Typparameter in Typdeklarationen haben, da diese bedeutungslos sind. Wenn Sie T verbrauchen, funktioniert einfach alles:

interface Command<T> { foo: T }
class MyCommand implements Command<{status:string}> { foo: { status: string; } }
class RPC { static call<T>(command:Command<T>):T { return; } }

let response = RPC.call(new MyCommand());
console.log(response.status);

Das ist einfach unglaublich! Vielen Dank!
Ich habe nicht gedacht, dass der Typparameter in den generischen Typ und eingefügt werden kann
TS wird es extrahieren.
Am 22. Februar 2016, 23:56 Uhr, schrieb "Ryan Cavanaugh" [email protected] :

Ein nicht konsumierter generischer Typparameter verhindert, dass Inferenz funktioniert. im
Im Allgemeinen sollten Sie niemals nicht verwendete Typparameter im Typ haben
Erklärungen, da sie bedeutungslos sind. Wenn Sie T konsumieren, ist alles einfach
funktioniert:

Schnittstellenbefehl{foo: T} -Klasse MyCommand implementiert den Befehl <{status: string}> {foo: {status: string; }} Klasse RPC {statischer Aufruf(Befehl: Befehl): T {return; }}
let response = RPC.call (neues MyCommand ());
console.log (response.status);

- -
Antworte direkt auf diese E-Mail oder sieh sie dir auf GitHub an
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius Wenn Sie APIs im RPC-Stil erstellen Ich habe einige Dokumente, die Sie möglicherweise nützlich finden: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

Die oben genannten Ansätze scheinen:

  • ziemlich kompliziert;
  • Behandeln Sie nicht die Verwendung von Zeichenfolgen im Code. string weder "Alle Referenzen finden" noch umgestaltbar sein (z. B. umbenennen).
  • Einige unterstützen nur "1st Level" -Eigenschaften und keine komplexeren Ausdrücke (die von einigen Frameworks unterstützt werden).

Hier ist eine weitere Idee, die von C # -Ausdrucksbäumen inspiriert wurde. Dies ist nur eine grobe Idee, nichts völlig durchdacht! Syntax ist schrecklich. Ich möchte nur sehen, ob dies jemanden inspiriert.

Angenommen, wir haben eine spezielle Art von Zeichenfolgen, um Ausdrücke zu bezeichnen.
Nennen wir es type Expr<T, U> = string .
Wobei T der Startobjekttyp und U der Ergebnistyp ist.

Angenommen, wir könnten eine Instanz von Expr<T,U> erstellen, indem wir ein Lambda verwenden, das einen Parameter vom Typ T und einen Mitgliederzugriff darauf ausführt.
Zum Beispiel: person => person.address.city .
In diesem Fall wird das gesamte Lambda zu einer Zeichenfolge kompiliert, die den Zugriff auf den Parameter enthält. In diesem Fall: "address.city" .

Sie können stattdessen eine einfache Zeichenfolge verwenden, die als Expr<any, any> .

Wenn Sie diesen speziellen Expr -Typ in der Sprache haben, können Sie Folgendes tun:

function pluck<T, U>(array: T[], prop: Expr<T, U>): U[];

let numbers = pluck([{x: 1}, {x: 2}], p => p.x);  // number[]
// compiles to:
// let numbers = pluck([..], "x");

Dies ist im Grunde eine begrenzte Form dessen, wofür Ausdrücke in C # verwendet werden.
Denkst du, das könnte verfeinert werden und irgendwohin führen, wo es interessant ist?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - Es tut mir leid, dass ich hier nicht auf Ihren Vorschlag antworte. Ich hoffe, dass er durch die Kommentare nicht "begraben" wird.) _

Ich denke, die Benennung dieser Funktion ('Type Property Type') ist sehr verwirrend und sehr schwer zu verstehen. Es war sehr schwer herauszufinden, was das hier beschriebene Konzept war und was es überhaupt bedeutet!

Erstens haben nicht alle Typen Eigenschaften! undefined und null nicht (obwohl dies nur die jüngsten Ergänzungen des Typsystems sind). Grundelemente wie number , string , boolean werden selten von einer Eigenschaft indiziert (z. B. 2 ["prop"] - obwohl dies zu funktionieren scheint, ist es fast immer ein Fehler)

Ich würde vorschlagen, dieses Problem zu benennen. Referenz des Schnittstelleneigenschaftstyps durch Zeichenfolgenliteralwerte . In diesem Thema geht es nicht um die Einführung eines neuen 'Typs', sondern um eine ganz bestimmte Möglichkeit, einen vorhandenen Typ mithilfe einer Zeichenfolgenvariablen oder eines Funktionsparameters zu referenzieren , dessen Wert zur Kompilierungszeit bekannt sein muss.

Es wäre sehr vorteilhaft gewesen, wenn dies so einfach wie möglich außerhalb des Kontextes eines bestimmten Anwendungsfalls beschrieben und veranschaulicht worden wäre:

interface MyInterface {
  prop1: number;
  prop2: string;
}

let prop1Name = "prop1";
type Prop1Type = MyInterface[prop1Name]; // Prop1Type is now 'number'

let prop2Name = "prop2";
type Prop2Type = MyInterface[prop2Name]; // Prop2Type is now 'string'

let prop3Name = "prop3";
type NonExistingPropType = MyInterface[prop3Name]; // Compilation error: property 'prop3' does not exist on 'MyInterface'.

let randomString = createRandomString();
type NotAvailablePropType = MyInterface[randomString]; // Compilation error: value of 'randomString' is not known at compile time.

_Edit: Um dies korrekt zu implementieren, muss der Compiler sicher sein, dass sich die der Variablen zugewiesene Zeichenfolge zwischen dem Punkt, an dem sie initialisiert wurde, und dem Punkt, auf den sie im Typausdruck verwiesen wurde, nicht geändert hat. Ich denke nicht, dass das sehr einfach ist? Würde diese Annahme über das Laufzeitverhalten immer gelten? _

_Edit 2: Vielleicht würde dies nur mit const funktionieren, wenn es mit einer Variablen verwendet wird? _

Ich bin nicht sicher, ob die ursprüngliche Absicht darin bestand, die Eigenschaftsreferenz nur im speziellen Fall zuzulassen, in dem ein Zeichenfolgenliteral an einen Funktions- oder Methodenparameter übergeben wird. z.B

function func(someString: string): MyInterface[someString] {
  ..
}

let x = func("prop"); // x gets the type of MyInterface.prop

Habe ich dies über das ursprünglich Verallgemeinerte hinaus verallgemeinert?

Wie würde dies mit einem Fall umgehen, in dem das Funktionsargument kein Zeichenfolgenliteral ist, dh zur Kompilierungszeit nicht bekannt ist? z.B

let x = func(getRandomString()); // What type if inferred for 'x' here?

Wird es einen Fehler geben oder standardmäßig any ?

(PS: Wenn dies tatsächlich beabsichtigt war, werde ich vorschlagen, dieses Problem durch Zeichenfolgenliteralfunktion oder Methodenargumente in

Hier ist ein einfaches Benchmark-Beispiel (genug für eine Illustration), das zeigt, was diese Funktion aktivieren muss:

Schreiben Sie den Typ dieser Funktion, der:

  • nimmt ein Objekt mit Versprechungsfeldern und
  • Gibt dasselbe Objekt zurück, wobei jedoch alle Felder aufgelöst sind und jedes Feld den richtigen Typ hat.
function awaitObject(obj) {
  var result = {};
  var wait = Object.keys(obj)
    .map(key => obj[key].then(val => result[key] = val));
  return Promise.all(wait).then(_ => result)
}

Beim Aufruf des folgenden Objekts:

var res = awaitObject({a: Promise.resolve(5), b: Promise.resolve("5")})

Das Ergebnis res sollte vom Typ {a: number; b: string}


Mit dieser Funktion (wie von @Artazor vollständig ausgearbeitet) wäre die Signatur

awaitObject<p, T[p]>(obj: {...p: Promise<T[p]>}):Promise<{...p: T[p]}>

edit: Der obige Rückgabetyp wurde behoben, es fehlte Promise<...> ^^

@spion

Vielen Dank, dass Sie versucht haben, ein Beispiel zu liefern, aber nirgendwo hier habe ich eine vernünftige und präzise Spezifikation und eine Reihe klar reduzierter Beispiele gefunden, die von praktischen Anwendungen getrennt sind und versuchen, die Semantik und die verschiedenen Randfälle hervorzuheben, wie dies funktionieren würde. sogar etwas so Grundlegendes wie:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Es wurden nur sehr wenige Anstrengungen unternommen, um einen effektiven und erklärenden Titel bereitzustellen (wie ich bereits erwähnt habe, ist 'Type Property Type' ein verwirrender und nicht sehr erklärender Name für diese Funktion, bei der es sich nicht wirklich um einen bestimmten Typ handelt, sondern um eine Typreferenz ). Mein bester vorgeschlagener Titel war _Interface-Eigenschaftstypreferenz durch Zeichenfolgenliteralfunktion oder Methodenargumente_ (vorausgesetzt, ich habe den Umfang nicht falsch verstanden).
  2. Es wurde nicht klar erwähnt, was passieren würde, wenn name zur Kompilierungszeit nicht bekannt, null oder undefiniert ist, z. B. func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. Es wurde nicht klar erwähnt, was passieren würde, wenn T ein primitiver Typ wie number oder null .
  4. Es wurde kein klares Detail angegeben, ob dies nur für Funktionen gilt und ob T[name] im Funktionskörper verwendet werden kann. Und was passiert in diesem Fall, wenn name im Funktionskörper neu zugewiesen wird und sein Wert daher beim Kompilieren möglicherweise nicht mehr bekannt ist?
  5. Keine wirkliche Spezifikation einer Syntax: zB funktionieren T["propName"] oder T[propName] auch alleine? Ohne Verweis auf einen Parameter oder eine Variable meine ich - es könnte nützlich sein!
  6. Keine Erwähnung oder Diskussion, ob T[name] in anderen Parametern oder sogar außerhalb von Funktionsbereichen verwendet werden kann, z
function func<T extends object>(name: string, val: T[name]) {
 ...
}
type A = { abcd: number };
const name = "abcd";
let x: A[name]; // Type of 'x' resolves to 'number'

_7. Keine wirkliche Diskussion über die relativ einfache Problemumgehung unter Verwendung eines zusätzlichen generischen Parameters und typeof (obwohl dies erwähnt wurde, aber relativ spät, nachdem die Funktion bereits akzeptiert wurde):

function get<T, V>(obj: T, propName: string): V {
    return obj[propName];
}

type MyType = { abcd: number };
let x: MyType  = { abcd: 12 };

let result = get<MyType, typeof x.abcd>(x, "abcd"); // Type of 'result' is 'number'

Fazit: Hier gibt es keinen wirklichen Vorschlag, nur eine Reihe von Anwendungsfalldemonstrationen. Ich bin überrascht, dass dies vom TypeScript-Team überhaupt akzeptiert oder sogar positiv aufgenommen wurde, da ich sonst nicht glauben würde, dass dies ihren Standards entspricht. Es tut mir leid, wenn ich hier etwas hart klinge (nicht typisch für mich), aber keine meiner Kritikpunkte ist persönlich oder auf eine bestimmte Person gerichtet.

Wollen wir wirklich so weit gehen, was die Metaprogrammierung betrifft?

JS ist eine dynamische Sprache, Code kann Objekte auf beliebige Weise manipulieren.
Es gibt Grenzen für das, was wir im Typsystem modellieren können, und es ist einfach, Funktionen zu entwickeln, die Sie in TS nicht modellieren können.
Die Frage ist vielmehr: Wie weit ist es vernünftig und nützlich zu gehen?

Das letzte Beispiel ( awaitObject ) von @spion ist zur gleichen Zeit:

  • Sehr komplex aus sprachlicher Sicht (ich stimme @malibuzios hier zu).
  • Sehr eng in Bezug auf die Anwendbarkeit. In diesem Beispiel werden bestimmte Einschränkungen ausgewählt und nicht gut verallgemeinert.

Übrigens , Promise .

Wir sind weit entfernt von der ursprünglichen Ausgabe, bei der es um die Eingabe einer API ging, die eine Zeichenfolge enthält, die Felder darstellt, z. B. _.pluck
Diese APIs sollten unterstützt werden, da sie alltäglich und nicht so kompliziert sind. Dafür brauchen wir keine Metamodelle wie { ...p: T[p] } .
Es gibt mehrere Beispiele im OP, einige weitere von Aurelia in meinem Kommentar im Namen der Ausgabe .
Ein anderer Ansatz kann diese Anwendungsfälle abdecken. Eine mögliche Idee finden Sie in meinem obigen Kommentar .

@ Jods4 Es ist überhaupt nicht eng. Es kann für eine Reihe von Dingen verwendet werden, die derzeit im Typensystem nicht modelliert werden können. Ich würde sagen, es ist eines der letzten großen fehlenden Teile im TS-Typsystem. Es gibt viele Beispiele aus der Praxis von https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 die einfacheren Dinge oben, das SQL-Abfrage-Generator-Beispiel "auswählen" und so weiter.

Dies ist awaitObject in Bluebird. Es ist eine echte Methode. Der Typ ist derzeit unbrauchbar.

Eine allgemeinere Lösung ist besser. Es wird sowohl für den einfachen als auch für den komplexen Fall funktionieren. In der Hälfte der Fälle wird eine unzureichende Lösung fehlen, die leicht zu Kompatibilitätsproblemen führen kann, wenn in Zukunft eine allgemeinere Lösung erforderlich ist.

Ja, es braucht viel mehr Arbeit, aber ich denke auch, dass @Artazor hervorragende Arbeit geleistet hat, um alle Aspekte zu analysieren und zu untersuchen, an die wir vorher noch nicht gedacht haben. Ich würde nicht bleiben, wir sind vom ursprünglichen Problem abgewichen, wir haben nur ein besseres Verständnis dafür.

Wir brauchen vielleicht einen besseren Namen dafür, ich würde es versuchen, aber ich verstehe diese Dinge normalerweise nicht richtig. Wie wäre es mit "Objektgenerika"?

@spion , nur für korrekte:

awaitObject<p,T[p]>(obj: {...p:Promise<T[p]>}): Promise<{...p:T[p]}>

(Sie haben ein Versprechen in der Ausgabesignatur verpasst)
-)

@ jods4 , ich stimme @spion zu
Es gibt viele reale Probleme, bei denen { ...p: T[p] } die Lösung ist. Um einen zu nennen: reagieren + Fluss / Redux

@spion @Artazor
Ich mache mir nur Sorgen, dass es ziemlich komplex wird, dies genau zu spezifizieren.

Die Motivation hinter diesem Thema hat sich verschoben. Ursprünglich ging es darum, APIs zu unterstützen, die eine Zeichenfolge zur Bezeichnung eines Objektfelds akzeptieren. Jetzt geht es hauptsächlich um APIs, die Objekte auf sehr dynamische Weise als Karten oder Datensätze verwenden. Beachten Sie, dass ich alles dafür bin, wenn wir zwei Fliegen mit einer Klappe schlagen können.

Wenn wir zu den APIs zurückkehren, bei denen das Problem mit Zeichenfolgen besteht, ist T[p] meiner Meinung nach keine vollständige Lösung (ich habe bereits gesagt, warum).

_Nur aus Gründen der Korrektheit_ awaitObject sollten auch Nicht-Promise-Eigenschaften akzeptiert werden, zumindest Bluebird props . Jetzt haben wir also:

awaitObject<p,T[p]>(obj: { ...p: T[p] | Promise<T[p]> }): Promise<T>

Ich habe den Rückgabetyp in Promise<T> geändert, da ich davon ausgehe, dass diese Notation funktioniert.
Es gibt andere Überladungen, z. B. eine, die ein Versprechen für ein solches Objekt abgibt (seine Signatur macht noch mehr Spaß). Das bedeutet, dass die Notation { ...p } als schlechtere Übereinstimmung angesehen werden muss als jeder andere Typ.

All dies zu spezifizieren wird harte Arbeit sein. Ich würde sagen, es ist der nächste Schritt, wenn Sie dies vorantreiben möchten.

@spion @ jods4

Ich wollte erwähnen, dass es bei dieser Funktion nicht um Generika und nicht um polymorphe Typen höherer Art geht. Dies ist einfach eine Syntax, um den Typ einer Eigenschaft durch einen enthaltenden Typ zu referenzieren (mit einigen "erweiterten" Erweiterungen, die unten beschrieben werden), was im Konzept nicht weit vom typeof . Beispiel:

type MyType = { abcd: number };
let y: MyType["abcd"]; // Technically this could also be written as MyType.abcd

Vergleichen Sie jetzt mit:

type MyType = { abcd: number };
let x: MyType;

let y: typeof x.abcd;

Es gibt zwei Hauptunterschiede zu typeof . Im Gegensatz zu typeof gilt dies eher für Typen (zumindest nicht primitive, eine Tatsache, die hier häufig weggelassen wird) als für Instanzen. Darüber hinaus (und dies ist eine wichtige Sache) wurde es erweitert, um die Verwendung von Literal-String-Konstanten (die zur Kompilierungszeit bekannt sein müssen) als Eigenschaftspfad-Deskriptoren und Generika zu unterstützen:

const propName = "abcd";
let y: MyType[propName];
// Or with a generic parameter:
let y: T[propName];

Technisch gesehen hätte typeof auch erweitert werden können, um String-Literale zu unterstützen (dies schließt den Fall ein, in dem x einen generischen Typ hat):

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

Und mit dieser Erweiterung könnte es auch verwendet werden, um einige der Zielfälle hier zu lösen:

function get<T>(propName: string, obj: T): typeof obj[propName]

typeof ist jedoch leistungsfähiger, da es eine unbegrenzte Anzahl von Verschachtelungsebenen unterstützt:

let y: typeof x.prop.childProp.deeperChildProp

Und dieser geht nur eine Ebene. Dh ich habe nicht geplant (soweit mir bekannt ist) zu unterstützen:

let y: MyType["prop"]["childProp"]["deeperChildProp"];
// Or alternatively
let y: MyType["prop.childProp.deeperChildProp"];

Ich denke, der Umfang dieses Vorschlags (wenn man ihn auf seiner Unbestimmtheitsebene überhaupt als Vorschlag bezeichnen kann) ist zu eng. Es kann helfen, ein bestimmtes Problem zu lösen (obwohl es möglicherweise alternative Lösungen gibt), was viele Menschen sehr eifrig zu machen scheint, es zu fördern. Es verbraucht jedoch auch wertvolle Syntax aus der Sprache. Ohne einen breiteren Plan und einen designorientierten Ansatz erscheint es unklug, schnell etwas einzuführen, das zum jetzigen Zeitpunkt noch nicht einmal eine klare Spezifikation hat.

_Edits: Einige offensichtliche Fehler in den Codebeispielen wurden korrigiert_

Ich habe ein bisschen über die Alternative typeof recherchiert:

Die zukünftige Sprachunterstützung für typeof x["abcd"] und typeof x[42] wurde genehmigt und fällt nun unter # 6606, das derzeit entwickelt wird (es gibt eine funktionierende Implementierung).

Das geht den halben Weg. Wenn diese vorhanden sind, kann der Rest in mehreren Phasen erledigt werden:

(1) Unterstützung für Zeichenfolgenliteralkonstanten (oder sogar numerische Konstanten - könnte für Tupel nützlich sein?) Als Eigenschaftsspezifizierer in Typausdrücken hinzufügen, z.

let x: MyType;
const propName = "abcd";
let y: typeof x[propName];

(2) Ermöglichen Sie die Anwendung dieser Bezeichner auf generische Typen

let x: T; // Where T should extend 'object'
const propName = "abcd";
let y: typeof x[propName];

(3) Ermöglichen Sie die Übergabe dieser Spezifizierer und "Instanziieren" der Referenzen in der Praxis durch Funktionsargumente ( typeof list[0] ist geplant, daher glaube ich, dass dies komplexere Fälle wie pluck abdecken könnte:

function get<T extends object>(obj: T, propertyName: string): typeof obj[propertyName];
function pluck<T extends object>(list: Array<T>, propertyName: string): Array<typeof list[0][propertyName]>;

( object Typ hier ist der bei # 1809 vorgeschlagene)

Obwohl leistungsfähiger (z. B. unterstützt möglicherweise typeof obj[propName][nestedPropName] ), ist es möglich, dass dieser alternative Ansatz möglicherweise nicht alle hier beschriebenen Fälle abdeckt. Bitte lassen Sie mich wissen, ob es Beispiele gibt, mit denen dies nicht zu tun scheint (ein Szenario, das mir in den Sinn kommt, ist, wenn die Objektinstanz überhaupt nicht übergeben wird. Es fällt mir jedoch schwer, mir einen Fall vorzustellen, in dem das wäre nötig, obwohl ich denke, dass das möglich ist).

_Edits: Einige Fehler im Code korrigiert_

@ Malibuzios
Einige Gedanken:

  • Dies behebt die API-Definition, hilft dem Anrufer jedoch nicht. Wir brauchen noch eine Art von nameof oder Expression auf der Anrufseite.
  • Es funktioniert für .d.ts -Definitionen, ist jedoch in TS-Funktionen / -Implementierungen im Allgemeinen nicht statisch ableitbar oder sogar überprüfbar.

Je mehr ich darüber nachdenke, desto mehr Expression<T, U> scheint die Lösung für diese Art von APIs zu sein. Es behebt die Probleme mit der Anrufstelle und die Eingabe des Ergebnisses und kann in einer Implementierung ableitbar + überprüfbar sein.

@ jods4

Sie haben in einem früheren Kommentar oben erwähnt, dass pluck mit Ausdrücken implementiert werden könnte. Obwohl ich mit C # sehr vertraut war, habe ich nie wirklich mit ihnen experimentiert, daher gebe ich zu, dass ich sie zu diesem Zeitpunkt nicht wirklich gut verstehe.

Ich wollte nur erwähnen, dass TypeScript Anstatt pluck , kann man einfach map und das gleiche Ergebnis erzielen, für das nicht einmal ein generischer Parameter angegeben werden muss ( es wird gefolgert) und würde auch eine vollständige Typprüfung und Vervollständigung geben:

let x = [{name: "John", age: 34}, {name: "Mary", age: 53}];

let result = x.map(obj => obj.name);
// 'result' is ["John", "Mary"] and its type inferred as 'string[]' 

Wobei map (zur Demonstration stark vereinfacht) als deklariert ist

map<U>(mapFunc: (value: T) => U): U[];

Das gleiche Inferenzmuster kann als "Trick" verwendet werden, um ein Ergebnis eines beliebigen Lambda (das nicht einmal aufgerufen werden muss) an eine Funktion zu übergeben und seinen Rückgabetyp darauf zu setzen:

function thisIsATrick<T, U>(obj: T, onlyPassedToInferTheReturnType: () => U): U {
   return;
}

let x = {name: "John", age: 34};

let result = thisIsATrick(x, () => x.age) // Result inferred as 'number' 

_Edit: Es könnte albern aussehen, es hier in einem Lambda zu übergeben und nicht nur das Ding selbst! In komplexeren Fällen wie verschachtelten Objekten (z. B. x.prop.Childprop ) kann es jedoch zu undefinierten oder Nullreferenzen kommen, die möglicherweise fehlerhaft sind. Wenn man es in einem Lambda hat, das nicht unbedingt genannt wird, wird das vermieden.

Ich gebe zu, dass ich mit einigen der hier diskutierten Anwendungsfälle nicht sehr vertraut bin. Persönlich hatte ich nie das Bedürfnis, eine Funktion aufzurufen, die einen Eigenschaftsnamen als Zeichenfolge verwendet. Das Übergeben eines Lambda (wo die von Ihnen beschriebenen Vorteile auch gelten) in Kombination mit der Typargumentschnittstelle reicht normalerweise aus, um viele häufig auftretende Probleme zu lösen.

Der Grund, warum ich den alternativen Ansatz typeof war hauptsächlich, dass er ungefähr dieselben Anwendungsfälle abdeckt und dieselbe Funktionalität für das bietet, was hier beschrieben wird. Würde ich es selbst benutzen? Ich weiß es nicht, hatte nie ein echtes Bedürfnis danach (es könnte einfach sein, dass ich fast nie externe Bibliotheken wie Unterstriche verwende, ich entwickle Dienstprogrammfunktionen fast immer selbst, auf der erforderlichen Basis).

@malibuzios Richtig , pluck ist ein bisschen albern mit Lambdas + map jetzt eingebaut.

In diesem Kommentar gebe ich 5 verschiedene APIs an, die alle Zeichenfolgen annehmen - sie sind alle mit der Beobachtung von Änderungen verbunden.

@ Jods4 und andere

Ich wollte nur erwähnen, dass nach Abschluss von # 6606 durch Implementierung nur von Stufe 1 (Verwendung konstanter String-Literale als Eigenschaftsspezifizierer) einige der hier benötigten Funktionen erreicht werden können, wenn auch nicht so elegant wie möglich (möglicherweise ist das erforderlich) Eigenschaftsname, der als Konstante deklariert werden soll (zusätzliche Anweisung hinzufügen).

function observeProperty<T, U>(obj: T, propName: string ): Subscriber<U> {
    ....
}

let x = { name: "John", age: 42 };

const propName = "age";
observeProperty<typeof x, typeof x[propName]>(x, propName);

Der Aufwand für die Implementierung ist jedoch möglicherweise erheblich geringer als bei der Implementierung von Stufe 2 und 3 (ich bin mir nicht sicher, aber es ist möglich, dass 1 bereits von # 6606 abgedeckt wird). Mit der Implementierung von Stufe 2 würde dies auch den Fall abdecken, in dem x einen generischen Typ hat (aber ich bin mir nicht sicher, ob dies wirklich benötigt wird?).

Bearbeiten: Der Grund, warum ich eine externe Konstante verwendet und den Eigenschaftsnamen nicht nur zweimal geschrieben habe, war nicht nur, die Eingabe zu reduzieren, sondern auch sicherzustellen, dass die Namen immer übereinstimmen, obwohl dies mit Tools wie "Umbenennen" und "Alle finden" immer noch nicht verwendet werden kann Referenzen ", was ich als ernsthaften Nachteil empfinde.

@ jods4 Ich suche immer noch nach einer besseren Lösung, bei der keine Zeichenfolgen verwendet werden. Ich werde versuchen zu überlegen, was mit nameof und seinen Varianten gemacht werden kann.

Hier ist eine andere Idee.

Da String-Literale bereits als Typen unterstützt werden, kann man zB schon schreiben:

let x: "HELLO"; 

Man kann eine an eine Funktion übergebene Literalzeichenfolge als eine Form der generischen Spezialisierung anzeigen

(_Edit: Diese Beispiele wurden korrigiert, um sicherzustellen, dass s im Funktionskörper unveränderlich ist, obwohl const an Parameterpositionen zu diesem Zeitpunkt nicht unterstützt wird (nicht sicher über readonly obwohl) ._):

function func(const s: string)

Der string type - Parameter im Zusammenhang mit s kann hier als impliziten generic betrachtet werden (wie es auf Stringliterale spezialisiert werden kann). Aus Gründen der Klarheit werde ich es expliziter schreiben (obwohl ich nicht sicher bin, ob es wirklich notwendig ist):

function func<S extends string>(const s: S)
func("TEST");

könnte sich intern spezialisieren auf:

function func(s: "TEST")

Und jetzt kann dies als alternative Möglichkeit verwendet werden, um den Eigenschaftsnamen zu "übergeben", was meiner Meinung nach die Semantik hier besser erfasst.

function observeProperty<T, S extends string>(obj: T, const propName: S): Subscriber<T[S]>

x = { name: "John",  age: 33};

observeProperty(x, nameof(x.age))

Da in T[S] sowohl T als auch S generische Parameter sind. Es erscheint natürlicher, zwei Typen an einer Typposition zu kombinieren, als Laufzeit- und Typbereichselemente zu mischen (z. B. T[someString] ).

_Edits: Umgeschriebene Beispiele mit einem unveränderlichen String-Parametertyp._

Da ich TS 1.8.7 und nicht die neueste Version verwende, war mir dies in neueren Versionen in nicht bewusst

const x = "Hello";

Der Typ von x wird bereits als der wörtliche Typ "Hello" (dh: x: "Hello" ), der sehr sinnvoll ist (siehe # 6554).

Wenn es also eine Möglichkeit gäbe, einen Parameter als const (oder vielleicht würde readonly auch funktionieren?):

function func<S extends string>(const s: S): S

Dann glaube ich, dass dies gelten könnte:

let result = func("abcd"); // type of 'result' inferred as the literal type "abcd"

Da einige davon ziemlich neu sind und auf den neuesten Sprachfunktionen beruhen, werde ich versuchen, sie so klar wie möglich zusammenzufassen:

(1) Wenn eine Zeichenfolgenvariable const (und möglicherweise auch readonly ?) Einen Wert erhält, der zur Kompilierungszeit bekannt ist, erhält die Variable automatisch einen Literaltyp mit demselben Wert ( Ich glaube, dies ist ein aktuelles Verhalten, das bei 1.8.x ) nicht auftritt, z

const x = "ABCD";

Die Art von x ist vermutlich "ABCD" , nicht string !, Z. B. kann man dies als x: "ABCD" .

(2) Wenn readonly Funktionsparameter zulässig wären, würde ein readonly -Parameter mit einem generischen Typ seinen Typ natürlich auf ein Zeichenfolgenliteral spezialisieren, wenn er als Argument empfangen wird, da der Parameter unveränderlich ist!

function func<S extends string>(readonly str: S);
func("ABCD");

Hier wurde S in den Typ "ABCD" , nicht in string !

Wenn der Parameter str jedoch nicht unveränderlich war, kann der Compiler nicht garantieren, dass er im Hauptteil der Funktion nicht neu zugewiesen wird. Daher wäre der abgeleitete Typ nur string .

function func<S extends string>(str: S) {
    str = "DCBA"; // This may happen
}

func("ABCD");

(3) Es ist möglich, dies auszunutzen und den ursprünglichen Vorschlag dahingehend zu ändern, dass der Eigenschaftsspezifizierer eine Referenz auf einen Typ ist , der auf eine Ableitung von string (in einigen Fällen) beschränkt werden müsste Es kann sogar angebracht sein, es nur auf Singleton-Literal-Typen zu beschränken, obwohl es derzeit keine Möglichkeit gibt, dies zu tun), anstatt auf eine Laufzeit-Entität:

function get<T extends object, S extends string>(obj: T, readonly propName: S): T[S]

Um dies aufzurufen, müssen keine Typargumente explizit angegeben werden, da TypeScript die Typargumentinferenz unterstützt:

let x = { name: "John", age: 42 };

get(x, "age"); // result type is inferred to be 'number'
// or for stronger type safety:
get(x, nameof(x.age)); // result type is inferred to be 'number'

_Edits: Einige Rechtschreib- und Codefehler wurden korrigiert._
_Hinweis: Eine verallgemeinerte und erweiterte Version dieses modifizierten Ansatzes wird jetzt auch in # 7730 verfolgt ._

Hier ist eine andere Verwendung des Typeigenschaftstyps (oder "indizierte Generika", wie ich sie in letzter Zeit gerne nenne), die in einer Diskussion mit @Raynos aufgetaucht ist

Wir können bereits den folgenden allgemeinen Prüfer für Arrays schreiben:

function tArray<T>(f:(t:any) => t is T) {
    return function (a:any): a is Array<T> {
        if (!Array.isArray(a)) return false;
        for (var k = 0; k < a.length; ++k)
            if (!f(a[k])) return false;
        return true;
    }
}

function tNumber(n:any): n is number {
    return typeof n === 'number'
}
var isArrayOfNumber = tArray(tNumber)

function test(x: {}) {
    if (isArrayOfNumber(x)) {
        return x[x.length - 1].toFixed(2); // this type checks
    }
}

Sobald wir Generika indiziert haben, können wir auch einen allgemeinen Prüfer für Objekte schreiben:

function tObject<p, T[p]>(checker: {...p: (t:any) => t is T[p]}) {
  return function(obj: any): obj is {...p: T[p] } {
    for (var key in checker) if (!checker[key](obj[key])) return false;
    return true;
  }
}

Zusammen mit den primitiven Prüfern für Zeichenfolge, Zahl, Boolescher Wert, Null und Undefiniert können Sie Folgendes schreiben:

var isTodoList = tObject({
  items: tArray(tObject({text: tString, completed: tBoolean})),
  showCompleted: tBoolean
})

und habe es Ergebnis mit der richtigen Laufzeitprüfung _und_ Kompilierzeit Typ Guard zur gleichen Zeit :)

Hat jemand schon daran gearbeitet oder ist es auf dem Radar von irgendjemandem? Dies ist eine wesentliche Verbesserung für die Anzahl der Standardbibliotheken, die Typisierungen verwenden können, einschließlich lodash oder ramda und vieler Datenbankschnittstellen.

@malibuzios Ich glaube, Sie nähern sich @Artazors Vorschlag :)

Um Ihre Bedenken auszuräumen:

function func<S extends string>(readonly str: S): T[str] {
 ...
}

Das wäre

function func<S extends string, T[S]>(str: S):T[S] { }

Auf diese Weise wird der Name an den spezifischsten Typ (einen String-Konstantentyp) gebunden, wenn Folgendes aufgerufen wird:

func("test")

Der Typ S wird zu "test" (nicht string ). Daher kann str kein anderer Wert zugewiesen werden. Wenn Sie das versucht haben, z

str = "other"

Der Compiler könnte einen Fehler erzeugen (es sei denn, es gibt Abweichungsprobleme;))

Anstatt nur den Typ einer Eigenschaft zu erhalten, möchte ich die Option haben, einen beliebigen Supertyp zu erhalten.

Mein Vorschlag ist daher, die folgende Syntax hinzuzufügen: Anstatt nur T[prop] , möchte ich die Syntax T[...props] hinzufügen. Wobei props eine Reihe von Mitgliedern von T . Und der resultierende Typ ist ein Supertyp von T mit Mitgliedern von T die in props .

Ich denke, es wäre sehr nützlich in Sequelize - einem beliebten ORM für node.js. Aus Sicherheitsgründen und aus Perfektionsgründen ist es ratsam, nur Attribute in einer Tabelle abzufragen, die Sie verwenden müssen. Das bedeutet oft super Typ eines Typs.

interface IUser {
    id: string;
    name: string;
    email: string;
    createdAt: string;
    updatedAt: string;
    password: string;
    // ...
}

interface Options<T> {
     attributes: (memberof T)[];
}

interface Model<IInstance> {
     findOne(options: Options<IInstance>): IInstance[...options.attributes];
}

declare namespace DbContext {
   define<T>(): Model<T>;
}

const Users = DbContext.define<IUser>({
   id: { type: DbContext.STRING(50), allowNull: false },
   // ...
});

const user = Users.findOne({
    attributes: ['id', 'email', 'name'],
    where: {
        id: 1,
    }
});

user.id
user.email
user.name

user.password // error
user.createdAt // error
user.updatedAt // error

(In meinem Beispiel enthält es den Operator memberof , wie Sie es erwarten, und auch den Ausdruck options.attributes , der mit typeof options.attributes identisch ist, aber ich denke, der typeof Operator

Wenn niemand darauf besteht, habe ich angefangen, daran zu arbeiten.

Was halten Sie von der Typensicherheit innerhalb der Funktion, dh stellen Sie sicher, dass eine return-Anweisung etwas zurückgibt, das einem return-Typ zugewiesen werden kann?

interface A {
     a: string;
}
function f(p: string): A[p] {
    return 'aaa'; // This is string, but can we ensure it is the intended A[p] ?
}

Auch der hier verwendete Name "Eigenschaftstyp" scheint etwas falsch. Es gibt an, dass der Typ Eigenschaften hat, die fast alle Typen haben.

Was ist mit "Property Referenced Type"?

Was halten Sie von der Typensicherheit innerhalb der Funktion?

Mein Brainstorming:

let a: A;
function f(p: string): A[p] {
  let x = a[p]; // typeof A[p], only when:
  // 1. p is directly referencing function argument
  // 2. function return type is Property Reference Type

  p = "abc"; // not allowed to assign a new value when p is used on Property Reference Type

  return x; // x is A[p], so okay
}

Und verbieten Sie normale Zeichenfolgen in der Rückleitung.

@malibuzios Problem mit Ihrer Idee ist, dass es erforderlich ist, alle Generika anzugeben, falls sie nicht abgeleitet werden können, was viral sein oder die Codebasis verschmutzen kann.

Irgendwelche Kommentare des TS-Teams zu https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@ RyanCavanaugh @ Mhegazy etc?

In Bezug auf den Namen habe ich begonnen, diese Funktion (zumindest die von @Artazor vorgeschlagene Form) "Indizierte Generika" zu nennen.

Eine Lösung aus einem anderen Blickwinkel könnte für dieses Problem sein. Ich bin mir nicht sicher, ob es schon gebracht wurde, es ist ein langer Faden. Durch die Entwicklung eines generischen String-Vorschlags könnten wir die Indexierungssignatur erweitern. Da String-Literale für den Indexertyp verwendet werden können, können diese äquivalent sein (da ich weiß, dass sie derzeit nicht vorhanden sind):

interface A1 {
    a: number;
    b: boolean;
}
interface A2 {
    [index: "a"]: number;
    [index: "b"]: boolean;
}

Also könnten wir dann einfach schreiben

declare function pluck<P, T extends { [indexer: P]: R; }, R>(obj: T, p: P): R;

Es gibt ein paar Dinge zu beachten:

  • Wie kann man angeben, dass P nur ein String-Literal sein kann?

    • Da String technisch alle String-Literale erweitert, wäre P extends string nicht sehr ideomatisch

    • Es wird derzeit darüber diskutiert, P super string Einschränkungen zuzulassen (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703).

    • Müssen wir uns wirklich darum kümmern? Wir können alles verwenden, was für den Indexertyp akzeptabel ist - wenn T einen Indexer von string s oder number s hat. dann kann P string oder number .

  • Wenn wir derzeit "something" als zweites Argument übergeben, ist es vom Typ string

    • String-Literale # 10195 könnten die Antwort sein

    • oder TS könnte daraus schließen, dass ein spezifischeres P verwendet werden sollte, wenn dies akzeptabel ist

  • Indexer sind implizit optional { [i: string]: number /* | undefined */ }

    • Wie kann man das durchsetzen, wenn wir wirklich nicht undefined in der Domain haben wollen?

  • Die automatische Typinferenz für alle Beziehungen zwischen P , T und R ist ein Schlüssel, damit dies funktioniert

@weswigham @mhegazy , und ich habe dies kürzlich diskutiert; Wir werden Sie über alle Entwicklungen informieren, auf die wir stoßen, und bedenken, dass dies nur ein Prototyp der Idee ist.

Aktuelle Ideen:

  • Ein keysof Foo -Operator zum Abrufen der Vereinigung von Eigenschaftsnamen aus Foo als String-Literal-Typen.
  • Ein Foo[K] -Typ, der angibt, dass es sich bei einem Typ K um einen String-Literal-Typ oder eine Vereinigung von String-Literal-Typen handelt.

Wenn Sie aus diesen Basisblöcken ein Zeichenfolgenliteral als geeigneten Typ ableiten müssen, können Sie schreiben

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}

Hier ist K ein Subtyp von keysof T was bedeutet, dass es sich um einen String-Literal-Typ oder eine Vereinigung von String-Literal-Typen handelt. Alles, was Sie für den Parameter key , sollte kontextbezogen durch dieses Literal / diese Vereinigung von Literalen eingegeben und als Singleton-String-Literal-Typ abgeleitet werden.

Zum Beispiel

interface HelloWorld { hello: any; world: any; }

function foo<K extends keysof HelloWorld>(key: K): K {
    return key;
}

// 'x' has type '"hello"'
let x = foo("hello");

Das größte Problem ist, dass keysof häufig den Betrieb "verzögern" muss. Es ist zu eifrig, wie ein Typ bewertet wird, was ein Problem für Typparameter ist, wie im ersten Beispiel, das ich gepostet habe (dh der Fall, den wir wirklich lösen wollen, ist eigentlich der schwierige Teil: Lächeln :).

Hoffe das gibt euch allen ein Update.

@ DanielRosenwasser Danke für das Update. Ich habe gerade gesehen, dass @weswigham eine PR über den keysof -Operator eingereicht hat, daher ist es vielleicht besser, dieses Problem an euch weiterzugeben.

Ich frage mich nur, warum Sie sich entschieden haben, von der ursprünglich vorgeschlagenen Syntax abzuweichen.

function get(prop: string): T[prop];

und keysof einführen?

T[prop] ist weniger allgemein und erfordert viele Interlaced-Maschinen. Eine große Frage hier ist, wie Sie den wörtlichen Inhalt von prop mit Eigenschaftsnamen von T Beziehung setzen würden. Ich bin mir nicht mal ganz sicher, was du tun würdest. Würden Sie einen impliziten Typparameter hinzufügen? Müssen Sie das kontextbezogene Schreibverhalten ändern? Müssen Sie Signaturen etwas Besonderes hinzufügen?

Die Antwort lautet wahrscheinlich Ja zu all diesen Dingen. Ich habe uns davon abgehalten, weil mir mein Bauch sagte, es sei besser, zwei einfachere, getrennte Konzepte zu verwenden und von dort aus aufzubauen. Der Nachteil ist, dass es in bestimmten Fällen etwas mehr Boilerplate gibt.

Wenn es neuere Bibliotheken gibt, die diese Art von Mustern verwenden und diese Boilerplate es ihnen schwer macht, in TypeScript zu schreiben, sollten wir das vielleicht in Betracht ziehen. Insgesamt ist diese Funktion jedoch in erster Linie für Bibliothekskonsumenten gedacht, da Sie auf der Nutzungsseite hier ohnehin die Vorteile nutzen können.

@ DanielRosenwasser Nachdem ich kaum das Kaninchenloch hinuntergegangen @ SaschaNaz- Idee finden. Ich denke, keysof ist in diesem Fall überflüssig. T[p] bezieht sich bereits darauf, dass p eine der wörtlichen Requisiten von T .

Mein grober Implementierungsgedanke war, einen neuen Typ namens PropertyReferencedType einzuführen.

export interface PropertyReferencedType extends Type {
        property: Symbol;
        targetType: ObjectType;
}

Wenn Sie eine Funktion eingeben, die mit einem Rückgabetyp deklariert ist, der PropertyReferencedType oder wenn Sie eine Funktion eingeben, die auf PropertyReferencedType verweist: Ein Typ von ElementAccessExpression wird durch eine Eigenschaft ergänzt, die auf die verweist Symbol der Eigenschaft, auf die zugegriffen wird.

export interface Type {
        flags: TypeFlags;                // Flags
        /* <strong i="20">@internal</strong> */ id: number;      // Unique ID
        //...
        referencedProperty: Symbol; // referenced property
}

Ein Typ mit einem referenzierten Eigenschaftssymbol kann also einem PropertyReferencedType . Während der Überprüfung müssen die referencedProperty p in T[p] . Außerdem muss der übergeordnete Typ eines Elementzugriffsausdrucks T zuweisbar sein. Und um die Sache einfacher zu machen, muss p auch const sein.

Der neue Typ PropertyReferencedType existiert nur innerhalb der Funktion als "ungelöster Typ". Auf Abruf muss der Typ mit p :

interface A { a: string }
declare function getProp(p: string): A[p]
getProp('a'); // string

Ein PropertyReferencedType nur über Funktionszuweisungen weitergegeben und kann nicht über Aufrufausdrücke weitergegeben werden, da ein PropertyReferencedType nur ein temporärer Typ ist, der bei der Überprüfung des Funktionskörpers mit dem Rückgabetyp T[p] helfen soll

Wenn Sie Operatoren vom Typ keysof und T[K] einführen, bedeutet dies, dass wir sie folgendermaßen verwenden könnten:

interface A {
  a: number;
  b: string;
}
type AK = keysof A; // "a" | "b"
type AV = A[AK]; // number | string ?
type AA = A["a"]; // number ?
type AB = A["b"]; // string ?
type AC = A["c"]; // error?
type AN = A[number]; // error?

type X1 = keysof { [index: string]: number; }; // string ?
type X2 = keysof { [index: string]: number; [index: number]: string; }; // string | number ?

@ DanielRosenwasser hätte dein Beispiel nicht die gleiche Bedeutung mit meinem

function foo<T, K extends keysof T>(obj: T, key: K): T[K] {
    // ...
}
// same as ?
function foo<K, V, T extends { [k: K]: V; }>(obj: T, key: K): V {
    // ...
}

Ich sehe nicht, wie die Signatur für Underscores _.pick :

o2 = _.pick(o1, 'p1', 'p2');

pick(Object, ...props: String[]) : WHAT GOES HERE;

@rtm Ich schlug es in https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380 vor. Obwohl es vielleicht besser ist, eine neue Ausgabe zu eröffnen, obwohl sie mit dieser zusammenhängt.

Implementierung jetzt in # 11929 verfügbar.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen