Typescript: Suggestion: Type Type de propriété

Créé le 28 nov. 2014  ·  76Commentaires  ·  Source: microsoft/TypeScript

Motivations

De nombreuses bibliothèques / frameworks / modèles JavaScript impliquent un calcul basé sur le nom de propriété d'un objet. Par exemple, le modèle Backbone , la transformation fonctionnelle pluck , ImmutableJS sont tous basés sur un tel mécanisme.

//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'];

Nous pouvons facilement comprendre dans ces exemples la relation entre l'API et la contrainte de type sous-jacente.
Dans le cas du modèle backbone, il s'agit simplement d'une sorte de _proxy_ pour un objet de type:

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

Pour le cas de pluck , c'est une transformation

T[] => U[]

où U est le type d'une propriété de T prop .

Cependant, nous n'avons aucun moyen d'exprimer une telle relation dans TypeScript, et nous nous retrouvons avec un type dynamique.

Solution proposée

La solution proposée est d'introduire une nouvelle syntaxe pour le type T[prop]prop est un argument de la fonction utilisant un type comme valeur de retour ou paramètre de type.
Avec cette nouvelle syntaxe de type, nous pourrions écrire la définition suivante:

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][] 

De cette façon, lorsque nous utilisons notre modèle Backbone, TypeScript pourrait correctement vérifier le type set appel get et set .

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

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

La constante prop

Contrainte

Évidemment, la constante doit être d'un type qui peut être utilisé comme type d'index ( string , number , Symbol ).

Cas d'indexable

Jetons un coup d'œil à notre définition de Map :

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

Si T est indexable, notre carte hérite de ce comportement:

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

Maintenant get a pour type get(prop: string): number .

Interrogatoire

Maintenant, il y a des cas où j'ai du mal à penser à un comportement _correct_, recommençons avec notre définition Map .
Si au lieu de passer { [index: string]: number } comme paramètre de type, nous aurions donné
{ [index: number]: number } le compilateur doit-il générer une erreur?

si nous utilisons pluck avec une expression dynamique pour prop au lieu d'une constante:

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

ou avec une constante qui n'est pas une propriété du type passé en paramètre.
si l'appel à pluck lève une erreur puisque le compilateur ne peut pas déduire le type T[prop] , devrait T[prop] être résolu en {} ou any , si oui, le compilateur avec --noImplicitAny devrait-il générer une erreur?

Fixed Suggestion help wanted

Commentaire le plus utile

@weswigham @mhegazy , et j'en ai discuté récemment; nous vous informerons de tous les développements que nous rencontrons, et gardez à l'esprit qu'il ne s'agit que d'un prototypage de l'idée.

Idées actuelles:

  • Un opérateur keysof Foo pour récupérer l'union des noms de propriétés de Foo tant que types littéraux de chaîne.
  • Un type Foo[K] qui spécifie que pour certains types K c'est une chaîne de types littéraux ou une union de types littéraux chaîne.

À partir de ces blocs de base, si vous devez déduire un littéral de chaîne comme type approprié, vous pouvez écrire

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

Ici, K sera un sous-type de keysof T ce qui signifie qu'il s'agit d'un type littéral de chaîne ou d'une union de types de littéraux de chaîne. Tout ce que vous passez pour le paramètre key doit être typé contextuellement par ce littéral / union de littéraux, et déduit comme un type littéral de chaîne singleton.

Par exemple

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

Le plus gros problème est que keysof doit souvent "retarder" son fonctionnement. Il est trop désireux de savoir comment il évalue un type, ce qui est un problème pour les paramètres de type comme dans le premier exemple que j'ai posté (c'est-à-dire que le cas que nous voulons vraiment résoudre est en fait le plus difficile: sourire :).

J'espère que cela vous donne à tous une mise à jour.

Tous les 76 commentaires

Copie possible du n ° 394

Voir également https://github.com/Microsoft/TypeScript/issues/1003#issuecomment -61171048

@NoelAbrahams Je ne pense vraiment pas que ce soit un doublon de # 394, au contraire les deux fonctionnalités sont assez complémentaires quelque chose comme:

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

Serait idéal

@fdecampredon

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

Que faire dans ce cas?

C'est plus ou moins le même cas que celui du dernier paragraphe de mon numéro. Comme je l'ai dit, nous avons le choix multiple, nous pouvons signaler une erreur, ou déduire any pour T[prop] , je pense que la deuxième solution est plus logique

Excellente proposition. D'accord, ce serait une fonctionnalité utile.

@fdecampredon , je pense que c'est un doublon. Voir le commentaire de Dan et la réponse correspondante qui contient la suggestion pour membertypeof .

OMI tout cela est une nouvelle syntaxe pour un cas d'utilisation plutôt étroit.

@NoelAbrahams ce n'est pas la même chose.

  • memberof T renvoie le type dont l'instance ne peut être qu'une chaîne avec un nom de propriété valide de T instance.
  • T[prop] renvoie le type de la propriété de T nommée avec une chaîne qui est représentée par prop argument / variable.

Il y a un brifge à memberof ce type de paramètre prop devrait être memberof T .

En fait, j'aimerais avoir un système plus riche pour l'inférence de type basé sur les métadonnées de type. Mais un tel opérateur est un bon début ainsi que memberof .

C'est intéressant et souhaitable. TypeScript ne fonctionne pas encore bien avec les frameworks lourds et cela aiderait évidemment beaucoup.

TypeScript ne fonctionne pas bien avec les frameworks lourds

Vrai. Cela ne change toujours pas le fait qu'il s'agit d'une suggestion en double.

encore et cela aiderait évidemment beaucoup [pour taper des frameworks lourds]

Je ne suis pas sûr de ça. Semble plutôt fragmentaire et quelque peu spécifique au modèle d'objet proxy décrit ci-dessus. Je préférerais de loin une approche plus holistique du problème des cordes magiques dans le sens du # 1003.

1003 suggère any comme type de retour d'un getter. Cette proposition ajoute en plus qu'en ajoutant un moyen de rechercher également le type de valeur - le mélange aboutirait à quelque chose comme ceci:

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

@spion ,

J'ai pensé au type de retour, mais je l'ai laissé de côté pour ne pas rendre la suggestion globale trop grosse.

C'était ma première pensée mais cela pose des problèmes. Que faire s'il y a plusieurs arguments de type memberof T , auquel se réfère membertypeof T ?

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

Cela résout le problème "à quel argument je me réfère", mais le nom membertypeof semble faux et n'est pas un fan de l'opérateur ciblant le nom de la propriété.

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

Je pense que cela fonctionne mieux.

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

Malheureusement, je ne suis pas sûr d'avoir une excellente solution, même si je pense que la dernière suggestion a un potentiel décent.

OK @NoelAbrahams il y avait un commentaire dans # 394 qui essayait de décrire plus ou moins la même chose que celui-ci.
Maintenant, je pense que T[prop] est peut-être un peu plus élégant que les différentes propositions de ce commentaire, et que la proposition de ce numéro va peut-être un peu plus loin dans la réflexion.
Pour ces raisons, je ne pense pas qu'il devrait être fermé en double.
Mais je suppose que je suis partial puisque c'est moi qui ai écrit le numéro;).

@fdecampredon , plus on est de fous: smiley:

@NoelAbrahams oups, j'ai raté cette partie. Bien sûr, ceux-ci sont à peu près équivalents (celui-ci ne semble pas introduire un autre paramètre générique, ce qui peut ou non être un problème)

Après avoir jeté un coup d'œil à Flow, je pense que ce serait plus élégant avec un système de type un peu plus fort et des types spéciaux plutôt qu'un rétrécissement de type ad hoc.

Ce que nous voulons dire avec get(prop: string): Contact[prop] par exemple n'est qu'une série de surcharges possibles:

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

// is morally equivalent to 

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

En supposant l'existence de l'opérateur de type & (types d'intersection), ce type est équivalent à

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

Maintenant que nous avons traduit notre cas non générique en expression de types uniquement sans traitement spécial (pas de [prop] ), nous pouvons aborder la question des paramètres.

L'idée est de générer un peu ce type à partir d'un paramètre de type. Nous pourrions définir des types génériques fictifs spéciaux $MapProperties , $Name et $Value pour exprimer notre type généré en terme d'expression de type uniquement (pas de syntaxe spéciale) tout en indiquant le vérificateur de type que quelque chose doit être fait.

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

Cela peut sembler compliqué et proche de la création de modèles ou des types dépendants de l'homme pauvre, mais cela ne peut pas être évité lorsque quelqu'un veut que les types dépendent des valeurs.

Un autre domaine où cela serait utile est l'itération sur les propriétés d'un objet typé:

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

Ces types devraient être au moins théoriquement possibles à déduire (il y a suffisamment d'informations sur les types pour inférer le résultat de la boucle for ), mais c'est aussi probablement assez compliqué.

Notez que la prochaine version de react bénéficierait grandement de cette approche, voir https://github.com/facebook/react/issues/3398

Comme https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -64944856, lorsque la chaîne est fournie par une expression autre qu'une chaîne littérale, cette fonctionnalité tombe rapidement en panne en raison du problème d'arrêt. Cependant, une version de base de ceci peut-elle encore être implémentée en utilisant le problème d'apprentissage de ES6 Symbol (# 2012)?

Approuvé.

Nous voudrons essayer ceci dans une branche expérimentale pour avoir une idée de la syntaxe.

Vous vous demandez simplement quelle version de la proposition va être mise en œuvre? C'est-à-dire que T[prop] va être évalué sur le site d'appel et remplacé avec empressement par un type concret ou va-t-il devenir une nouvelle forme de variable de type?

Je pense que nous devrions nous appuyer sur une syntaxe plus générale et moins verbeuse telle que définie dans # 3779.

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

Ou n'est-il pas possible de déduire le type de A?

Je veux juste dire que j'ai créé un petit outil de codegen pour faciliter l'intégration TS avec ImmutableJS en attendant la solution normale: https://www.npmjs.com/package/tsimmutable. C'est assez simple, mais je pense que cela fonctionnera pour la plupart des cas d'utilisation. Peut-être que cela aidera quelqu'un.

Je tiens également à noter que la solution avec un type de membre peut ne pas fonctionner avec ImmutableJS:

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 Quelque chose comme ça pourrait fonctionner:

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>

Cela n'exclut pas les nœuds DOM ou les objets Date, mais il ne devrait pas du tout être autorisé à les utiliser dans des structures immuables. https://github.com/facebook/immutable-js/wiki/Converting-from-JS-objects

Travailler progressivement à partir de la meilleure solution de contournement

Je pense qu'il est utile de commencer par la meilleure solution de contournement actuellement disponible, puis de la travailler progressivement à partir de là.

Disons que nous avons besoin d'une fonction qui renvoie la valeur d'une propriété pour tout objet non primitif contenant:

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

Maintenant, nous voulons que la valeur de retour ait le type de la propriété cible, donc un autre paramètre de type générique pourrait être ajouté pour son type:

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

Ainsi, un cas d'utilisation avec une classe ressemblerait à ceci:

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

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

Et result recevrait correctement le type number .

Cependant, il semble y avoir une référence dupliquée à «membre» dans l'appel: l'une est dans le paramètre type, et l'autre est une chaîne _literal_ qui recevra toujours la représentation sous forme de chaîne du nom de la propriété cible. L'idée de cette proposition est que ces deux peuvent être unifiés en un seul paramètre de type qui ne recevrait que la représentation sous forme de chaîne.

Il faut donc observer ici que la chaîne agirait également comme un _ paramètre générique_, et devra être passée comme un littéral pour que cela fonctionne (d'autres cas pourraient ignorer silencieusement sa valeur à moins qu'une forme de réflexion d'exécution ne soit disponible). Donc, pour clarifier la sémantique, il doit y avoir un moyen de signifier une relation entre le paramètre générique et la chaîne:

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

Et maintenant, les appels ressembleraient à:

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

Ce qui se résoudrait en interne à:

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

Cependant, puisque C contient à la fois une instance et une propriété statique nommée member , l'expression générique de type supérieur T[PName] est ambiguë. Puisque l'intention ici est principalement de l'appliquer aux propriétés des instances, une solution comme l' opérateur proposé T[PName] est interprété par certains comme représentant une _valeur reference_, pas nécessairement un type:

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

(Cela fonctionnerait également si T est un type d'interface compatible, car typeon prend également en charge les interfaces)

Pour obtenir la propriété statique, la fonction serait appelée avec le constructeur lui-même et le type de constructeur devrait être passé comme typeof C :

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

(En interne typeon (typeof C) résout en typeof C donc cela fonctionnerait)
result recevra désormais correctement le type string .

Pour prendre en charge des types supplémentaires d'identificateurs de propriété, cela peut être réécrit comme suit:

type PropertyIdentifier = string|number|Symbol;

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

Il y a donc plusieurs fonctionnalités différentes nécessaires pour prendre en charge cela:

  • Autorise la transmission de valeurs littérales en tant que paramètres génériques.
  • Autorisez ces littéraux à recevoir la valeur des arguments de fonction.
  • Autoriser une forme de génériques de type supérieur.
  • Permet de référencer un type de propriété via un paramètre générique littéral sur un type _generic_ via des génériques de type supérieur

Repenser le problème

Revenons maintenant à la solution de contournement d'origine:

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

Cette solution de contournement présente un avantage, c'est qu'elle peut être facilement étendue pour prendre en charge des noms de chemin plus complexes, référençant non seulement les propriétés directes, mais aussi les propriétés des objets imbriqués:

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

    return <P> resultValue;
}

et maintenant:

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

result obtiendrait correctement le type string[] ici.

La question est de savoir si les chemins imbriqués doivent également être pris en charge? La fonctionnalité sera-t-elle vraiment utile si aucune saisie automatique n'est disponible lors de la saisie?

Cela suggère qu'une solution différente pourrait être possible, qui fonctionnerait dans l'autre sens. Retour à la solution de contournement d'origine:

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

En regardant cela dans l'autre sens, il est possible de prendre la référence de type elle-même et de la convertir en chaîne:

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

@typeReferencePathOf est similaire dans son concept à nameOf mais s'applique aux paramètres génériques. Il prendrait la référence de type reçu typeof instance.member (ou bien typeon C.member ), extrairait le chemin member la propriété "member" lors de la compilation temps. Un complément moins spécialisé @typeReferenceOf se résoudrait en la chaîne complète "typeof instance.member" .

Alors maintenant:

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

Se résoudrait à:

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

Et une implémentation similaire pour getPath :

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

Se résoudrait à:

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

Puisque @typeReferencePathOf(P) n'est défini que comme valeur par défaut. L'argument peut être énoncé manuellement si nécessaire:

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

Si le second paramètre de type n'est pas fourni, @typeReferencePathOf() pourrait se résoudre en undefined ou bien la chaîne vide "" . Si un type était fourni mais n'avait pas de chemin interne, il se résoudrait en "" .

Avantages de cette approche:

  • Ne nécessite pas de génériques de type supérieur.
  • Ne nécessite pas d'autoriser les littéraux comme paramètres génériques.
  • Ne nécessite aucune extension des génériques.
  • Permet l'auto-complétion lors de la saisie de l'expression typeof et utilise un mécanisme existant de vérification de type pour la valider au lieu d'avoir besoin de la convertir à partir d'une représentation sous forme de chaîne au moment de la compilation.
  • Peut également être utilisé dans des classes génériques.
  • Ne nécessite pas typeon (mais pourrait le supporter).
  • Peut être appliqué dans d'autres scénarios.

+1

Agréable! J'ai déjà commencé à rédiger une grande proposition pour résoudre le même problème, mais j'ai trouvé cette discussion, alors discutons ici.

Voici un extrait de mon problème non soumis:

Question : Comment la fonction suivante doit-elle être déclarée dans .d.ts pour être utilisable dans des contextes où elle est censée être utilisée?

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

Cette fonction (avec de légères variations) peut être trouvée dans presque toutes les bibliothèques

Il existe deux cas d'utilisation distincts du mapValues dans la nature:

une. _Object-as-a-_ _Dictionary _ où les noms de propriétés sont dynamiques, les valeurs ont le même type et la fonction est en général monomorphe (consciente de ce type)

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 _ où chaque propriété a son propre type, tandis que la fonction est paramétriquement polymorphe (par implémentation)

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"

La meilleure instrumentation disponible pour le mapValues avec TypeScript actuel est:

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

Il n'est pas difficile de voir que cette signature correspond parfaitement au cas d'utilisation _Object-as-a-_ _Dictionary _, tout en produisant un résultat 1 quelque peu surprenant de l'inférence de type lorsqu'elle est appliquée dans le cas où _Object-as-a-_ _Record _ était l'intention.

Le type {p1: T1, p2: T2, ...pn: Tn} sera contraint au {[key: string]: T1 | T2 | ... | Tn } confondant tous les types et supprimant efficacement toutes les informations sur la structure d'un enregistrement:

  • le bon et attendu 2 pour être bien console.log(res.a[0].toFixed(2)) sera rejeté par le compilateur;
  • le console.log((<number>res['a'][0]).toFixed(2)) accepté a deux emplacements incontrôlés et sujets aux erreurs: le nom de propriété de type cast et arbitraire.

1, 2 - pour les programmeurs qui migrent de l'ES vers TS


Étapes possibles pour résoudre ce problème (et en résoudre d'autres également)

1. Introduisez des enregistrements variadiques à l'aide de types littéraux de chaîne

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. Autoriser les types formels à être indexés avec d'autres types formels qui sont contraints d'étendre 'string'

exemple:


declare function mapValues<p extends string, T1[p], T2[p]>(
     obj:{...p: T1[p]}, fn:(arg: T1[p]) => T2[p]
): {...p: T2[p]};
3. Permettre la déstructuration des types formels

exemple:

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

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

avec les trois étapes ensemble, nous pourrions écrire

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]> 

Je sais que la syntaxe est plus verbeuse que celle proposée par @fdecampredon
mais je ne peux pas imaginer comment exprimer le type du mapValues ou du combineReducers avec la proposition originale.

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

avec ma proposition, sa déclaration ressemblera à:

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

Est-il exprimable avec la proposition originale?

@spion , @fdecampredon ?

@Artazor Certainement une amélioration. Cela semble maintenant assez bon pour exprimer mon cas d'utilisation de référence de cette fonctionnalité:

En supposant que user.id et user.name sont des types de colonnes de base de données contenant number et string conséquence soit Column<number> et Column<string>

Écrivez le type d'une fonction select qui prend:

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

et renvoie Query<{id: number; name: string}>

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

Je ne sais pas comment cela va être vérifié et déduit. On dirait qu'il pourrait y avoir un problème, car le type T n'existe pas. Le type de retour doit-il être exprimé sous une forme déstructurée? c'est à dire

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

@Artazor a oublié de demander, le paramètre p extends string est-il vraiment nécessaire?

Est-ce que quelque chose comme ça ne serait pas suffisant?

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

ie pour toutes les variables de type T [p] dans {...p:Column<T[p]>}

edit: une autre question, comment cela va-t-il être vérifié pour l'exactitude?

@spion J'ai votre point de vue - vous m'avez mal compris (mais c'est de ma faute), par le T[p] j'ai voulu dire une chose complètement différente, et vous verrez que p extends string est une chose cruciale ici. Laissez-moi vous expliquer mes pensées en profondeur.

Considérons quatre cas d'utilisation pour les structures de données: List , Tuple , Dictionary et Record . Nous les désignerons sous le nom de _ structures de données conceptuelles_ et les décrirons avec les propriétés suivantes:

Structures de données conceptuellesAccès par
une. position
(nombre)
b. clé
(chaîne)
Compte de
articles
1. variable
(le même rôle pour chaque élément)
listedictionnaire
2. fixe
(chaque élément joue son propre rôle unique)
TupleRecord

En JavaScript, ces quatre cas sont soutenus par deux formes de stockage: un Array - pour a1 et a2 , et un Object - pour b1 et b2 .

_Note 1 _. Pour être honnête, il n'est pas correct de dire que le Tuple est soutenu par le Array , car le seul type de tuple "vrai" est le type de l'objet arguments qui n'est pas un Array . Néanmoins, ils sont plus ou moins interchangeables, notamment dans le cadre de la déstructuration et du Function.prototype.apply qui ouvre la porte à la méta-programmation. Le TypeScript exploite également les tableaux pour modéliser les tuples.

_Note 2 _. Alors que le concept complet du dictionnaire avec des clés arbitraires a été introduit des décennies plus tard sous la forme de l'ES6 Map , la décision initiale de Brendan Eich de confondre un Record et un Dictionary limité (avec clés de chaîne uniquement) sous le même capot de Object (où obj.prop équivaut à obj["prop"] ) est devenu l'élément le plus controversé de tout le langage (à mon avis).
C'est à la fois la malédiction et la bénédiction de la sémantique JavaScript. Cela rend la réflexion triviale et encourage les programmeurs à basculer librement entre les niveaux _programming_ et _meta-programmation_ à un coût mental presque nul (même sans le remarquer!). Je crois que c'est la partie essentielle du succès de JavaScript en tant que langage de script.

Il est maintenant temps pour le TypeScript de fournir le moyen d'exprimer des types pour cette sémantique étrange. Quand on pense au niveau de la _programmation_, tout va bien:

Types au niveau de la programmationAccès par
une. position
(nombre)
b. clé
(chaîne)
Compte de
articles
1. variable
(le même rôle pour chaque élément)
T []{[clé: chaîne]: T}
2. fixe
(chaque élément joue son propre rôle unique)
[T1, T2, ...]{clé1: T1, clé2: T2, ...}

Cependant, lorsque nous passons au niveau _meta-programmation_, alors les choses qui étaient fixées au niveau _programming_ deviennent soudainement variables! Et ici, nous reconnaissons soudainement les relations entre les tuples et les signatures de fonction et proposons des choses comme # 5453 (Variadic Kinds) qui ne couvre en fait qu'une petite partie (mais assez importante) des besoins de métaprogrammation - la concaténation des signatures en donnant la possibilité de déstructurer un paramètre dans les types des rest-args:

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

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

Dans le cas, si # 6018 sera également implémenté, la classe Function pourrait ressembler à

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

C'est génial mais de toute façon incomplet.

Par exemple, imaginez que nous souhaitons attribuer un type correct à la fonction suivante:

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

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

Avec les types variadiques proposés, nous ne pouvons pas transformer les types de signature. Ici, nous avons besoin de quelque chose de plus puissant. En fait, il est souhaitable d'avoir des fonctions de compilation qui peuvent fonctionner sur des types (comme dans le Flow de Facebook). Cependant, je suis sûr que cela ne sera possible que lorsque (et si) le système de type du TypeScript serait suffisamment stable pour l'exposer aux programmeurs finaux sans risque significatif de casser un utilisateur lors d'une prochaine mise à jour mineure. Ainsi, nous avons besoin de quelque chose de moins radical que le support complet de la méta-programmation mais toujours capable de traiter les problèmes démontrés.

Pour résoudre ce problème, je souhaite introduire la notion de _objet signature_. En gros c'est soit

  1. l'ensemble des noms de propriété de l'objet, ou
  2. l'ensemble des clés du dictionnaire, ou
  3. l'ensemble des index numériques d'un tableau
  4. l'ensemble des positions numériques d'un tuple.

Idéalement, il serait bien d'avoir des types littéraux entiers ainsi que des types littéraux chaîne pour représenter les signatures des tableaux ou des tuples comme

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

les règles pour les littéraux entiers sont congruentes aux littéraux de chaîne;

Ensuite, nous pouvons introduire la syntaxe d'abstraction de signature, qui correspond exactement à la syntaxe rest / spread de l'objet:

{...Props: T }

à une exception près: ici Props est l'identifiant lié sous le quantificateur universel:

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

ainsi, il est introduit dans la portée lexicale de type et peut être utilisé n'importe où dans cette portée comme nom du type. Cependant, son utilisation est double: lorsqu'il est utilisé à la place du nom de propriété repos / propagation, il représente l'abstraction sur la signature de l'objet lorsqu'il est utilisé comme un type autonome, il représente un sous-type de la chaîne (ou du nombre respectivement).

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

Et voici la partie la plus sophistiquée: _Key Dependent Types_

nous introduisons une construction de type spéciale: T for Prop (j'aimerais utiliser cette syntaxe plutôt que T [Prop] qui vous a confondu) où Prop est le nom de la variable de type qui contient l'abstraction de signature de l'objet. Par exemple <Prop extends string, T for Prop> introduit deux types formels dans la portée lexicale de type, le Prop et le T où il est connu que pour chaque valeur particulière p de Prop il y aura son propre type T .

Nous ne disons pas que quelque part se trouve un objet qui a des propriétés Props et leurs types sont T ! Nous introduisons uniquement une dépendance fonctionnelle entre deux types. Le type T est corrélé aux membres de type Props, et c'est tout!

Cela nous donne la possibilité d'écrire des choses telles que

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

cependant, le deuxième paramètre n'est pas traité comme un objet mais plutôt comme une carte abstraite des identificateurs vers les types. Dans cette perspective, T for P peut être utilisé pour représenter des séquences abstraites de types pour des tuples lorsque P est un sous-type de nombre ( @JsonFreeman ?)

Lorsque T est utilisé quelque part dans {...P: .... T .... } il représente exactement un type particulier de cette carte.

C'est mon idée principale.
Attendant avec impatience toutes les questions, pensées, critiques -)

Bon, donc extends string est de prendre en compte les tableaux (et les arguments variadiques) dans un cas, et les constantes de chaîne (en tant que types) dans l'autre. Sucré!

Nous ne disons pas que quelque part se trouve un objet qui a des propriétés Props et leurs types sont T! Nous introduisons uniquement une dépendance fonctionnelle entre deux types. Le type T est corrélé aux membres de type Props, et c'est tout!

Je ne voulais pas dire cela, je le pensais davantage car leurs types sont T [p], un dictionnaire de types indexés par p. Si c'est une bonne intuition, je la garderais.

Dans l'ensemble cependant, la syntaxe pourrait nécessiter un peu plus de travail, mais l'idée générale semble géniale.

Est-il possible d'écrire unwrap pour des arguments variadiques?

edit: tant pis, je viens de réaliser que votre proposition d'extension aux types variadiques résout cela.

Bonjour tous le monde,
Je me demande comment résoudre l'un de mes problèmes et j'ai trouvé cette discussion.
Le problème est:
J'ai la méthode RpcManager.call(command:Command):Promise<T> , et l'utilisation serait la suivante:

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

La solution, je pense, pourrait être comme:

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

Des pensées à ce sujet?

@ antanas-arvasevicius, le dernier bloc de code de cet exemple doit faire ce que vous voulez.

il semble que vous ayez davantage une question de savoir comment accomplir une tâche spécifique; veuillez utiliser Stack Overflow ou signaler un problème si vous pensez avoir trouvé un bogue du compilateur.

Salut Ryan, merci pour ta réponse.
J'ai essayé ce dernier bloc de code mais cela ne fonctionne pas.

Démo rapide:

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

Désolé de ne pas avoir utilisé Stack Overflow, mais je pensais que c'était lié à ce problème :)
Dois-je ouvrir un nouveau numéro sur ce sujet?

Un paramètre de type générique non utilisé empêche l'inférence de fonctionner; en général, vous ne devriez jamais avoir de paramètres de type inutilisés dans les déclarations de type car ils n'ont aucun sens. Si vous consommez T , tout fonctionne:

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

C'est tout simplement incroyable! Je vous remercie!
Je n'ai pas pensé que le paramètre de type peut être placé dans un type générique et
TS va l'extraire.
Le 22 février 2016 à 23 h 56, "Ryan Cavanaugh" [email protected] a écrit:

Un paramètre de type générique non utilisé empêche l'inférence de fonctionner; dans
général, vous ne devriez jamais avoir de paramètres de type inutilisés dans le type
déclarations car elles n'ont aucun sens. Si vous consommez du T, tout simplement
travaux:

Commande interface{foo: T} class MyCommand implémente la commande <{status: string}> {foo: {status: string; }} classe RPC {appel statique(commande: commande): T {retour; }}
let response = RPC.call (new MyCommand ());
console.log (response.status);

-
Répondez directement à cet e-mail ou affichez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -187404245
.

@ antanas-arvasevicius si vous créez des API de style RPC J'ai quelques documents que vous pourriez trouver utiles: https://github.com/alm-tools/alm/blob/master/docs/contributing/ASYNC.md : rose:

Les approches ci-dessus semblent:

  • assez compliqué;
  • n'abordez pas l'utilisation de chaînes dans le code. string ne sont pas "Trouver toutes les références" -able, ni refactorisable (par exemple, renommer).
  • certains ne prennent en charge que les propriétés de "1er niveau" et non les expressions plus complexes (qui sont prises en charge par certains frameworks).

Voici une autre idée, inspirée des arbres d'expressions C #. C'est juste une idée approximative, rien de bien pensé! La syntaxe est terrible. Je veux juste voir si cela inspire quelqu'un.

Supposons que nous ayons un type spécial de chaînes pour désigner les expressions.
Appelons cela type Expr<T, U> = string .
T est le type d'objet de départ et U est le type de résultat.

Supposons que nous puissions créer une instance de Expr<T,U> en utilisant un lambda qui prend un paramètre de type T et y effectue un accès membre.
Par exemple: person => person.address.city .
Lorsque cela se produit, l'ensemble du lambda est compilé en une chaîne contenant tout accès sur le paramètre, dans ce cas: "address.city" .

Vous pouvez utiliser une chaîne simple à la place, qui serait considérée comme Expr<any, any> .

Avoir ce type spécial Expr dans le langage permet des choses comme ça:

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

Il s'agit essentiellement d'une forme limitée de l'utilisation des expressions en C #.
Pensez-vous que cela pourrait être affiné et mener quelque part intéressant?

@fdecampredon @RyanCavanaugh

_ ( @ jods4 - Je suis désolé de ne pas répondre à votre suggestion ici. J'espère qu'elle ne sera pas «enterrée» par les commentaires) _

Je pense que la dénomination de cette fonctionnalité («Type Property type») est très déroutante et très difficile à comprendre. Il était très difficile de même comprendre quel était le concept décrit ici et ce qu'il signifiait en premier lieu!

Premièrement, tous les types n'ont pas de propriétés! undefined et null ne le font pas (bien que ce ne soient que des ajouts récents au système de types). Les primitives comme number , string , boolean sont rarement indexées par une propriété (par exemple 2 ["prop"]? Bien que cela semble fonctionner, c'est presque toujours une erreur)

Je suggérerais de nommer ce problème Référence de type de propriété d'interface via des valeurs littérales de chaîne . Le sujet ici ne concerne pas l'introduction d'un nouveau 'type', mais une manière très particulière de référencer un existant en utilisant une variable de chaîne ou un paramètre de fonction dont la valeur _doit être connue au moment de la compilation_.

Il aurait été très bénéfique que cela soit décrit et illustré le plus simplement possible, en dehors du contexte d'un cas d'utilisation particulier:

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: Il semble que pour implémenter cela correctement, le compilateur doit savoir avec certitude que la chaîne affectée à la variable n'a pas changé entre le point où elle a été initialisée et le point auquel elle a été référencée dans l'expression de type. Je ne pense pas que ce soit très facile? Cette hypothèse sur le comportement à l'exécution serait-elle toujours valable? _

_Edit 2: Cela ne fonctionnerait peut-être qu'avec const lorsqu'il est utilisé avec une variable? _

Je ne sais pas si l'intention d'origine était de n'autoriser que la référence de propriété au cas très particulier d'une chaîne littérale est passée à un paramètre de fonction ou de méthode? par exemple

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

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

Ai-je généralisé cela au-delà de ce qui était initialement prévu?

Comment cela gérerait-il un cas où l'argument de la fonction n'est pas une chaîne littérale, c'est-à-dire inconnue au moment de la compilation? par exemple

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

Sera-t-il une erreur, ou par défaut any peut-être?

(PS: Si c'était effectivement l'intention, alors je suggérerai de renommer ce problème en Référence de type de propriété Interface par une fonction littérale de chaîne ou des arguments de méthode - Aussi long que cela se soit avéré, c'est beaucoup plus précis et explicatif que le titre actuel.)

Voici un exemple de référence simple (suffisant pour une illustration) qui montre ce que cette fonctionnalité doit activer:

Écrivez le type de cette fonction, qui:

  • prend un objet contenant des champs de promesse, et
  • renvoie le même objet mais avec tous les champs résolus, chaque champ ayant le type approprié.
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)
}

Lorsqu'il est appelé sur l'objet suivant:

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

Le résultat res doit être de type {a: number; b: string}


Avec cette fonctionnalité (telle qu'expliquée par @Artazor), la signature serait

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

edit: correction du type de retour ci-dessus, il manquait Promise<...> ^^

@spion

Merci d'avoir essayé de fournir un exemple, mais nulle part ici je n'ai trouvé une spécification raisonnable et concise et un ensemble d'exemples clairs et réduits qui sont _détachés_ d'applications pratiques et essayent de mettre en évidence la sémantique et les différents cas extrêmes de la façon dont cela fonctionnerait, même quelque chose d'aussi basique que:

function func<T extends object>(name: string): T[name] {
 ...
}
  1. Il y a eu très peu d'efforts pour fournir un titre efficace et explicatif (comme je l'ai mentionné, 'Type Property Type' est un nom déroutant et peu explicatif pour cette fonctionnalité, qui ne concerne pas vraiment un type particulier mais une référence de type). Mon meilleur titre suggéré était _Interface property type reference by string literal function or method arguments_ (en supposant que je n'ai pas mal compris la portée de ceci).
  2. Il n'y a pas eu de mention claire de ce qui se passerait si name n'était pas connu au moment de la compilation, nul ou non défini, par exemple func<MyType>(getRandomString()) , func<MyType>(undefined) .
  3. Il n'y a pas eu de mention claire de ce qui se passerait si T était un type primitif comme number ou null .
  4. Aucun détail clair n'a été fourni pour savoir si cela s'applique uniquement aux fonctions et si T[name] peut être utilisé dans le corps de la fonction. Et dans ce cas, que se passe-t-il si name est réaffecté dans le corps de la fonction et que sa valeur peut donc ne plus être connue au moment de la compilation?
  5. Aucune spécification réelle d'une syntaxe: par exemple, T["propName"] ou T[propName] fonctionnera-t-il aussi tout seul? sans référence à un paramètre ou à une variable, je veux dire - cela pourrait être utile!
  6. Aucune mention ou discussion si T[name] peut être utilisé dans d'autres paramètres ou même en dehors des portées de fonction, par exemple
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'

_sept. Pas de véritable discussion sur la solution de contournement relativement simple utilisant un paramètre générique supplémentaire et typeof (bien qu'il ait été mentionné, mais relativement tard après que la fonctionnalité ait déjà été acceptée):

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'

En conclusion: il n'y a pas de vraie proposition ici, seulement un ensemble de démonstrations de cas d'utilisation. Je suis surpris que cela ait été accepté ou même suscité un intérêt positif de la part de l'équipe TypeScript, car je ne penserais pas autrement que ce serait à la hauteur de leurs normes. Je suis désolé si je peux paraître un peu dur ici (pas typique de moi) mais aucune de mes critiques n'est personnelle ou dirigée vers un individu en particulier.

Voulons-nous vraiment aller aussi loin en termes de méta-programmation?

JS est un langage dynamique, le code peut manipuler des objets de manière arbitraire.
Il y a des limites à ce que nous pouvons modéliser dans le système de types et il est facile de proposer des fonctions que vous ne pouvez pas modéliser dans TS.
La question est plutôt: jusqu'où est-il raisonnable et utile d'aller?

Le dernier exemple ( awaitObject ) de @spion est en même temps:

  • _Très complexe_ du point de vue de la langue (je suis d'accord avec @malibuzios ici).
  • _Très étroit_ en termes d'applicabilité. Cet exemple choisit des contraintes spécifiques et ne se généralise pas bien.

BTW @spion vous n'avez pas Promise .

Nous sommes loin du problème d'origine, qui consistait à taper des API qui prennent une chaîne représentant des champs, tels que _.pluck
Ces API devraient être prises en charge car elles sont courantes et pas si compliquées. Nous n'avons pas besoin de méta-modèles tels que { ...p: T[p] } pour cela.
Il y a plusieurs exemples dans l'OP, d'autres d'Aurelia dans mon commentaire au nom du problème .
Une approche différente peut couvrir ces cas d'utilisation, voir mon commentaire ci-dessus pour une idée possible.

@ jods4 ce n'est pas du tout étroit. Il peut être utilisé pour un certain nombre de choses qui sont actuellement impossibles à modéliser dans le système de type. Je dirais que c'est l'une des dernières grandes pièces manquantes dans le système de type TS. Il y a beaucoup d'exemples du monde réel ci-dessus par @Artazor https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -177287714 les choses plus simples ci-dessus, l'exemple de "select" du générateur de requêtes sql et ainsi de suite.

C'est awaitObject dans bluebird. C'est une vraie méthode. Le type est actuellement inutile.

Une solution plus générale sera meilleure. Cela fonctionnera pour les cas simples et complexes. Une solution sous-alimentée manquera dans la moitié des cas et causera facilement des problèmes de compatibilité s'il est nécessaire d'en adopter une plus générale à l'avenir.

Oui, cela nécessite beaucoup plus de travail, mais je pense aussi que @Artazor a fait un excellent travail en analysant et en explorant tous les aspects auxquels nous n'avions pas pensé auparavant. Je ne voudrais pas rester que nous nous sommes éloignés du problème d'origine, nous n'en avons qu'une meilleure compréhension.

Nous avons peut-être besoin d'un meilleur nom pour cela, j'essaierais, mais je ne comprends généralement pas ces choses. Que diriez-vous des "Génériques d'objets"?

@spion , juste pour

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

(vous avez manqué une promesse dans la signature de sortie)
-)

@ jods4 , je suis d'accord avec @spion
Il existe de nombreux problèmes du monde réel où { ...p: T[p] } est la solution. Pour n'en nommer qu'un: react + flux / redux

@spion @Artazor
Je crains simplement que cela devienne assez complexe à préciser.

La motivation derrière ce problème a dérivé. À l'origine, il s'agissait de prendre en charge les API qui acceptent une chaîne pour désigner un champ objet. Maintenant, il est principalement question des API qui utilisent des objets comme des cartes ou des enregistrements de manière très dynamique. Notez que si nous pouvons faire d'une pierre deux coups, je suis tout à fait d'accord.

Si nous revenons aux API qui acceptent le problème des chaînes, le T[p] n'est pas une solution complète à mon avis (j'ai déjà dit pourquoi).

_Juste pour l'exactitude_ awaitObject devrait également accepter les propriétés non-Promise, au moins Bluebird props fait. Alors maintenant, nous avons:

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

J'ai changé le type de retour en Promise<T> car je m'attends à ce que cette notation fonctionne.
Il existe d'autres surcharges, par exemple celle qui prend une promesse pour un tel objet (sa signature est encore plus amusante). Cela signifie donc que la notation { ...p } doit être considérée comme une pire correspondance que tout autre type.

Spécifier tout cela sera un travail difficile. Je dirais que c'est la prochaine étape si vous voulez faire avancer les choses.

@spion @ jods4

Je voulais mentionner que cette fonctionnalité ne concerne pas les génériques ni les types polymorphes de type supérieur. C'est simplement une syntaxe pour référencer le type d'une propriété via un type conteneur (avec quelques extensions "avancées" décrites ci-dessous), ce qui n'est pas si loin du concept typeof . Exemple:

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

Maintenant, comparez à:

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

let y: typeof x.abcd;

Il existe deux différences principales avec typeof . Contrairement à typeof , cela s'applique aux types (au moins non primitifs, un fait souvent omis ici) plutôt qu'aux instances. De plus (et c'est un élément majeur), il a été étendu pour prendre en charge l'utilisation de constantes de chaîne littérale (qui doivent être connues au moment de la compilation) comme descripteurs de chemin de propriété et génériques:

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

Cependant, techniquement, typeof aurait également pu être étendu pour prendre en charge les chaînes littérales (cela inclut le cas où x a un type générique):

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

Et avec cette extension, il pourrait également être utilisé pour résoudre certains des cas cibles ici:

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

typeof est cependant plus puissant car il prend en charge une quantité indéfinie de niveaux d'imbrication:

let y: typeof x.prop.childProp.deeperChildProp

Et celui-ci ne va qu'un seul niveau. Ie pas prévu (pour autant que je sache) pour soutenir:

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

Je pense que la portée de cette proposition (si cela peut même être qualifié de proposition à son niveau d'imprécision) est trop étroite. Cela peut aider à résoudre un problème particulier (bien qu'il puisse y avoir des solutions alternatives), ce qui semble rendre de nombreuses personnes très désireuses de le promouvoir. Cependant, il consomme également une syntaxe précieuse du langage. Sans un plan plus large et une approche orientée conception, il ne semble pas judicieux d'introduire à la hâte quelque chose qui à ce jour n'a même pas de spécification claire.

_Edits: correction de quelques erreurs évidentes dans les exemples de code_

J'ai fait quelques recherches sur l'alternative typeof :

La prise en charge future des langues pour typeof x["abcd"] et typeof x[42] a été approuvée et relève maintenant de # 6606, qui est actuellement en cours de développement (il y a une implémentation fonctionnelle).

Cela fait la moitié du chemin. Avec ceux-ci en place, le reste peut être fait en plusieurs étapes:

(1) Ajout de la prise en charge des constantes littérales de chaîne (ou même des constantes numériques - pourrait être utile pour les tuples?) Comme spécificateurs de propriété dans les expressions de type, par exemple:

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

(2) Autoriser l'application de ces spécificateurs à des types génériques

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

(3) Permettre à ces spécificateurs d'être passés, et en pratique "instancier" les références, via des arguments de fonction ( typeof list[0] est prévu donc je pense que cela pourrait couvrir des cas plus complexes comme pluck :

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 type voici celui proposé au # 1809)

Bien que plus puissante (par exemple, peut prendre en charge typeof obj[propName][nestedPropName] ), il est possible que cette approche alternative ne couvre pas tous les cas décrits ici. S'il vous plaît laissez-moi savoir s'il y a des exemples qui ne semblent pas être traités par cela (un scénario qui me vient à l'esprit est lorsque l'instance d'objet n'est pas du tout passée, cependant, il est difficile pour moi à ce stade d'imaginer un cas où ce serait nécessaire, même si je suppose que c'est possible).

_Edits: correction de quelques erreurs dans le code_

@malibuzios
Quelques idées:

  • Cela corrige la définition de l'API mais n'aide pas l'appelant. Nous avons encore besoin d'une sorte de nameof ou Expression sur le site d'appel.
  • Cela fonctionne pour les définitions .d.ts , mais il ne sera généralement pas statiquement déductible ni même vérifiable dans les fonctions / implémentations TS.

Plus j'y pense, plus Expression<T, U> ressemble à la solution pour ce type d'API. Il corrige les problèmes de site d'appel, la saisie du résultat et peut être inférable + vérifiable dans une implémentation.

@ jods4

Vous avez mentionné dans un commentaire précédent que pluck pourrait être implémenté avec des expressions. Bien que je sois très familier avec C #, je n'ai jamais vraiment expérimenté avec eux, donc j'avoue que je ne les comprends pas vraiment à ce stade.

Quoi qu'il en soit, je voulais juste mentionner que TypeScript prend en charge _type argument inference_ , donc au lieu d'utiliser pluck , on peut simplement utiliser map et obtenir le même résultat, ce qui ne nécessiterait même pas de spécifier un paramètre générique ( il est déduit) et donnerait également une vérification et un achèvement de type complets:

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[]' 

map (très simplifié pour la démonstration) est déclaré comme

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

Ce même modèle d'inférence peut être utilisé comme une `` astuce '' pour passer tout résultat d'un lambda arbitraire (qui n'a même pas besoin d'être appelé) à une fonction et lui définir son type de retour:

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: ça peut sembler idiot de le passer dans un lambda ici et pas seulement dans la chose elle-même! cependant, dans des cas plus complexes comme des objets imbriqués (par exemple x.prop.Childprop ), il peut y avoir des références non définies ou nulles susceptibles d'erreur. L'avoir dans un lambda, qui n'est pas nécessairement appelé, évite cela.

J'avoue que je ne connais pas très bien certains des cas d'utilisation évoqués ici. Personnellement, je n'ai jamais ressenti le besoin d'appeler une fonction qui prend un nom de propriété comme chaîne. Passer un lambda (où les avantages que vous décrivez sont également valables) en combinaison avec une interface d'argument de type est généralement suffisant pour résoudre de nombreux problèmes courants.

La raison pour laquelle j'ai suggéré l'approche alternative typeof était principalement parce que cela semble couvrir à peu près les mêmes cas d'utilisation et fournir les mêmes fonctionnalités pour ce que les gens décrivent ici. L'utiliserais-je moi-même? Je ne sais pas, je n'ai jamais ressenti un réel besoin (il se pourrait simplement que je n'utilise presque jamais de bibliothèques externes comme le soulignement, je développe presque toujours des fonctions utilitaires par moi-même, en cas de besoin).

@malibuzios Vrai, pluck est un peu idiot avec les lambdas + map maintenant intégré.

Dans ce commentaire, je donne 5 API différentes qui prennent toutes des chaînes - elles sont toutes liées à l'observation des changements.

@ jods4 et autres

Je voulais juste mentionner que lorsque # 6606 est terminé, en implémentant uniquement l'étape 1 (autoriser l'utilisation de littéraux de chaîne constante comme spécificateurs de propriété), certaines des fonctionnalités nécessaires ici peuvent être obtenues, mais pas aussi élégamment qu'elles le pourraient (cela peut nécessiter le nom de la propriété à déclarer comme constante, en ajoutant une instruction supplémentaire).

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

Cependant, la quantité d'efforts pour mettre en œuvre cela peut être considérablement inférieure à celle des étapes 2 et 3 (je ne suis pas sûr, mais il est possible que 1 soit déjà couvert par # 6606). Avec la mise en œuvre de l'étape 2, cela couvrirait également le cas où x a un type générique (mais je ne suis pas sûr si c'est vraiment nécessaire?).

Edit: La raison pour laquelle j'ai utilisé une constante externe et non seulement écrit le nom de la propriété deux fois n'était pas seulement de réduire la saisie, mais aussi de m'assurer que les noms correspondent toujours, bien que cela ne puisse toujours pas être utilisé avec des outils comme "renommer" et "trouver tout références », ce que je trouve être un sérieux inconvénient.

@ jods4 Je cherche toujours une meilleure solution qui n'utilise pas de chaînes. Je vais essayer de réfléchir à ce qui peut être fait avec nameof et ses variantes.

Voici une autre idée.

Puisque les littéraux de chaîne sont déjà pris en charge en tant que types, on peut par exemple déjà écrire:

let x: "HELLO"; 

On peut voir une chaîne littérale passée à une fonction comme une forme de spécialisation générique

(_Edit: ces exemples ont été corrigés pour s'assurer que s est immuable dans le corps de la fonction, bien que const ne soit pas pris en charge aux positions des paramètres à ce stade (pas sûr de readonly bien que)._) :

function func(const s: string)

Le type de paramètre string associé à s peut être vu comme un générique implicite ici (car il peut être spécialisé dans les chaînes littérales). Pour plus de clarté, je l'écrirai plus explicitement (même si je ne suis pas sûr s'il y a vraiment besoin de le faire):

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

pourrait se spécialiser en interne pour:

function func(s: "TEST")

Et maintenant, cela peut être utilisé comme une manière alternative de "passer" le nom de la propriété, ce qui, selon moi, capture mieux la sémantique ici.

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

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

observeProperty(x, nameof(x.age))

Puisque dans T[S] , T et S sont des paramètres génériques. Il semble plus naturel d'avoir deux types combinés dans une position de type que de mélanger des éléments d'exécution et de portée de type (par exemple T[someString] ).

_Edits: exemples réécrits pour avoir un type de paramètre chaîne immuable.

Puisque j'utilise TS 1.8.7 et non la dernière version, je ne savais pas que dans les versions plus récentes de

const x = "Hello";

Le type de x est déjà déduit comme le type littéral "Hello" , (ie: x: "Hello" ) ce qui a beaucoup de sens (voir # 6554).

Donc, naturellement, s'il y avait un moyen de définir un paramètre comme const (ou peut-être que readonly fonctionnerait également?):

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

Ensuite, je crois que cela pourrait tenir:

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

Étant donné que certains de ces éléments sont plutôt nouveaux et reposent sur des fonctionnalités linguistiques récentes, je vais essayer de le résumer aussi clairement que possible:

(1) Lorsqu'une variable de chaîne const (et peut-être readonly également?) Reçoit une valeur connue au moment de la compilation, la variable reçoit automatiquement un type littéral qui a la même valeur ( Je crois que c'est un comportement récent qui ne se produit pas sur 1.8.x ), par exemple

const x = "ABCD";

Le type de x est supposé être "ABCD" , et non string !, Par exemple, on peut dire ceci comme x: "ABCD" .

(2) Si les paramètres de fonction readonly étaient autorisés, un paramètre readonly avec un type générique spécialiserait naturellement son type en une chaîne littérale quand il est reçu comme argument, car le paramètre est immuable!

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

Ici S a été résolu dans le type "ABCD" , et non string !

Cependant, si le paramètre str n'était pas immuable, le compilateur ne peut pas garantir qu'il n'est pas réaffecté dans le corps de la fonction, ainsi, le type déduit serait juste string .

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

func("ABCD");

(3) Il est possible d'en profiter et de modifier la proposition originale pour que le spécificateur de propriété soit une référence à un type , qui devrait être contraint à être un dérivé de string (dans certains cas, il peut même être approprié de le contraindre à des types littéraux singleton uniquement, bien qu'il n'y ait actuellement pas vraiment de moyen de le faire), plutôt qu'une entité d'exécution:

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

L'appeler ne nécessiterait pas de spécifier explicitement des arguments de type, car TypeScript prend en charge l'inférence d'arguments de type:

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: correction de quelques fautes d'orthographe et de code ._
_Remarque: une version généralisée et étendue de cette approche modifiée est désormais également suivie dans # 7730._

Voici une autre utilisation du type de propriété type (ou "génériques indexés" comme j'aime les appeler récemment) qui est apparue lors d'une discussion avec @Raynos

Nous pouvons déjà écrire le vérificateur général suivant pour les tableaux:

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

Une fois que nous aurons indexé les génériques, nous serions également capables d'écrire un vérificateur général pour les objets:

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

qui, avec les vérificateurs primitifs pour chaîne, nombre, booléen, null et undefined, vous permettrait d'écrire des choses comme ceci:

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

et obtenez le résultat avec le bon vérificateur d'exécution _et_ garde de type à la compilation, en même temps :)

Quelqu'un a-t-il déjà travaillé à ce sujet, ou est-ce sur le radar de quelqu'un? Ce sera une amélioration majeure du nombre de bibliothèques standard pouvant utiliser des typages, y compris des types de lodash ou ramda et de nombreuses interfaces de base de données.

@malibuzios Je crois que vous approchez de la suggestion de @Artazor :)

Pour répondre à vos préoccupations:

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

Ce serait

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

De cette façon, le nom serait verrouillé sur le type le plus spécifique (un type de constante de chaîne) lorsqu'il est appelé avec:

func("test")

Le type S devient "test" (et non string ). En tant que tel, str ne peut pas être réaffecté à une valeur différente. Si vous avez essayé cela, par exemple

str = "other"

Le compilateur peut produire une erreur (sauf s'il y a des problèmes de solidité de la variance;))

Au lieu d'obtenir simplement le type d'une propriété, j'aimerais avoir la possibilité d'obtenir un super type de type arbitraire.

Donc, ma suggestion est d'ajouter la syntaxe suivante: Au lieu d'avoir simplement T[prop] , je voudrais ajouter la syntaxe T[...props] . Où props doit être un tableau de membres de T . Et le type résultant est un super type de T avec des membres de T définis dans props .

Je pense que ce serait très utile dans Sequelize - un ORM populaire pour node.js. Pour des raisons de sécurité et pour des raisons de performances, il est sage de ne rechercher que les attributs d'une table que vous devez utiliser. Cela signifie souvent un super type d'un type.

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

(Dans mon exemple, il inclut l'opérateur memberof , qui est ce que vous attendez, et aussi l'expression options.attributes qui est identique à typeof options.attributes , mais je pense typeof opérateur

Si personne n'insiste, j'ai commencé à travailler là-dessus.

Que pensez-vous de la sécurité des types à l'intérieur de la fonction, c'est-à-dire garantir qu'une instruction return renvoie quelque chose qui peut être assigné à un type de retour?

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] ?
}

Le nom "Type de propriété" utilisé ici semble également un peu incorrect. Il indique en quelque sorte que le type a des propriétés, que presque tous les types ont.

Qu'en est-il du "Type référencé de propriété"?

Que pensez-vous de la sécurité des types à l'intérieur de la fonction

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

Et interdire la chaîne normale sur la ligne de retour.

Le problème de

Des commentaires de l'équipe TS à propos de https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -239653337?

@RyanCavanaugh @mhegazy etc?

Concernant le nom, j'ai commencé à appeler cette fonctionnalité (au moins la forme proposée par @Artazor ) "

Une solution sous un autre angle de vue pourrait être pour ce problème. Je ne sais pas si cela a déjà été apporté, c'est un long fil. En développant une suggestion générique de chaîne, nous pourrions étendre la signature d'indexation. Puisque les littéraux de chaîne peuvent être utilisés pour le type d'indexeur, nous pourrions les avoir équivalents (car je sais qu'ils ne le sont pas pour le moment):

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

Alors, on pourrait juste écrire alors

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

Il y a quelques éléments à considérer:

  • comment indiquer que P ne peut être qu'une chaîne littérale?

    • puisque string étend techniquement tous les littéraux de chaîne, P extends string ne serait pas un

    • une autre discussion est en cours sur l'autorisation P super string contrainte

    • devons-nous vraiment nous en soucier? nous pouvons utiliser tout ce qui est acceptable pour le type d'indexeur - si T a un indexeur de string s ou number s. puis P peut être string ou number .

  • actuellement, si nous passons "something" comme second argument, il sera de type string

    • les littéraux de chaîne # 10195 pourraient être la réponse

    • ou TS pourrait en déduire qu'un P plus spécifique devrait être utilisé s'il est acceptable

  • les indexeurs sont implicitement optionnels { [i: string]: number /* | undefined */ }

    • comment appliquer si nous ne voulons vraiment pas avoir undefined dans le domaine?

  • l'inférence automatique de type pour toutes les relations entre P , T et R est une clé pour le faire fonctionner

@weswigham @mhegazy , et j'en ai discuté récemment; nous vous informerons de tous les développements que nous rencontrons, et gardez à l'esprit qu'il ne s'agit que d'un prototypage de l'idée.

Idées actuelles:

  • Un opérateur keysof Foo pour récupérer l'union des noms de propriétés de Foo tant que types littéraux de chaîne.
  • Un type Foo[K] qui spécifie que pour certains types K c'est une chaîne de types littéraux ou une union de types littéraux chaîne.

À partir de ces blocs de base, si vous devez déduire un littéral de chaîne comme type approprié, vous pouvez écrire

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

Ici, K sera un sous-type de keysof T ce qui signifie qu'il s'agit d'un type littéral de chaîne ou d'une union de types de littéraux de chaîne. Tout ce que vous passez pour le paramètre key doit être typé contextuellement par ce littéral / union de littéraux, et déduit comme un type littéral de chaîne singleton.

Par exemple

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

Le plus gros problème est que keysof doit souvent "retarder" son fonctionnement. Il est trop désireux de savoir comment il évalue un type, ce qui est un problème pour les paramètres de type comme dans le premier exemple que j'ai posté (c'est-à-dire que le cas que nous voulons vraiment résoudre est en fait le plus difficile: sourire :).

J'espère que cela vous donne à tous une mise à jour.

@DanielRosenwasser Merci pour la mise à jour. Je viens de voir @weswigham soumettre un PR à propos de l'opérateur keysof , il est donc peut-être préférable de vous confier ce problème.

Je me demande simplement pourquoi vous avez décidé de vous écarter de la syntaxe proposée d'origine?

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

et introduisez keysof ?

T[prop] est moins général et nécessite beaucoup de machines entrelacées. Une grande question ici est de savoir comment relieriez même le contenu littéral de prop aux noms de propriété de T . Je ne suis même pas complètement sûr de ce que vous feriez. Souhaitez-vous ajouter un paramètre de type implicite? Auriez-vous besoin de changer le comportement de saisie contextuelle? Auriez-vous besoin d'ajouter quelque chose de spécial aux signatures?

La réponse est probablement oui à toutes ces choses. Je nous ai éloignés de cela parce que mon instinct m'a dit qu'il valait mieux utiliser deux concepts plus simples et séparés et construire à partir de là. L'inconvénient est qu'il y a un peu plus de passe-partout dans certains cas.

S'il existe des bibliothèques plus récentes qui utilisent ces types de modèles et que ce passe-partout leur rend difficile l'écriture en TypeScript, alors peut-être devrions-nous envisager cela. Mais dans l'ensemble, cette fonctionnalité est principalement destinée aux consommateurs de bibliothèques, car le site d'utilisation est l'endroit où vous obtenez de toute façon les avantages ici.

@DanielRosenwasser A peine descendu dans le @SaschaNaz ? Je pense que keysof est redondant dans ce cas. T[p] rapporte déjà que p doit être l'un des accessoires littéraux de T .

Ma première pensée d'implémentation était d'introduire un nouveau type appelé PropertyReferencedType .

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

Lors de la saisie d'une fonction déclarée avec un type de retour de PropertyReferencedType ou de la saisie d'une fonction faisant référence à PropertyReferencedType : Un type de ElementAccessExpression sera augmenté d'une propriété qui référence le symbole de la propriété accédée.

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

Ainsi, un type avec un symbole de propriété référencé est assignable à un PropertyReferencedType . Lors de la vérification, le referencedProperty doit correspondre à p en T[p] . De plus, le type parent d'une expression d'accès aux éléments doit être assignable à T . Et pour faciliter les choses, p doit également être const.

Le nouveau type PropertyReferencedType n'existe qu'à l'intérieur de la fonction en tant que "type non résolu". Sur le site d'appel, il faut résoudre le type avec p :

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

Un PropertyReferencedType se propage uniquement via les affectations de fonctions et ne peut pas se propager via des expressions d'appel, car un PropertyReferencedType n'est qu'un type temporaire destiné à aider à vérifier le corps d'une fonction avec le type de retour T[p] .

Si vous introduisez les opérateurs de type keysof et T[K] , cela voudrait-il dire que nous pourrions les utiliser comme ceci:

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 votre exemple n'aurait-il pas le même sens avec mon

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 {
    // ...
}

Je ne vois pas comment la signature serait écrite pour _.pick de Underscore:

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

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

@rtm Je l'ai suggéré dans https://github.com/Microsoft/TypeScript/issues/1295#issuecomment -234724380. Bien qu'il puisse être préférable d'ouvrir un nouveau numéro, même s'il est lié à celui-ci.

Implémentation maintenant disponible dans # 11929.

Cette page vous a été utile?
0 / 5 - 0 notes