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.
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
prop
Offensichtlich muss die Konstante von einem Typ sein, der als Indextyp verwendet werden kann ( string
, number
, Symbol
).
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
.
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?
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.
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 sichmembertypeof 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
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:
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:
typeof
und verwendet einen vorhandenen Mechanismus zur Typprüfung, um ihn zu validieren, anstatt ihn zur Kompilierungszeit aus einer Zeichenfolgendarstellung konvertieren zu müssen.typeon
(könnte es aber unterstützen).+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:
console.log(res.a[0].toFixed(2))
wird vom Compiler abgelehnt.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
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
Beispiel:
declare function mapValues<p extends string, T1[p], T2[p]>(
obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
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 Datenstrukturen | Zugang durch | ||
---|---|---|---|
ein. Position (Nummer) | b. Schlüssel (Zeichenfolge) | ||
Graf von Artikel | 1. Variable (die gleiche Rolle für jeden Artikel) | Liste | Wörterbuch |
2. behoben (Jeder Gegenstand spielt seine eigene Rolle) | Tupel | Aufzeichnung |
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 dasArray
, da der einzige "wahre" Tupeltyp der Typ desarguments
Objekts ist, das ist keinArray
. Trotzdem sind sie mehr oder weniger austauschbar, insbesondere im Zusammenhang mit der Destrukturierung und demFunction.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, einRecord
und ein limitiertesDictionary
(mit) zu kombinieren Nur String-Schlüssel) unter der gleichen Haube vonObject
(wobeiobj.prop
demobj["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 Programmierebene | Zugang 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
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 TypenT
! 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:
string
weder "Alle Referenzen finden" noch umgestaltbar sein (z. B. umbenennen).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:
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] {
...
}
name
zur Kompilierungszeit nicht bekannt, null oder undefiniert ist, z. B. func<MyType>(getRandomString())
, func<MyType>(undefined)
.T
ein primitiver Typ wie number
oder null
.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?T["propName"]
oder T[propName]
auch alleine? Ohne Verweis auf einen Parameter oder eine Variable meine ich - es könnte nützlich sein!T[name]
in anderen Parametern oder sogar außerhalb von Funktionsbereichen verwendet werden kann, zfunction 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:
Ü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:
nameof
oder Expression
auf der Anrufseite..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:
P
nur ein String-Literal sein kann?P extends string
nicht sehr ideomatischP super string
Einschränkungen zuzulassen (# 7265, # 6613, https://github.com/Microsoft/TypeScript/issues/6613#issuecomment-175314703).T
einen Indexer von string
s oder number
s hat. dann kann P
string
oder number
."something"
als zweites Argument übergeben, ist es vom Typ string
P
verwendet werden sollte, wenn dies akzeptabel ist{ [i: string]: number /* | undefined */ }
undefined
in der Domain haben wollen?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:
keysof Foo
-Operator zum Abrufen der Vereinigung von Eigenschaftsnamen aus Foo
als String-Literal-Typen.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.
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:
keysof Foo
-Operator zum Abrufen der Vereinigung von Eigenschaftsnamen ausFoo
als String-Literal-Typen.Foo[K]
-Typ, der angibt, dass es sich bei einem TypK
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
Hier ist
K
ein Subtyp vonkeysof 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 Parameterkey
, sollte kontextbezogen durch dieses Literal / diese Vereinigung von Literalen eingegeben und als Singleton-String-Literal-Typ abgeleitet werden.Zum Beispiel
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.