Typescript: Proposition : Types variadiques -- Donner des types spécifiques aux fonctions variadiques

Créé le 29 oct. 2015  ·  265Commentaires  ·  Source: microsoft/TypeScript

Types variadiques

Donner des types spécifiques aux fonctions variadiques

Cette proposition permet à Typescript de donner des types aux fonctions d'ordre supérieur qui prennent un nombre variable de paramètres.
Des fonctions comme celle-ci incluent concat , apply , curry , compose et presque tous les décorateurs qui enveloppent une fonction.
En Javascript, ces fonctions d'ordre supérieur sont censées accepter des fonctions variadiques en tant qu'arguments.
Avec les normes ES2015 et ES2017, cette utilisation deviendra encore plus courante à mesure que les programmeurs commenceront à utiliser des arguments de propagation et des paramètres de repos pour les tableaux et les objets.
Cette proposition aborde ces cas d'utilisation avec une stratégie de typage unique et très générale basée sur des genres d'ordre supérieur.

Cette proposition résoudrait totalement ou partiellement plusieurs problèmes, notamment :

  1. #5331 -- Tuples comme types pour les arguments de repos ...
  2. #4130 - Le compilateur signale de manière incorrecte la non-concordance de la signature paramètre/cible d'appel lors de l'utilisation de l'opérateur de propagation
  3. #4988 -- Les tuples devraient être clonables avec Array.prototype.slice()
  4. #1773 -- Génériques variadiques ?
  5. #3870 -- Types de repos dans les génériques pour les types d'intersection.
  6. #212 -- bind, call et apply ne sont pas typés (nécessite les types this-function de #3694).
  7. #1024 -- Paramètres de repos tapés avec des génériques

Je mettrai à jour cette proposition sur mon fork du Typescript-Handbook: sandersn/ TypeScript-Handbook@76f5a75868de3fb1ad4dbed5db437a8ab61a2698
J'ai une implémentation en cours à sandersn/ TypeScript@f3c327aef22f6251532309ba046874133c32f4c7 qui a actuellement mis en œuvre les parties simples de la proposition.
Il remplace la partie 2 de ma proposition précédente, #5296.
Edit : Ajout d'une section sur l'assignabilité. Je ne suis plus sûr qu'il remplace strictement le #5296.

Exemple d'aperçu avec curry

curry pour les fonctions avec deux arguments est simple à écrire en Javascript et Typescript :

function curry(f, a) {
    return b => f(a, b);
}

et en Typescript avec des annotations de type :

function curry<T, U, V>(f: (t: T, u: U) => V, a:T): (b:U) => V {
    return b => f(a, b);
}

Cependant, une version variadique est facile à écrire en Javascript mais ne peut pas être typée en TypeScript :

function curry(f, ...a) {
    return ...b => f(...a, ...b);
}

Voici un exemple d'utilisation des types variadiques de cette proposition pour taper curry :

function curry<...T,...U,V>(f: (...ts: [...T, ...U]) => V, ...as:...T): (...bs:...U) => V {
    return ...b => f(...a, ...b);
}

La syntaxe des types de tuples variadiques que j'utilise ici correspond à la syntaxe de propagation et de repos utilisée pour les valeurs en Javascript.
Ceci est plus facile à apprendre mais peut rendre plus difficile la distinction entre les annotations de type et les expressions de valeur.
De même, la syntaxe de concaténation ressemble à une construction de tuple, même s'il s'agit en réalité de la concaténation de deux types de tuple.

Regardons maintenant un exemple d'appel à curry :

function f(n: number, m: number, s: string, c: string): [number, number, string, string] {
    return [n,m,s,c];
}
let [n,m,s,c] = curry(f, 1, 2)('foo', 'x');
let [n,m,s,c] = curry(f, 1, 2, 'foo', 'x')();

Au premier appel,

V = [number, number, string, string]
...T = [number, number]
...U = [string, string]

Au deuxième appel,

V = [number, number, string, string]
...T = [number, number, string, string]
...U = []

Syntaxe

La syntaxe d'une variable de type variadique est ...T où _T_ est un identifiant qui est par convention une seule lettre majuscule, ou T suivi d'un identifiant PascalCase .
Les variables de type variadique peuvent être utilisées dans un certain nombre de contextes syntaxiques :

Les genres variadiques peuvent être liés à l'emplacement habituel pour la liaison des paramètres de type, y compris les fonctions et les classes :

function f<...T,...U>() {}
}
class C<...T> {
}

Et ils peuvent être référencés dans n'importe quel type d'emplacement d'annotation :

function makeTuple<...T>(ts:...T): ...T {
    return ts;
}
function f<...T,...U>(ts:...T): [...T,...U] {
    // note that U is constrained to [string,string] in this function
    let us: ...U = makeTuple('hello', 'world');
    return [...ts, ...us];
}

Les variables de type variadiques, comme les variables de type, sont assez opaques.
Ils ont une opération, contrairement aux variables de type.
Ils peuvent être concaténés avec d'autres types ou avec des tuples réels.
La syntaxe utilisée pour cela est identique à la syntaxe de propagation de tuple, mais dans l'emplacement d'annotation de type :

let t1: [...T,...U] = [...ts,...uProducer<...U>()];
let t2: [...T,string,string,...U,number] = [...ts,'foo','bar',...uProducer<...U>(),12];

Les types de tuple sont des instances de genres variadiques, ils continuent donc à apparaître partout où les annotations de type étaient auparavant autorisées :

function f<...T>(ts:...T): [...T,string,string] { 
    // note the type of `us` could have been inferred here
    let us: [string,string] = makeTuple('hello', 'world');
    return [...ts, ...us];
}

let tuple: [number, string] = [1,'foo'];
f<[number,string]>(tuple);

Sémantique

Une variable kind variadique représente un type de tuple de n'importe quelle longueur.
Puisqu'il représente un ensemble de types, nous utilisons le terme « genre » pour le désigner, suivant son utilisation dans la théorie des types.
Parce que l'ensemble de types qu'il représente est constitué de tuples de n'importe quelle longueur, nous qualifions 'kind' avec 'variadic'.

Par conséquent, déclarer une variable de type tuple variadique lui permet de prendre n'importe quel type de tuple _single_.
Comme les variables de type, les variables de genre ne peuvent être déclarées que comme paramètres de fonctions, de classes, etc., ce qui leur permet ensuite d'être utilisées à l'intérieur du corps :

function f<...T>(): ...T {
    let a: ...T;
}

L'appel d'une fonction avec des arguments tapés en tant que kind variadique affectera un type de tuple spécifique au kind :

f([1,2,"foo"]);

Attribue le type de tuple ...T=[number,number,string] ...T . So in this application of f , let a:...T is instantiated as let a:[number,number,string] . However, because the type of a is not known when the function is written, the elements of the tuple cannot be referenced in the body of the function. Only creating a new tuple from a` est autorisé.
Par exemple, de nouveaux éléments peuvent être ajoutés au tuple :

function cons<H,...Tail>(head: H, tail: ...Tail): [H,...Tail] {
    return [head, ...tail];
}
let l: [number, string, string, boolean]; 
l = cons(1, cons("foo", ["baz", false]));

Comme les variables de type, les variables de type variadiques peuvent généralement être déduites.
Les appels à cons auraient pu être annotés :

l = cons<number,[string,string,boolean]>(1, cons<string,[string,boolean]>("foo", ["baz", false]));

Par exemple, cons doit déduire deux variables, un type _H_ et un genre _...Tail_.
Dans l'appel le plus interne, cons("foo", ["baz", false]) , H=string et ...Tail=[string,boolean] .
Dans l'appel le plus externe, H=number et ...Tail=[string, string, boolean] .
Les types assignés à _...Tail_ sont obtenus en tapant les littéraux de liste sous forme de tuples -- les variables d'un type de tuple peuvent également être utilisées :

let tail: [number, boolean] = ["baz", false];
let l = cons(1, cons("foo", tail));

De plus, des variables de genre variadiques peuvent être déduites lorsqu'elles sont concaténées avec des types :

function car<H,...Tail>(l: [H, ...Tail]): H {
    let [head, ...tail] = l;
    return head;
}
car([1, "foo", false]);

Ici, le type de l est déduit comme [number, string, boolean] .
Puis H=number et ...Tail=[string, boolean] .

Limites de l'inférence de type

Les genres concaténés ne peuvent pas être déduits car le vérificateur ne peut pas deviner où doit se trouver la limite entre deux genres :

function twoKinds<...T,...U>(total: [...T,string,...U]) {
}
twoKinds("an", "ambiguous", "call", "to", "twoKinds")

Le vérificateur ne peut pas décider d'attribuer

  1. ...T = [string,string,string], ...U = [string]
  2. ...T = [string,string], ...U = [string,string]
  3. ...T = [string], ...U = [string,string,string]

Certains appels sans ambiguïté sont victimes de cette restriction :

twoKinds(1, "unambiguous", 12); // but still needs an annotation!

La solution consiste à ajouter des annotations de type :

twoKinds<[string,string],[string,string]>("an", "ambiguous", "call", "to", "twoKinds");
twoKinds<[number],[number]>(1, "unambiguous", 12);

Des dépendances incontrôlables entre les arguments de type et le corps de la fonction peuvent survenir, comme dans rotate :

function rotate(l:[...T, ...U], n: number): [...U, ...T] {
    let first: ...T = l.slice(0, n);
    let rest: ...U = l.slice(n);
    return [...rest, ...first];
}
rotate<[boolean, boolean, string], [string, number]>([true, true, 'none', 12', 'some'], 3);

Cette fonction peut être typée, mais il existe une dépendance entre n et les variables kind : n === ...T.length doit être vrai pour que le type soit correct.
Je ne sais pas si c'est un code qui devrait être autorisé.

Sémantique sur les classes et les interfaces

La sémantique est la même sur les classes et les interfaces.

À FAIRE : Il y a probablement des rides spécifiques à la classe dans la sémantique.

Affectabilité entre les tuples et les listes de paramètres

Les types de tuples peuvent être utilisés pour donner un type aux arguments de repos des fonctions à l'intérieur de leur portée :

function apply<...T,U>(ap: (...args:...T) => U, args: ...T): U {
    return ap(...args);
}
function f(a: number, b: string) => string {
    return b + a;
}
apply(f, [1, 'foo']);

Dans cet exemple, la liste de paramètres de f: (a: number, b:string) => string doit être assignable au type de tuple instancié pour le genre ...T .
Le type de tuple déduit est [number, string] , ce qui signifie que (a: number, b: string) => string doit être attribuable à (...args: [number, string]) => string .

Comme effet secondaire, les appels de fonction pourront tirer parti de cette assignabilité en répartissant les tuples dans les paramètres de repos, même si la fonction n'a pas de type de tuple :

function g(a: number, ...b: [number, string]) {
    return a + b[0];
}
g(a, ...[12, 'foo']);

Types de tuple générés pour les paramètres optionnels et de repos

Étant donné que les tuples ne peuvent pas représenter directement des paramètres facultatifs, lorsqu'une fonction est affectée à un paramètre de fonction typé par un genre de tuple, le type de tuple généré est une union de types de tuple.
Regardez le type de h après qu'il a été curry :

function curry<...T,...U,V>(cur: (...args:[...T,...U]) => V, ...ts:...T): (...us:...U) => V {
    return ...us => cur(...ts, ...us);
}
function h(a: number, b?:string): number {
}
let curried = curry(h, 12);
curried('foo'); // ok
curried(); // ok

Ici ...T=([number] | [number, string]) , donc curried: ...([number] | [number, string]) => number qui peut être appelé comme on peut s'y attendre. Malheureusement, cette stratégie ne fonctionne pas pour les paramètres de repos. Ceux-ci sont simplement transformés en tableaux :

function i(a: number, b?: string, ...c: boolean[]): number {
}
let curried = curry(i, 12);
curried('foo', [true, false]);
curried([true, false]);

Ici, curried: ...([string, boolean[]] | [boolean[]]) => number .
Je pense que cela pourrait être pris en charge s'il y avait un cas particulier pour les fonctions avec un paramètre de repos de tuple, où le dernier élément du tuple est un tableau.
Dans ce cas, l'appel de fonction permettrait à des arguments supplémentaires du type correct de correspondre au tableau.
Cependant, cela semble trop complexe pour en valoir la peine.

Extensions aux autres parties de tapuscrit

  1. Typescript ne permet pas aux utilisateurs d'écrire un type de tuple vide.
    Cependant, cette proposition nécessite que les types variadiques puissent être liés à un tuple vide.
    Ainsi, Typescript devra prendre en charge les tuples vides, même si ce n'est qu'en interne.

    Exemples

La plupart de ces exemples sont possibles en tant que fonctions à argument fixe dans Typescript actuel, mais avec cette proposition, ils peuvent être écrits en tant que variadiques.
Certains, comme cons et concat , peuvent être écrits pour des tableaux homogènes dans Typescript actuel, mais peuvent maintenant être écrits pour des tuples hétérogènes en utilisant des types de tuples.
Cela suit de plus près la pratique Javascript typique.

Renvoie un type concaténé

function cons<H,...T>(head: H, tail:...T): [H, ...T] {
    return [head, ...tail];
}
function concat<...T,...U>(first: ...T, ...second: ...U): [...T, ...U] {
    return [...first, ...second];
}
cons(1, ["foo", false]); // === [1, "foo", false]
concat(['a', true], 1, 'b'); // === ['a', true, 1, 'b']
concat(['a', true]); // === ['a', true, 1, 'b']

let start: [number,number] = [1,2]; // type annotation required here
cons(3, start); // == [3,1,2]

Type concaténé comme paramètre

function car<H,...T>(l: [H,...T]): H {
    let [head, ...tail] = l;
    return head;
}
function cdr<H,...T>(l: [H,...T]): ...T {
    let [head, ...tail] = l;
    return ...tail;
}

cdr(["foo", 1, 2]); // => [1,2]
car(["foo", 1, 2]); // => "foo"

Fonctions variadiques comme arguments

function apply<...T,U>(f: (...args:...T) => U, args: ...T): U {
    return f(...args);
}

function f(x: number, y: string) {
}
function g(x: number, y: string, z: string) {
}

apply(f, [1, 'foo']); // ok
apply(f, [1, 'foo', 'bar']); // too many arguments
apply(g, [1, 'foo', 'bar']); // ok
function curry<...T,...U,V>(f: (...args:[...T,...U]) => V, ...ts:...T): (...us: ...U) => V {
    return us => f(...ts, ...us);
}
let h: (...us: [string, string]) = curry(f, 1);
let i: (s: string, t: string) = curry(f, 2);
h('hello', 'world');
function compose<...T,U,V>(f: (u:U) => U, g: (ts:...T) => V): (args: ...T) => V {
    return ...args => f(g(...args));
}
function first(x: number, y: number): string {
}
function second(s: string) {
}
let j: (x: number, y: number) => void = compose(second, first);
j(1, 2);

À FAIRE : f pourrait-il retourner ...U au lieu de U ?

Décorateurs

function logged<...T,U>(target, name, descriptor: { value: (...T) => U }) {
    let method = descriptor.value;
    descriptor.value = function (...args: ...T): U {
        console.log(args);
        method.apply(this, args);
    }
}

Questions ouvertes

  1. L'histoire d'assignabilité de tuple à liste de paramètres tient-elle? C'est particulièrement fragile autour des paramètres optionnels et de repos.
  2. Le type inféré sera-t-il une union de tuples comme dans le cas du paramètre optionnel ? Étant donné que bind , call et apply sont des méthodes définies sur Function, leurs arguments de type doivent être liés au moment de la création de la fonction plutôt que le site d'appel bind (par exemple). Mais cela signifie que les fonctions avec des surcharges ne peuvent pas prendre ou renvoyer des types spécifiques à leurs arguments -- elles doivent être une union des types de surcharge. De plus, Function n'a pas de constructeur qui spécifie directement les arguments de type, il n'y a donc vraiment aucun moyen de fournir les types corrects à bind et al. À FAIRE : Ajoutez un exemple ici. Notez que ce problème n'est pas nécessairement unique aux fonctions variadiques.
  3. Les paramètres de repos devraient-ils être dans une casse spéciale pour conserver leur belle syntaxe d'appel, même lorsqu'ils sont générés à partir d'un type de tuple ? (Dans cette proposition, les fonctions typées par un genre de tuple doivent passer des tableaux à leurs paramètres de repos, elles ne peuvent pas avoir de paramètres supplémentaires.)
Fix Available In Discussion Suggestion

Commentaire le plus utile

Ce problème est maintenant résolu par #39094, prévu pour TS 4.0.

Tous les 265 commentaires

+1, c'est vraiment utile pour la programmation fonctionnelle en TypeScript ! Comment cela fonctionnerait-il avec des arguments optionnels ou de repos ? Plus concrètement, la fonction compose peut-elle être utilisée sur des fonctions avec des arguments rest ou des arguments optionnels ?

Bon point. Je pense que vous pourriez affecter le plus petit type de tuple autorisé à une fonction de paramètre facultatif, car les tuples ne sont que des objets, qui autorisent des membres supplémentaires. Mais ce n'est pas l'idéal. Je vais voir si je peux comprendre l'exemple compose , puis je mettrai à jour la proposition.

En fait, les types d'union fonctionneraient probablement mieux. Quelque chose comme

function f(a: string, b? number, ...c: boolean[]): number;
function id<T>(t: T): T;
let g = compose(f, id): (...ts: ([string] | [string, number] | [string, number, boolean[]]) => number

g("foo"); // ok
g("foo", 12); // ok
g("foo", 12, [true, false, true]); // ok

Cela casse toujours les paramètres de repos, cependant.

@ahejlsberg , vous aviez des idées sur la façon dont les types de tuple fonctionneraient, je pense.

Donc :+1: là-dessus. Pour information, ceci est lié à (et remplirait) #3870. Nous avons essayé d'implémenter une API de type composition dans TypeScript, mais nous devons contourner certaines des limitations notées dans cette proposition. Cela résoudrait certainement certains de ces problèmes!

Il semble cependant que vous souhaitiez parfois "fusionner" de tels types de tuples au lieu de les persister, en particulier avec quelque chose comme composer. Par exemple:

function compose<T, ...U>(base: T, ...mixins: ...U): T&U {
    /* mixin magic */
}

De plus, dans beaucoup de vos exemples, vous avez utilisé des primitives. Comment verriez-vous quelque chose de plus complexe fonctionner, surtout s'il y a des conflits ?

Malheureusement, cette proposition telle quelle ne traite pas de #3870 ou de la composition de type, puisque le seul opérateur de composition pour les types de tuples est [T,...U] . Vous pouvez également l'écrire sous la forme T + ...U (ce qui est plus indicatif de ce qui arrive aux types), mais #3870 et votre bibliothèque de composition de types ont besoin de T & ...U . Je pense que cela pourrait être possible, mais je dois d'abord comprendre les idées de @JsonFreeman et

Remarque : j'ai décidé d'utiliser la syntaxe [...T, ...U] car elle ressemble à la syntaxe de diffusion de valeur équivalente, mais T + ...U est plus indicatif de ce qui se passe avec les types. Si nous nous retrouvons avec les deux, alors + et & pourraient être les opérateurs à utiliser.

Gros :+1: sur ça !

+1 génial ! Cela permettrait d'exprimer de telles choses de manière beaucoup plus expressive et légère.

Mon point dans #3870 semble être un problème ici. Plus précisément, je m'inquiète de déduire des arguments de type pour les paramètres de type variadiques.

L'inférence d'argument de type est un processus assez compliqué, et il a changé de manière subtile au fil du temps. Lorsque des arguments sont comparés à des paramètres afin d'inférer des arguments de type, il n'y a aucune garantie sur l'ordre dans lequel les candidats sont inférés, ni sur le nombre de candidats inférés (pour un paramètre de type donné). Cela n'a généralement pas été un problème car le résultat présenté à l'utilisateur n'expose pas (dans la plupart des cas) ces détails. Mais si vous créez un type de tuple à partir des résultats d'inférence, cela expose certainement à la fois l'ordre et le nombre des inférences. Ces détails n'étaient pas censés être observables.

Est-ce grave ? Je pense que cela dépend de la façon dont fonctionne exactement l'inférence. Quel est le résultat de ce qui suit :

function f<...T>(x: ...T, y: ...T): ...T { }
f(['hello', 0, true], [[], 'hello', { }]); // what is the type returned by f?

@jbondc , - semble être une bonne idée. Je vais le garder à l'esprit mais pas l'explorer ici, car je pense que nous devrions introduire de nouveaux opérateurs de type un à la fois. & et + créent de nouveaux types, mais & crée un type d'intersection alors que + crée un nouveau type de tuple (c'est pourquoi je préfère la syntaxe [T,...U] au lieu de T + ...U , car [T,U] déjà pour les types).

@JsonFreeman Je pense qu'il est acceptable de faire l'une des deux choses avec des paramètres de type répétés :

  1. Union les types : f(['hello', 1], [1, false]): [string | number, number | boolean]
  2. Interdire l'inférence de paramètres de type de tuple répétés, en particulier si l'inférence d'argument de type s'avère compliquée. Quelque chose comme ça:
f(['hello', 1], [1, false]) // error, type arguments required
f<[string, number]>(['hello', 1], [1, false]) // error, 'number' is not assignable to 'string'
f<[string | number, number | boolean]>(['hello', 1], [1, false]); // ok

Je pense que les vraies bibliothèques (comme les extensions réactives liées à

Dans les exemples ci-dessus, curry est le plus difficile à déduire -- vous devez ignorer f: (...args:[...T,...U]) => V , déduire ...ts:...T , puis revenir en arrière et définir ...U sur ce qui est laissés après avoir consommé ...T partir des paramètres de f .

J'ai commencé à prototyper ceci (sandersn/TypeScript@1d5725d), mais je ne suis pas encore allé aussi loin. Une idée si cela fonctionnera ?

Je me tromperais en refusant tout ce dont la sémantique n'est pas claire (comme des inférences répétées au même paramètre de type étalé). Cela apaise également mon inquiétude ci-dessus.

Je ne peux pas penser à un bon mécanisme pour taper du curry. Comme vous le soulignez, vous devez ignorer la liste des paramètres de la première fonction pour consommer l'argument ...T , puis voir ce qui reste. Il devrait y avoir une politique pour reporter les inférences à un paramètre de type étendu s'il n'est pas final dans sa liste. Cela pourrait devenir désordonné.

Cela dit, je pense que cela vaut la peine d'essayer. Il y a une forte demande pour la fonctionnalité.

Je pense que vous devriez ignorer plusieurs types de tuples qui se produisent dans le même contexte (par exemple, de niveau supérieur comme (...T,string,...U) => V ou concaténés comme [...T,...U,...T] ). Ensuite, vous pouvez effectuer plusieurs passages sur les types ignorés, en éliminant les types déjà déduits et en sautant à nouveau les types encore ambigus. Si, à un moment donné, aucun type unique n'est disponible pour l'inférence, arrêtez et renvoyez une erreur.

Donc voilà. Compliqué.

Vous pourrez peut-être vous inspirer d'un problème similaire. C'est en fait un peu similaire au problème de déduire une union ou une intersection. Lors de l'inférence à un type d'union qui inclut un paramètre de type membre du contexte d'inférence, comme dans function f<T>(x: T | string[]) , vous ne savez pas s'il faut inférer à T. La manifestation prévue du type d'union peut avoir été string[] . Donc tapuscrit déduit d'abord tous les autres constituants, puis si aucune déduction n'a été faite, déduit T.

Dans le cas de l'intersection, c'est encore plus difficile car vous devrez peut-être diviser le type de l'argument entre les différents constituants de l'intersection. Typescript ne fait aucune inférence aux types d'intersection.

Et si vous n'autorisiez la propagation du tuple que s'il s'agit du dernier type de sa séquence ? Donc [string, ...T] serait autorisé, mais [...T, string] ne le serait pas ?

Si je comprends bien, cela résoudrait en fait l'histoire du mixin dans TypeScript. Ai-je raison dans cette compréhension?

Peut-être. Pouvez-vous donner un exemple? Je ne suis pas à l'aise avec les modèles de mixin.

La syntaxe d'une variable de type variadique est ...T où T est un identifiant qui est par convention une seule lettre majuscule, ou T suivi d'un identifiant PascalCase.

Peut-on laisser le cas d'un identifiant de paramètre de type au développeur ?

@aleksey-bykov +1. Je ne vois pas de raison pour que ce ne soit pas le cas.

Les développeurs ayant une formation Haskell apprécieraient cela.

Désolé, cette phrase peut être analysée de manière ambiguë. Je voulais dire "ou" pour analyser étroitement : "par convention (une seule lettre majuscule || T suivie d'un identifiant PascalCase)". Je ne propose pas de contraindre le cas des identifiants, je rappelle juste la convention.

Pour ce que ça vaut, cependant, _J'ai une formation en Haskell et je n'aime pas briser les conventions de la langue dans laquelle j'écris.

Désolé d'avoir déraillé. Ma dernière question curieuse (si cela ne vous dérange pas que je pose la question) quelle est la "convention" de TypeScript qui pourrait être enfreinte et qui est concerné ?

@sandersn

Cela devrait taper check, en supposant que T & ...U signifie T & U & V & ... (qui est le comportement intuitif).

function assign<T, U, ...V>(obj: T, src: U, ...srcs: ...V): T & U & ...V {
  if (arguments.length < 2) return <T & U & ...V> obj

  for (const key of Object.keys(src)) {
    (<any> obj)[key] = (<any> src)[key]
  }

  if (arguments.length === 2) return <U> obj
  return mixin<T, ...V>(obj, ...srcs)
}

Ou dans un fichier de définition :

interface Object {
    assign<T, U, ...V>(host: T, arg: U, ...args: ...V): T & U & ...V
}

@aleksey-bykov la convention dont je parle est le cas des identificateurs de paramètres de type. Qui est concerné ? Les personnes qui doivent lire un nouveau code Typescript qu'elles n'ont jamais vu auparavant - les conventions aident les nouveaux lecteurs à comprendre le nouveau code plus rapidement.

@sandersn Ce que @aleksey-bykov a eu l'impression que ce qui suit serait _syntaxiquement_ invalide :

function assign<a, b, ...cs>(x: a, y: b, ...zs: ...cs): a & b & ...cs;

Les opérations @isiahmeadows & et | sur les genres ne sont pas couvertes dans cette proposition, bien que je devrais les ajouter aux questions ouvertes/travail futur si je ne l'ai pas fait. À l'heure actuelle, le seul opérateur proposé est la concaténation : [THead, ...TTail] .

Une différence est que la concaténation produit toujours un type de tuple tandis que & et | produisent respectivement des types d'intersection et d'union.

@sandersn Mon exemple assign dans TypeScript serait trivial à changer avec ça.

Même si:

  1. L'intersection serait similaire à la concaténation, bien que cela ressemble plus à la concaténation de dictionnaires qu'à la concaténation de listes. Les types variadiques peuvent y être implémentés par-dessus les machines existantes.
  2. L'union serait comme l'intersection, sauf qu'elle ne garde que les parties communes. Encore une fois, des types variadiques pourraient être implémentés en plus des machines existantes.

@isiahmeadows Une intersection n'est pas en général une concaténation de dictionnaires. Cela n'est vrai que pour une intersection de types d'objets, mais pas par exemple une intersection d'unions. Les unions ne sont pas non plus la même chose que de simplement prendre les propriétés que les objets ont en commun. Les deux sont mieux caractérisés par l'ensemble des valeurs qui les habitent.

@sandersn Je suis un peu confus au sujet de l'inférence d'argument de type avec des types variadiques. Que faut-il en déduire ici ?

function foo<...T>(...rest: ...T): ...T { }
foo('str', 0, [0]);

Le résultat est-il [string, number, number[]] ? Cela signifierait que vous devez vous fier à l'inférence d'arguments de type en ajoutant des candidats dans un ordre de gauche à droite, ce qui n'est pas une hypothèse triviale. Ce serait également la première fois que le système de types présente la liste des candidats à l'inférence à l'utilisateur.

Je sais que c'est une proposition expérimentale / précoce, mais nous pourrions discuter ...T syntaxe
La syntaxe proposée est donc :

declare function f<...T>(...a: ...T);

comparons avec la syntaxe existante des paramètres de repos :

declare function f(...a: number[]);

donc le type de paramètre a qui attrape les arguments de repos est number[] , donc nous pouvons clairement comprendre qu'il s'agit d'un tableau. Par analogie, je peux en déduire que ...T de la proposition représente également un tableau. Mais ce n'est pas très évident.
Ensuite, disons que nous pourrions définir des paramètres de repos plus restrictifs :

declare function f(...a: [number, string]);
// same as
declare function f(c: number, d: string); // or very close to

Alors maintenant, nous voyons toujours que le type de a est un tuple (qui est un tableau).

Ma proposition est d'utiliser une manière plus cohérente de traiter la notion de ...T pour la représenter comme "une liste ordonnée abstraite de types". Et utilisez-le de la même manière que nous utilisons l'opérateur spread :

var a: [number, string] = [1, "1"];
var b = [true, ...a]; // this must be [boolean, number, string], but it doesn't work :)

Donc ...a en cas de variable, est juste 1, "1" .

Ma syntaxe pour définir les paramètres de repos par la notion ...T :

declare function f<...T>(...a: [...T]);
declare function g<H, ...T>(head: H, ...tail: [...T]): [H, ...T];

Pour moi, cela a beaucoup plus de sens.

@Igorbek J'ai couru en supposant que declare function f<...T>(...a: ...T); fonctionnait déjà comme ça. Mais je ne vois pas l'utilité de declare function f(...a: [number, string]); .

Pour être plus clair.

Syntaxe initialement proposée pour les paramètres de repos :

function func<...T>(...a: ...T)

Si je peux faire ça

function g<...T>(...a: ...T): [number, ...T] { ... }

alors je serai capable de faire ceci:

function f<...T>(...a: ...T): [...T] { return a; }

Donc le type de a est [...T] (nous retournons ainsi), mais nous l'avons défini comme ...T dans la signature.
On pourrait dire que ...T et [...T] sont identiques, mais cela ne fonctionne pas en cas de variables.
Pour les variables :

var a = [1, 2];
[a] === [[1,2]];
[...a] === [1, 2];
f(...a) === f(1, 2)
...a === 1, 2 // virtually

Si nous appliquons la même chose aux paramètres de repos standard

function f(...a: number[]): number[] { return a; }

le type de a est number[] (par type de retour), comme il a été défini dans la signature.

@isiahmeadows oui, function f(...a: [number, string]) ne fonctionne pas. Je viens de développer des réflexions sur la façon dont nous pouvons traiter les paramètres de repos.

Donc, aller plus loin. Pour définir explicitement les paramètres de type, la syntaxe suivante a été proposée :

function f<...T, ...U>()
f<[number, string], [boolean, number]>();

Se tourne vers:

f<...[number, string], ...[boolean, number]>();

Donc ça peut marcher aussi :

function g<T1, T2, T3>()

g<A, B, C>();
// same as
g<...[A, B, C]>();
g<...[A], ...[B, C]>(); 
g<...[A], B, C, ...[]>();

@JsonFreeman c'est

Pouvez-vous également travailler sur un exemple d'ensemble exposé de candidats à l'inférence de type ? Comme je l'ai dit, je ne comprends pas très bien le fonctionnement de l'algorithme d'inférence, donc un exemple m'aiderait à voir ce que vous voulez dire.

Et encore mieux :

function<...T>(...a: T): T;
// same as
function<...T>(...a: [...T]): T;

Je suggère de préfixer [] à l'identifiant de type pour signifier le reste des paramètres de type.

function fn<R, []T>(...a:[]T): R;

C'est 1 caractère plus court que ...T et (à mon avis) fait moins de bruit visuel.

@aleksey-bykov Je suis en fait de l'avis contraire à ce sujet. Cela ne correspond pas à la syntaxe des paramètres de repos existante, donc je pense que c'est également moins clair d'un coup d'œil.

[...T] / T tant que type de paramètre de tableau de repos me semble bien meilleur. Encore une fois, comparez avec array et leur opérateur sprad :

| tableaux | types (de la proposition) | types (ma mise à jour) |
| --- | --- | --- |
| var x = [1,2] | non | T = [T1, T2] |
| [0, ...x] === [0,1,2] | [T0, ...T] === [T0, T1, T2] | [T0, ...T] === [T0, T1, T2] |
| f(x) === f([1, 2]) | non | f<T>() === f<[T1, T2]>() |
| f(...x) === f(1, 2) | f<...T>() === f<[T, T2]> ? | f<...T>() === f<T1, T2> |
| f(0, ...x) === f(1, 2) | f<T0, ...T>() === f<T0, [T, T2]> ? | f<T0, ...T>() === f<T0, T1, T2> |

De la proposition

function g<...T>(...x: ...T) {
 // being called as g(1, "a");
  var a: ...T; // [number, string] ?
  var b: [number, ...T]; // [number, number, string]
  var c: [...T]; // [number, string] - same as a ? so [...T] is same as ...T - weird
}

De ma mise à jour

function g<...T>(...x: T) {
 // being called as g(1, "a");
  var a: T; // [number, string]
  var b: [number, ...T]; // [number, number, string]
  var c: [...T]; // [number, string]
}

La mise à jour semble plus agréable maintenant IMO. Les listes pour représenter les types sonnent très bien, mais même les lisps typés ne vont pas aussi loin (les types homoiconiques, n'importe qui? :sourire:).

Je reçois l'attrait de la pureté, mais je regarde aussi l'aspect pragmatique. Les listes seraient également relativement faciles à implémenter seules, mais elles ne correspondent pas au reste du langage. C'est presque comme les nombreuses tentatives d'implémentation de monades en Java (le langage) ou de lambdas en C - elles s'avèrent toujours incroyablement moches et hackish.

@sandersn je peux essayer d'expliquer ce que je veux dire en exposant la liste des candidats. L'inférence d'argument de type génère une liste de candidats pour chaque paramètre de type. Ensuite, il vérifie si un candidat est un supertype de tous les autres, et si c'est le cas, ce candidat est le gagnant. Donc dans l'exemple suivant :

function foo<T>(a: T, b: T): T {}
foo(["hi", 0], ["", ""]);

Les arguments seront saisis, puis déduits de chaque paramètre. Deux candidats seront générés, soit (string | number)[] et string[] . Mais le premier va gagner car c'est un supertype du second. Et par conséquent, l'utilisateur n'observe jamais que string[] n'a jamais été dans l'image. Il y a une inférence pour T , et tous les autres candidats sont invisibles. Cela signifie qu'il y a deux choses invisibles pour l'utilisateur, à savoir l'ordre des candidats et les multiplicités des candidats.

Voici un problème avec les multiplicités si vous vous fiez à la liste des candidats comme liste d'éléments dans le tuple noté ...T :

function foo<...T>(...rest: ...T): ...T
foo(0, 1);

Je pense que vous voudriez déduire [number, number] pour T étant donné l'intention de votre proposition telle que je la comprends. Mais en raison de la ligne de vérification contient https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts#L6256 , le candidat number ne sera ajouté qu'une seule fois, et T sera déduit comme [number] . C'est le problème de la multiplicité dont je parlais.

Quant à l'ordre, il est de gauche à droite. Mais il y a plusieurs passes, et les arguments seront retraités s'ils contiennent des expressions de fonction qui seront typées contextuellement. S'il y a n arguments contenant des expressions de fonction typées contextuellement, alors il y a n + 1 passages sur les arguments. Un exemple est Array.prototype.reduce, où le paramètre initialValue est effectivement tapé et déduit avant le rappel, malgré le fait qu'il se trouve à droite. Ainsi, quelque chose comme ce qui suit pourrait être un problème pour la proposition :

function foo<...T>(...rest: ...T): ...T
foo(x => x, 0);

Intuitivement, T devrait être [(x: any) => any, number] , mais si vous vous fiez à l'ordre dans lequel les candidats sont ajoutés, ce sera [number, (x: any) => any] . En effet, l'inférence des arguments de type est généralement de gauche à droite, mais les fonctions soumises au typage contextuel sont reportées à la fin.

La multiplicité et les problèmes d'ordre que j'ai expliqués sont des exemples d'apparition de la liste des candidats. @ahejlsberg sera sûrement une bonne personne pour poser des questions à ce sujet, et en effet, il peut aider à expliquer, confirmer ou réfuter tout ce que j'ai dit.

@JsonFreeman pourquoi pensez-vous que ce serait un problème ?
Il peut être implémenté en introduisant virtuellement des types génériques supplémentaires pour chaque argument factuel de repos et en inférant une fonction avec une longueur de paramètres fixe.
Par exemple,

function foo<...T>(...rest: T) { ... }
foo(x => x, 0);
// to infer, the following function is used
function foo2<T0, T1>(rest0: T0, rest1: T1) { ... }
foo2(x => x, 0);
// inferred as
foo2<(x: any) => any, number>
// T0 = (x: any) => any
// T1 = number
// T = [T0, T1] = [(x: any) => any, number]

BTW, pouvons-nous en déduire que x => x est de type { <T>(x: T): T; } ?

@Igorbek Je pense que votre suggestion sur les paramètres de type de fabrication (au moins comme intuition, quelle que soit la façon dont elle est implémentée) est la bonne façon de le faire. Vous pouvez déduire une séquence de types pour T , où chaque élément de la séquence a un index et une liste de candidats (c'est une autre façon de mettre en œuvre ce que vous avez mentionné).

Cependant, mon point était que je ne pense pas que ce soit ce qui se produirait naturellement si vous réutilisiez simplement la liste des candidats d'inférence en tant que tuple inféré. Il faudrait des mécanismes explicites pour que la bonne chose se produise.

Pour votre point sur { <T>(x: T): T; } , cela ne se généralise pas bien à la saisie de choses comme x => foo(x) où foo est une fonction. Vous auriez besoin de connaître le type de x pour effectuer une résolution de surcharge pour foo .

Un petit pas en dehors de la bataille avec les règles d'inférence du vérificateur de type.
J'ai un commentaire/une suggestion sur la syntaxe. Je pense qu'il existe deux options cohérentes mais mutuellement exclusives :

1. Arguments formels de type repos

Si nous choisissons ce formulaire :

type F<...Args> = (...args:...Args) => ...Args

alors nous devrions l'utiliser comme

var a:  F // a: () => []
var b:  F<number> // b: (arg: number) => [number]
var c:  F<number, string> // c: (arg1: number, arg2: string) => [number, string]
...

Ainsi ce sera de vrais types formels de repos. Ils ne doivent être utilisés qu'à la dernière position de la section de paramètre de type formel.

2. Arguments de repos de type tuple

(...args:[string, number]) => boolean    IS EQUIVALENT TO   (s: string, n: number) => boolean

Dans ce cas, nous avons toujours un nombre fixe d'emplacements dans la section des paramètres de type formel.

function f<T>(...args: T): T {
    return args;
}

nous en déduisons que T devrait être un type tuple si l'une des conditions est remplie :

  1. T est utilisé pour les paramètres de repos comme (...args: T) => T
  2. T est utilisé dans la composition de propagation comme [...T] ou [nombre, ...T, chaîne]

Ainsi, nous n'avons pas besoin d'utiliser des points de suspension dans la section des paramètres de type formel (nous pouvons le déduire même _syntaxiquement_ sans aucun vérificateur de type)

dans ce cas, on peut aussi écrire

function f<T>(...args: [...T]): [...T] {
    return args;
}

mais c'est redondant.

Personnellement, j'aimerais voir le dernier implémenté dans TypeScript. @JsonFreeman , @sandersn ?

@Artazor Je pense que cela se résume à l'expressivité, et je ne pense pas que les deux approches soient nécessairement équivalentes. Le second inclut la possibilité de diffuser un paramètre de type rest à l'intérieur d'un type de tuple, alors que le premier ne semble pas le faire.

Je pense que pour les références de type générique, il s'agit simplement de décider où et syntaxiquement comment utiliser un paramètre de type rest. Cela devrait être décidé pour tous les constructeurs de types qui prennent une séquence de types (tuples, signatures, références de types génériques).

Pour les signatures génériques, c'est plus compliqué à cause de l'inférence d'argument de type. Et si vous aviez les éléments suivants :

function callback(s: string, n: number): void { }
declare function foo<...T>(cb: (...cbArgs: T) => void, ...args: T): [...T];

foo(callback, "hello", 0, 1);

Qu'est-ce que foo retourne? Mon point est simplement que les gens s'attendent à ce que les règles génériques soient les mêmes pour les types génériques et les signatures génériques, mais si vous rendez les types génériques plus expressifs, l'inférence d'arguments de type a besoin d'un moyen de le gérer. Il peut s'agir simplement d'identifier formellement les cas difficiles pour l'inférence d'arguments de type et de demander à l'utilisateur de passer des arguments de type explicites dans ces cas.

Pour ce qui est de mon avis, je pense que votre option 1 est meilleure. Personnellement, je ne vois pas l'utilité d'utiliser des types de tuple comme paramètres de repos. Je pense qu'un paramètre de repos ne devrait être autorisé qu'à être un type de tableau ou un paramètre de type de repos, car il est censé avoir une longueur variable. J'aime aussi le concept d'un paramètre de type rest étant une séquence abstraite de types, non associée à quelque chose qui existe déjà dans le système de types.

Ma philosophie sur les tuples est qu'ils représentent un sous-ensemble de valeurs de tableau dont la longueur est connue. Ces valeurs de tableau sont des entités d'exécution réelles. Je n'aime pas l'idée de les utiliser comme une sorte de périphérique de système de types pour représenter une séquence abstraite de types (par exemple la séquence de paramètres dans une signature). Mais que vous soyez autorisé à diffuser un paramètre de type rest dans un tuple est une autre histoire.

J'aime la proposition de tuple car elle est plus puissante et résout plus de cas d'utilisation, il est également très intuitif que je puisse diffuser un tuple en tant que paramètre de repos car les tuples ne sont que des tableaux et lors de l'appel d'une fonction avec un paramètre de repos, je peux diffuser le tableau. Le système de types correspondrait alors mieux à ma compréhension du code.

@JsonFreeman dans votre cas foo renverrait [string, number, number] car cela serait déduit de ...args , le type cb déduit serait (string, number, number) => void et le rappel passé ignorerait simplement le dernier argument qui est très commun dans les deux TS et JS.

Je n'aime pas l'idée de les utiliser comme une sorte de périphérique de système de types pour représenter une séquence abstraite de types

C'est exactement ce qu'ils sont, JS ne connaît pas les tuples, seulement TS. Pour TS, un tuple est une séquence de types.

J'aime aussi l'approche basée sur les tuples. Surtout si on pouvait avoir des signatures de fonctions compatibles comme ça :

// all are equivalent
(a: A, b: B, c: C) => R;
(a: A, b: B, ...rest: [C]) => R;
(a: A, ...rest: [B, C]) => R;
(...args: [A, B, C]) => R;

// this is more complicated 
(a: A, ...rest: T[]) => R;
(...args: [A, ...T]) => R; // no in current syntax

Ce dernier, nous ne pouvons pas l'exprimer avec la syntaxe actuelle, mais nous le pourrions si nous avions adopté #6229.
Donc, pour moi, il semble qu'un moyen approprié soit d'utiliser des tuples et d'unifier des tuples pour exprimer davantage. Sans tuples plus expressifs, il serait difficile d'avoir quelque chose comme [...T, ...T] car T en tant que tuple a une longueur ouverte.

@JsonFreeman pour votre exemple, @Pajn a montré exactement ce que je comprends de cela - il n'y a aucun problème visible à déduire ces types.

@JsonFreeman, je ferais mieux d'utiliser cette syntaxe

declare function foo<T>(cb: (...cbArgs: T) => void, ...args: T): T;
declare function foo<T>(cb: (...cbArgs: T) => void, ...args: T): [...T]; // same

Hm, cela peut probablement introduire une certaine ambiguïté :

declare function foo<T>(...args: T): T;
foo(1); // T is [number] or number[]?

// however, here it'd be more explicit
declare function foo<T>(...args: T[]): T[];
foo(1); // T is number[]

// and here
declare function foo<T>(...args: [...T]): T;
foo(1); // T is [number]

Je pourrais accepter l'idée de diffuser un paramètre de type rest dans un tuple. Mais je ne suis pas sûr de vouloir qu'un paramètre de type rest soit implicitement interprété comme un tuple. L'exemple de @Pajn fonctionnerait toujours si les paramètres de type rest sont autorisés à être répartis dans toutes les positions de séquence de type (tuples, listes de paramètres, arguments de type).

@Igorbek Vous avez raison sur l'ambiguïté de votre premier exemple. Votre troisième exemple est également problématique. Étant donné une séquence comme number, string , il y a 2 instanciations possibles de la signature. À savoir (arg1: number, arg2: string) => [number, string] ainsi que (arg1: [number, string]) => [number, string] (en adoptant l'interprétation de tuple implicite pour l'exemple).

L'autre chose étrange à propos de l'interprétation implicite des tuples est la suivante : disons que vous avez un paramètre de type rest T instancié à number, string . Supposons maintenant que vous les transmettez en tant qu'arguments de type, Foo<T> . Est-ce à interpréter comme Foo<[number, string]> alors que Foo<...T> est Foo<number, string> ? Il y a un argument pour cela, car cela étendrait l'opérateur spread au système de types. Mais je préfère quand même que la version tuple soit représentée par Foo<[...T]>

Appelez-moi fou, mais je sens des défauts fondamentaux à l'idée d'utiliser
tuples. Que se passe-t-il si vous essayez de répartir un type de tuple sur trop de
paramètres? Comme ça?

declare function foo<T>(...args: [...T]): void
foo<[number]>(1, 2)

Aussi, que se passe-t-il si les paramètres de type sont du mauvais type ou utilisés dans
lieux insolites, potentiellement erronés ?

// 1. unusual place
declare foo<T>(x: T, ...ys: [...T]): void

// 2. bad type
declare foo<T>(...xs: [...T]): void
foo<number>(2)

Le premier exemple est directement pertinent pour Function#apply (et pourrait être un
erreur), et la seconde est une erreur non évidente qui échouera à compiler,
et non trivial à détecter avec Intellisense.

Le dimanche 28 février 2016, 03:04 Jason Freeman [email protected] a écrit :

L'autre chose étrange à propos de l'interprétation implicite des tuples est la suivante :
vous avez un paramètre de type rest T instancié en nombre, chaîne.
Maintenant, disons que vous les transmettez en tant qu'arguments de type, Foo. Est-ce pour être
interprété comme Foo<[nombre, chaîne]> alors que Foo<...T> est Foo chaîne>?

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

@JsonFreeman

Votre troisième exemple est également problématique. Étant donné une séquence comme number, string , il y a 2 instanciations possibles de la signature. À savoir (arg1: number, arg2: string) => [number, string] ainsi que (arg1: [number, string]) => [number, string] (en adoptant l'interprétation de tuple implicite pour l'exemple).

D'après mon troisième exemple, il est clair qu'il ne peut être interprété que comme (...args: [number, string]) => [number, string] :

declare function foo<T>(...args: [...T]): T;
foo(1, "a"); // T is [number, string]
const result: [number, string] = foo<[number, string]>(1, "a");

// however, it is assignable to/from the following signatures:
const f1: (arg1: number, arg2: string) => [number, string] = foo<[number, string]>;
const f2: (arg1: number, ...rest: [string]) => [number, string] = foo<[number, string]>;

L'autre chose étrange à propos de l'interprétation implicite du tuple est la suivante : disons que vous avez un paramètre de type rest T instancié à number, string .

T ne peut pas être instancié en number, string car il s'agit d'un vrai tuple. Il doit être [number, string] .

Supposons maintenant que vous les transmettez en tant qu'arguments de type, Foo<T> . Est-ce à interpréter comme Foo<[number, string]> alors que Foo<...T> est Foo<number, string> ?

Vrai. Cependant, avoir <...T> semble redondant pour ces cas d'utilisation particuliers dont nous discutons (capture des types positionnés pour les arguments de repos). Néanmoins, disons que nous l'avons.

Il y a un argument pour cela, car cela étendrait l'opérateur spread au système de types. Mais je préfère quand même que la version tuple soit représentée par Foo<[...T]>

Il y a deux cas où nous pourrions utiliser cette syntaxe :

// in a signature declaration
declare function foo<[...T]>(...args: [...T]): [...T];
// and when type instantiated, so in the usage
type T = [number, string]
foo<T>();
foo<[...T]>();
// the latter can virtually be replaced as
type _T = [...T]; // which is a type operation that should produce [number, string]
foo<_T>();
// and more
type Extended = [boolean, ...T]; // [boolean, number, string]

Donc, pour l'utilisation, ce n'est rien de plus qu'un opérateur de type comme | , & ou [] . Mais dans la déclaration, cette syntaxe peut être interprétée comme T extends any[] ou n'importe quel type de base pour tous les tuples, pour indiquer qu'il doit s'agir d'un type de tuple.

@isiahmeadows

Que se passe-t-il si vous essayez de répartir un type de tuple sur trop de
paramètres? Comme ça?

declare function foo<T>(...args: [...T]): void
foo<[number]>(1, 2); // ok, foo<[number]> is of type (...args: [number]) => void
// [1, 2] is being passed in place of args
// is [1, 2] which is [number, number] assignable to [number]? yes, with current rules
// no error

Aussi, que se passe-t-il si les paramètres de type sont du mauvais type ou utilisés dans
lieux insolites, potentiellement erronés ?

// 1. unusual place
declare foo<T>(x: T, ...ys: [...T]): void
// 1. [...T] can be interpret as a type constraint "must be a tuple type"
// 2. if we call with type specified
foo<number>(1); // number doesn't meet constraint
foo<[number]>(1, 2); // argument of type 'number' is not assignable to parameter 'x' of type '[number]'
foo<[number]>([1], 2); // ok
// 3. if we call without type, it must be inferred
foo(1); // according to current rules, T would be inferred as '{}[]' - base type of all tuples
        // so, argument of type 'number' is not assignable to parameter 'x' of type '{}[]'
foo([1, 2], 2); // T is inferred as '[number, number]
                // rest arguments of type '[number]' are not assignable to rest parameters 'ys' of type '[number, string]'
foo([1], 2, 3); // T is '[number]',
                // x is of type '[number]',
                // ys is of type '[number]',
                // rest arguments are of type '[number, number]' which is assignable to '[number]',
                // no error

// 2. bad type
declare foo<T>(...xs: [...T]): void
foo<number>(2); // type 'number' doesn't meet constraint

Je ne vois toujours pas l'intérêt de représenter ces choses sous forme de tuples. De plus, je pense qu'ils devraient être déclarés comme <...T> et non <T> . Comme je l'ai déjà dit, je ne considère pas les types de tuples comme un périphérique approprié à utiliser pour des séquences de types de longueur arbitraire dans le système de types. Je ne suis toujours pas convaincu que cela soit nécessaire pour l'expressivité que les gens veulent.

Je suis d'accord que cela pourrait être plus expressif, mais avoir l'opérateur 'spread' dans la position des paramètres de type nous limitera à attraper les arguments de repos une seule fois, de la même manière que nous ne pouvons pas avoir de paramètres de repos deux fois. Donc, étant donné <...T> et <A, B, C> , T les attrapera comme [A, B, C] . Et nous ne serions pas en mesure d'exprimer <...T, ...U> car ce serait ambigu - [A, B, C], [] ou [A, B], [C] ou ... etc.

Disons que je voulais exprimer une fonction avec le comportement suivant :

declare function foo(a: A, b: B): R;
declare function boo(c: C, d: D, e: E): U;

let combined: (a: A, b: B, c: C, d: D, e: E) => [R, U] = combine(foo, boo);

// so the signature could be:

declare function combine<R, U, ???>(
  f1: (...args: [...T1]) => R,
  f2: (...args: [...T2]) => U):
    (...args: [...T1, ...T2]) => [R, U];

// if ??? is '...T1, ...T2'
combine<R, U, A, B, C, D, E> // what will be T1 and T2 ?
combine<R, U, ...[A, B, C], ...[D, E]> // ok ? so we will preserve spread to specific positions. so then
combine<...[R, U], A, ...[B, C, D], E> // will be restricted.
// however, ES6 allows to do it with function arguments
f(1, 2, 3);
f(...[1, 2], 3);
f(...[1], ...[2, 3]);

// if ??? is 'T1 extends TupleBase, T2 extends TupleBase'
// or just '[...T1], [...T2]' as a shortcut for such constraints
combine<R, U, [A, B, C], [D, E]> // pretty explicit, and doesn't occupy spread operator for type arguments

Ok, je vois maintenant comment tu penses. Il semble que ce que vous proposez soit en fait une caractéristique différente de ce que je pensais. Au lieu d'ajouter une nouvelle construction pour capturer une séquence de paramètres de type, vous souhaitez simplement que les types de tuples soient répartis car ils représentent déjà une séquence de types. De cette façon, il est possible de passer plusieurs tuples de différentes longueurs de manière plus transparente.

En javascript, cela ressemble plus à function foo([...rest]) { } au lieu de function foo(...rest) { } .

Cela a plus de sens pour moi maintenant, merci pour l'explication. Je pense que c'est une approche raisonnable.

@JsonFreeman Exactement !

@JsonFreeman Question : pourquoi [1, 2] devrait-il satisfaire [number] ? Cela me semble très étrange. Cela fonctionnerait réellement serait très surprenant. Ce n'est pas du tout sûr.

Cependant, je n'ai rien contre l'utilisation des tuples pour les types variadiques (je suis neutre, aussi, soyez honnête).

@isiahmeadows en quoi [1, 2] n'est-il pas substituable à [number] ? C'est certainement un sous-type. C'est la même chose que { x: 1, y: 2 } est un { x: number } valide

D'accord. Je concède en partie, mais prends en compte Function.prototype.apply, qui accepte un tuple d'arguments.

interface Function<T, U, V> {
    (this: T...args: [...U]): V;
    apply(object: T, args: U): V;
}

Si l'appelant renvoie une TypeError sur trop d'arguments, alors en passer trop entraînera une erreur d'exécution, et non une erreur de compilation comme il se doit.

N'est-il pas assez rare qu'une fonction JS lève TypeError lorsqu'elle passe trop d'arguments ? Quels sont quelques exemples ?

@isiahmeadows comme exemple abstrait, j'ai compris que l'erreur qui vous inquiète est :

function f(x: number): void {
  // throw if too many arguments
}
f.apply(undefined, [1,2,3]); // runtime error, no compile-time error
f(1,2,3) // compile-time error and runtime error.

Est-ce exact?

@sandersn , je pense que TypeError sur trop d'arguments est quelque chose qui viole l'esprit du JS, car nous passons généralement la fonction avec des arguments moins formels que ceux qui seront transmis à cette fonction. Nous ne les utilisons tout simplement pas. Par exemple Array.prototype.forEach

Qu'en est-il du curry de fonction ? C'est probablement beaucoup plus courant, avec Ramda
et lodash/fp.

Le lundi 29 février 2016, à 13h45, Anatoly Ressin [email protected] a écrit :

@sandersn https://github.com/sandersn , je pense que TypeError est également activé
de nombreux arguments est quelque chose qui viole l'esprit de la JS, car nous
passent généralement la fonction avec des arguments moins formels que les vrais qui
être passé dans cette fonction. Nous ne les utilisons tout simplement pas. Par exemple
Array.prototype.forEach

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

@isiahmeadows Je dirais que le curry basé sur le arguments.length est très instable et sujet aux erreurs d'exécution. Le vrai curry est à l'épreuve des arguments :

var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // still 7

Lorsque je passe ma fonction avec une signature fixe comme rappel quelque part, j'y pense de la manière suivante : "ma fonction attend _au moins_ ces arguments"

Qu'en est-il des choses comme foldl ?

const list = [1, 2, 3]
console.log(foldl((a, b) => a + b, 0, list))
console.log(foldl((a, b) => a + b, 0)(list))
console.log(foldl((a, b) => a + b)(0, list))
console.log(foldl((a, b) => a + b)(0)(list))

C'est très courant dans la programmation fonctionnelle. Et en omettant le dernier
l'argument est assez courant.

Le lundi 29 février 2016, à 13 h 52, Anatoly Ressin [email protected] a écrit :

@isiahmeadows https://github.com/isiahmeadows Je dirais que le curry
basé sur aruments.length est très instable et sujet aux erreurs d'exécution.
Le vrai curry est à l'épreuve des arguments :

var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // toujours 7

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

Si vous voulez passer cela comme rappel à, disons, map (travailler sur une liste
des listes), vous aurez probablement envie de le curry.

Le lundi 29 février 2016, 13:59 Isiah Meadows [email protected] a écrit :

Et des choses comme foldl ?

const list = [1, 2, 3]
console.log(foldl((a, b) => a + b, 0, list))
console.log(foldl((a, b) => a + b, 0)(list))
console.log(foldl((a, b) => a + b)(0, list))
console.log(foldl((a, b) => a + b)(0)(list))

C'est très courant dans la programmation fonctionnelle. Et en omettant le dernier
l'argument est assez courant.

Le lundi 29 février 2016, 13:52 Anatoly Ressin [email protected]
a écrit:

@isiahmeadows https://github.com/isiahmeadows Je dirais que le curry
basé sur aruments.length est très instable et sujet aux erreurs d'exécution.
Le vrai curry est à l'épreuve des arguments :

var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // toujours 7

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

Je pense que c'est surtout à cause de ça :

type T = [number, string];
var a: T = [1, "a", 2]; // valid

// in this cases tuple types or parameter types cannot be inferred:
f(...a, true); // you could think number,string,boolean were passed, but weren't
const c = [...a, true]; // you could think that is of type [number, string, boolean] but it's not
// according to current rules, the best inferred types might be [number, string, number|string|boolean]

// same manner with variadic kinds, types are constructed properly:
type R = [...T, boolean]; // [number, string, boolean]

C'est pourquoi j'ai proposé #6229

La question de savoir si [1, 2] satisfait [number] est une question valable à poser et à débattre. Mais qu'est-ce que cela a à voir avec la fonctionnalité de tuples extensibles?

C'est si une application variadique de tuples doit ignorer les arguments supplémentaires
ou pas. Cette fonction surchargée devrait développer davantage ma préoccupation.

declare function foo(x: number, ...args: string[]): void
declare function foo<T>(...args: [...T]): void
foo<[number]>(1, 2)

// This will always fail
declare function foo(x: number, ...args: string[]): void
declare function foo<T>(x: T): void
foo<number>(1, 2)

Le lundi 29 février 2016, à 18 h 47, Jason Freeman [email protected] a écrit :

La question de savoir si [1, 2] satisfait [nombre] est valable à poser
et débat. Mais qu'est-ce que cela a à voir avec la fonctionnalité de tuples extensibles?

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

Et c'est pourquoi, pour des raisons pratiques, je préférerais des paramètres de type repos
types variadiques.

Le lundi 29 février 2016, 19h00, Isiah Meadows [email protected] a écrit :

Il s'agit de savoir si une application variadique de tuples doit ignorer les
arguments ou pas. Cette fonction surchargée devrait élaborer plus de mon
préoccuper.

declare function foo(x: number, ...args: string[]): void


declare function foo<T>(...args: [...T]): void
foo<[number]>(1, 2)

// This will always fail
declare function foo(x: number, ...args: string[]): void
declare function foo<T>(x: T): void
foo<number>(1, 2)

Le lundi 29 février 2016, 18:47 Jason Freeman [email protected]
a écrit:

La question de savoir si [1, 2] satisfait [nombre] est valable à poser
et débat. Mais qu'est-ce que cela a à voir avec la fonctionnalité de tuples extensibles?

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

@JsonFreeman c'est parce que l'opérateur de propagation pour les types et les tableaux/tuples. Si l'opérateur de type propagation est autorisé sous la forme de "types donnés A , B et T = [A] , alors [...T, B] construira [A, B] " (ce qui est implicitement proposé) alors il ne serait pas aligné avec l'opérateur de propagation de tableau/tuple. Étant donné var a: [A] et var b: B , le type d'expression [...a, b] ne peut pas être prouvé comme étant de type [A, B] . Selon les règles actuelles des tuples, il pourrait être prouvé qu'il est de type [A, A|B] .
Cela a-t-il du sens pour vous ? Ou je peux créer un tableau de comparaison pour mettre en évidence cette inadéquation.

@Igorbek, je comprends ce que vous dites. Cela vient finalement du fait que le compilateur a une parfaite connaissance des types qu'il traite, mais pas une parfaite connaissance des valeurs. En particulier, dans votre exemple, la valeur a a une longueur inconnue, alors que le type [A] a une longueur connue. C'était l'une des raisons pour lesquelles j'étais initialement mal à l'aise à l'idée d'utiliser des types de tuple à cette fin. Mais je ne suis pas sûr que ce soit un problème grave.

@isiahmeadows Je vois ce que vous demandez, mais pourquoi le problème est-il plus clair avec les paramètres de type repos ? Si vous avez plus d'arguments que d'arguments de type, la même question peut être posée.

La solution de type sûr serait plus cohérente avec le reste de la
langage s'il imitait la syntaxe de l'argument.

Mon point est que si vous diffusez efficacement un paramètre de repos, vous obtenez
exactement les types d'arguments et pas plus. Les fonctions au curry ont un retour
type dépendant du type d'argument. Donc si vous appliquez un argument de trop
pour appliquer partiellement une fonction curry, vous obtiendrez un tout autre
taper. La gestion des types de repos tels que les tuples entraînerait plutôt des erreurs d'exécution,
qui ne sont jamais bons.

Le mardi 1 mars 2016, 06:07 Jason Freeman [email protected] a écrit :

@Igorbek https://github.com/Igorbek Je comprends ce que vous dites.
Cela vient finalement du fait que le compilateur a une parfaite connaissance
des types auxquels il a affaire, mais pas une parfaite connaissance des valeurs. Dans
particulier, dans votre exemple, la valeur a a une longueur inconnue, alors que le
le type [A] a une longueur connue. C'est l'une des raisons pour lesquelles j'ai été initialement
mal à l'aise d'utiliser des types de tuple à cette fin. Mais je ne suis pas sûr
c'est un problème sérieux.

@isiahmeadows https://github.com/isiahmeadows Je vois ce que vous demandez
à propos, mais pourquoi le problème est-il plus clair avec les paramètres de type rest?

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

@isiahmeadows pouvez-vous donner un exemple de code pour le problème de curry ?

Je pense toujours que même si vous utilisiez des paramètres de type rest (pour lesquels je suis tout à fait), vous devriez explicitement décider de ne pas autoriser les arguments en excès, mais je suis d'accord avec @isiahmeadows que vous devriez probablement le faire.

@sandersn @JsonFreeman

type FullCurry<T> = ((initial: T, xs: T[]) => T) | ((initial: T) => (xs: T[]) => T)
declare function foldl<T>(func: (acc: T, item: T) => T, initial: T, xs: T[]): T
declare function foldl<T>(func: (acc: T, item: T) => T): FullCurry<T>
declare function foldl<T>(func: (acc: T, item: T) => T, initial: T): (xs: T[]) => T

interface Function<T, R, ...A> {
    apply<U extends T>(inst: U, args: [...A]): R
    apply(inst: T, args: [...A]): R
}

function apply(reducer: (initial: number) => number): (number[]) => number {
    reducer.apply(undefined, [0, []])
}

const func = apply(foldl<number>((x, y) => x + y))

func([1, 2, 3]) // Runtime error

Je vais ajouter ma variante aussi. Voyons l'exemple de curry variadique de la proposition :

function curry<...T,...U,V>(f: (...ts: [...T, ...U]) => V, ...as:...T): (...bs:...U) => V {
    return ...b => f(...as, ...b);
}

Alors, j'ai commencé à l'utiliser:

function f(a: number, b: string, c: string) { return c.toUpperCase(); }
var a: [number, string] = [1, "boo", 2]; // valid
const cf = curry(f, ...a); // cf is of type string => string
cf("a"); // runtime error

@isiahmeadows Qu'ils soient représentés en tant que paramètres de type rest ou en tant que types de tuple, il semble que vous vous opposez à la possibilité de les répartir dans une position de tuple.

@Igorbek Je pense que votre exemple est similaire en ce sens que le problème n'est pas de savoir comment les séquences de type variadique sont représentées. C'est la capacité de les répartir en tuples qui entraîne des problèmes.

@JsonFreeman C'est plus que je m'oppose à ce comportement :

class A {}
class B {}
class C {}

declare function foo(a: A, b: B): C;

// This should not work
let value: [A, B, C]
foo(...value)

Cela clarifie-t-il ?

@isiahmeadows ça devrait fonctionner en fait

@JsonFreeman
Je pense que ça ne devrait pas. C'est ma plus grande objection. Je pense que c'est potentiellement dangereux si c'est le cas.

Question : quel devrait être le type de retour inféré de ret ?

declare function foo(a: A, b: B, c: C, d: D): D
let ret = foo.bind(...[new A(), new B(), new D()])

C'est en fait assez important.

Ce dernier exemple semble définitivement ne pas fonctionner. Essentiellement, vous avez besoin d'un mécanisme pour aligner les séquences de types si function.bind va vraiment fonctionner correctement. Vous auriez besoin de quelque chose qui s'apparente à l'unification, où les types des arguments à lier sont comparés aux arguments de la fonction d'origine, puis le reste est dans le type de retour.

Cela dit, il ne semble pas que quoi que ce soit dans ce qui a été proposé ou discuté puisse gérer cela (indépendamment du fait que des arguments supplémentaires de tuples soient autorisés), bien qu'il soit possible que j'aie raté quelque chose.

Je pense que le plus gros problème est qu'une sorte de correspondance de motif de tuple, où le type de chaque paramètre est comparé aux types de propagation, doit être effectuée avec les paramètres de type (à la LiveScript/CoffeeScript's arguments) pour résoudre ce problème. C'est probablement impossible autrement. Et pour ce qui est de sa complexité, bonne chance pour la mettre en œuvre. :le sourire:

@JsonFreeman

Ou pour être plus précis, cela nécessitera une vérification de type non stricte (au sens d'avidité vs paresseux) pour fonctionner. Je pense aussi que c'est probablement une extension plus utile que les types variadiques, de toute façon, car elle ouvre à peu près la porte à beaucoup d'autres choses plus utiles, comme les types auto-récursifs.

// I hate this idiom.
interface NestedArray<T> extends Array<Nested<T>> {}
type Nested<T> = T | NestedArray<T>

// I would much prefer this, but it requires non-strict type checking.
type Nested<T> = T | Nested<T>[]

Heureusement, la vérification de type non stricte devrait être un changement purement ininterrompu dans la mesure où seul le code qui auparavant ne parvenait pas à vérifier fonctionne désormais.

C'est probablement la chose la plus importante qui bloque le typage correct de Function.prototype.bind , à part le fait que cela nécessitera une signature de type très complexe.

C'est un lien intéressant. Je ne suis pas convaincu qu'ils soient liés cependant. Le problème des types récursifs est une conséquence de la politique de mise en cache des génériques et de la représentation des alias de type dans le compilateur. Toutes les informations sont là, c'est juste la conception du compilateur qui gêne.

Pour la correspondance de modèle de tuple, vous ne pouvez pas toujours savoir combien d'arguments sont mis en correspondance avec le tuple. Si vous répartissez un tableau dans les arguments de bind , vous ne savez pas combien il en reste dans le rappel résultant.

@JsonFreeman Cela étant dit, pensez-vous qu'en tant qu'étape de l'argument d'adoption, la proposition d'opérateur de propagation #6229 doit être considérée en premier ?

@JsonFreeman

Et une vérification non stricte des types permettrait suffisamment de paresse pour faciliter la résolution de ce problème avec Function.prototype.bind . Avec une telle paresse, vous pouvez accomplir ce type avec ce qui suit (ce qui nécessitera une syntaxe de tuple pour les séquencer, à moins que plusieurs paramètres de repos ne conviennent dans une déclaration de type):

interface Function {
    bind<R, T, ...X, ...Y>(
        this: (this: T, ...args: [...X, ...Y]) => R,
        thisObject: T,
        ...args: [...X]
    ): (this: any, ...rest: [...Y]) => R
}

Pourquoi cela nécessiterait-il une vérification de type non stricte pour déduire? Vous devez déduire le type de repos étape par étape pour vérifier par rapport à la fonction. Compte tenu de ce qui suit, voici comment il devrait vérifier :

// Values
declare function func(a: number, b: string, c: boolean, d?: symbol): number

let f = func.bind(null, 1, "foo")

// How to infer
bind<R, T, ...X, ...Y>(
    this: (this: T, ...args: [...X, ...Y]) => R,
    thisObject: T,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => R

// Infer first type parameter
bind<number, T, ...X, ...Y>(
    this: (this: T, ...args: [...X, ...Y]) => number,
    thisObject: T,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => number

// Infer second type parameter
bind<number, any, ...X, ...Y>(
    this: (this: any, ...args: [...X, ...Y]) => number,
    thisObject: any,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => number

// Infer first part of rest parameter
bind<number, any, number, ...*X, ...Y>(
    this: (this: any, ...args: [number, ...*X, ...Y]) => number,
    thisObject: any,
    ...args: [number, ...*X]
): (this: any, ...rest: [...Y]) => number

// Infer second part of rest parameter
bind<number, any, number, string, ...*X, ...Y>(
    this: (this: any, ...args: [number, string, ...*X, ...Y]) => number,
    thisObject: any,
    ...args: [number, string, ...*X]
): (this: any, ...rest: [...Y]) => number

// First rest parameter ends: all ones that only uses it are fully spread
bind<number, any, number, string, ...Y>(
    this: (this: any, ...args: [number, string, ...Y]) => number,
    thisObject: any,
    ...args: [number, string]
): (this: any, ...rest: [...Y]) => number

// Infer first part of next rest parameter
bind<number, any, number, string, boolean, ...*Y>(
    this: (this: any, ...args: [number, string, boolean, ...*Y]) => number,
    thisObject: any,
    ...args: [number, string]
): (this: any, ...rest: [boolean, ...*Y]) => number

// Infer second part of next rest parameter
// Note that information about optional parameters are retained.
bind<number, any, number, string, boolean, symbol?, ...*Y>(
    this: (
        this: any,
        ...args: [number, string, boolean, symbol?, ...*Y]
    ) => number,
    thisObject: any,
    ...args: [number, string]
): (this: any, ...rest: [boolean, symbol?, ...*Y]) => number

// Second rest parameter ends: all ones that only uses it are exhausted
bind<number, any, number, string, boolean, symbol?>(
    this: (this: any, ...args: [number, string, boolean, symbol?]) => number,
    thisObject: any,
    ...args: [number, string]
): (this: any, ...rest: [boolean, symbol?]) => number

// All rest parameters that are tuples get converted to multiple regular
parameters
bind<number, any, number, string, boolean, symbol?>(
    this: (
        this: any,
        x0: number,
        x1: string,
        x2: boolean,
        x3?: symbol
    ) => number,
    thisObject: any,
    x0: number,
    x1: string
): (this: any, x0: boolean, x1?: symbol) => number

// And this checks

C'est ainsi que fonctionne la vérification de type non stricte. Il déduit les types selon les besoins, au lieu de l'instant où il les voit. Vous pouvez (et devez) combiner les deux passes, afin que les mauvais types échouent. Exemple:

let f = func.bind(null, 1, Symbol("oops"))

// How to infer
bind<R, T, ...X, ...Y>(
    this: (this: T, ...args: [...X, ...Y]) => R,
    thisObject: T,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => R

// Infer first type parameter
bind<number, T, ...X, ...Y>(
    this: (this: T, ...args: [...X, ...Y]) => number,
    thisObject: T,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => number

// Infer second type parameter
bind<number, any, ...X, ...Y>(
    this: (this: any, ...args: [...X, ...Y]) => number,
    thisObject: any,
    ...args: [...X]
): (this: any, ...rest: [...Y]) => number

// Infer first part of rest parameter
bind<number, any, number, ...*X, ...Y>(
    this: (this: any, ...args: [number, ...*X, ...Y]) => number,
    thisObject: any,
    ...args: [number, ...*X]
): (this: any, ...rest: [...Y]) => number

// Infer second part of rest parameter
bind<number, any, number, string, ...*X, ...Y>(
    this: (this: any, ...args: [number, string, ...*X, ...Y]) => number,
    thisObject: any,
    ...args: [number, symbol /* expected string */, ...*X] // fail!
): (this: any, ...rest: [...Y]) => number

Dans ce cas, le paramètre attendu doit être le premier inféré dans ce tour, en effectuant une itération en profondeur d'abord. Dans ce cas, le premier inféré dans cette recherche était une chaîne, et les symboles ne sont pas attribuables aux chaînes, donc cela échoue avec cela.


Et à cause de cela et en essayant de taper Function.prototype.apply , mon opinion sur l'utilisation de tuples pour appliquer des types de repos a changé.

interface Function {
    apply<T, R, ...X>(
        this: (this: T, ...args: [...X]) => R,
        thisArg: T,
        args: [...X]
    ): R
}

Quelques autres remarques :

  1. Il doit y avoir un moyen de répartir les tableaux et les tuples en tant que paramètres de type rest.

ts interface Foo extends Function<void, ...string[]> {}

  1. Il doit y avoir deux types distincts pour les constructeurs et les callables, les fonctions étant l'union des deux. Les objets appelables doivent implémenter l'interface appelable, les constructeurs de classe doivent implémenter l'interface constructible et les fonctions ES5 doivent implémenter l'union des deux.
  2. Function.prototype.bind et vos amis doivent vérifier toutes les surcharges pour la fonction. S'il y en a plusieurs qui fonctionnent, il devrait renvoyer une union de tous.

Ces paramètres de type dans votre exemple ne sont cependant pas vraiment les paramètres de type de la signature de liaison. Ils appartiennent au type Fonction. Mais oui, l'idée est que si vous pouviez utiliser deux paramètres de repos, ou répartir deux paramètres de type repos dans un tuple, vous seriez capable d'écrire ceci.

Pour que la signature de bind soit suffisamment flexible, la limite entre ...X et ...Y doit être décidée par appel. Il faudrait en déduire. Cependant, ce serait un problème si une signature utilisait ...X de manière isolée. Dans ce cas, la frontière n'aura pas été décidée. Par exemple:

interface SomeType<T, R, ...X, ...Y> {
     someMethod(someArgs): [...X]; // No way of knowing how long X is 
}

Et les surcharges sont un problème pour le type Function. Je ne pense pas que vous souhaitiez utiliser le type union par élément sur chaque argument, car cela permettrait de mélanger et de faire correspondre les paramètres entre les surcharges. C'est ce que tu voulais dire ?

@JsonFreeman

_TL;DR : Passer au saut de ligne horizontal. J'ai une nouvelle idée plus pratique._

  1. Oui, je suis conscient qu'ils appartiennent vraiment à Function lui-même.
  2. Ce genre de problème est la raison pour laquelle j'ai dit qu'une correspondance de type non stricte (au sens de Haskell) est nécessaire. Vous ne pouvez pas résoudre le type avec impatience normalement, car celui-ci nécessiterait une recherche itérative et paresseuse. Il est possible de déterminer par algorithme, mais vous devrez garder une trace de choses qui n'auraient normalement pas besoin d'être suivies en C++.
  3. Si les deux arguments sont isolés (comme dans votre exemple), le compilateur devrait se plaindre. Et cette situation peut être détectée avec une analyse de dépendance au niveau du type de chaque argument variadique dans l'interface/whatever. Ce n'est pas non plus trivial, mais cela peut être vérifié lors de la lecture de la déclaration de type elle-même (en fait, peu de temps après).

Bien que je pense aussi qu'il pourrait être un peu plus faisable de définir ce genre de situations uniquement sur la méthode en question. Il sera également beaucoup plus facile et plus rapide de détecter les types de problèmes potentiels auxquels vous avez fait allusion.

interface Function<R, T, ...A> {
    // Split it up for just this method, since it's being resolved relative to the
    // method itself.
    bind[...A = ...X, ...Y](
        this: (this: T, ...args: [...X, ...Y]) => R,
        thisObject: T,
        ...args: [...X]
    ): (this: any, ...rest: [...Y]) => R
}

Il y a un autre problème potentiel qui sera beaucoup plus difficile à résoudre (et pourquoi je pense qu'il devrait être limité à 2, pas à _n _ divisions) :

declare function foo<...T>[...T = ...A, ...B, ...C](
    a: [...A, ...C],
    b: [...A, ...B],
    c: [...B, ...C]
): any

// This should obviously check, but it's non-trivial to figure that out.
let x = foo<
    boolean, number, // ...A
    string, symbol,  // ...B
    Object, any[]  // ...C
>(
    [true, 1, {}, []],
    [true, 1, "hi", Symbol()],
    ["hi", Symbol(), {}, []]
)

_Désolé si je m'enfonce trop dans la théorie de l'informatique ici..._

Oui, je pense que c'est la bonne idée. Ce n'est pas joli, mais je ne vois pas d'autre moyen de taper correctement bind , connaissant les arguments de type de Function . L'ultime est qu'une frontière doit être déduite. Et je suis d'accord qu'il devrait être limité à 2 seaux afin que vous deviez déduire 1 limite, au lieu d'un nombre arbitraire de limites, qui peut exploser de manière combinatoire.

Il y a probablement d'autres problèmes auxquels nous n'avons pas pensé.

@JsonFreeman Un autre problème concerne des choses comme curry . Je n'ai pas encore trouvé quelque chose qui puisse le taper correctement. Et ça va prendre du temps avant que je puisse. Je devrais faire un piratage sérieux de type Haskell pour arriver à un tel processus.

Réfléchir à la façon dont ce type de proposition pourrait fonctionner avec certaines fonctions de Bluebird.

interface PromiseConstructor {
    // all same type
    all<T>(promises: PromiseLike<T>[]):  Promise<T[]>;
    join<T>(...promises: PromiseLike<T>[]):  Promise<T[]>;
    // varying types
    all<...T>(promises: [...PromiseLike<T>]): Promise<[...T]>;
    join<...T>(...promises: [...PromiseLike<T>]): Promise<[...T]>;
    // this is sketchy...    ^
}

interface Promise<T> {
    // all same type
    then<U>(onFulfill: (values: T) => U): Promise<U>;
    spread<U>(onFulfill: (...values: T) => U): Promise<U>;
}
interface Promise<...T> {
    // varying types
    then<U>(onFulfill: (values: [...T]) => U): Promise<U>;
    spread<U>(onFulfill: (...values: [...T]) => U): Promise<U>;
}

Avons-nous une solution pour le all<...T>(promises: [...PromiseLike<T>]): Promise<...T>; ci-dessus ?

@DerFlatulator

Voir mon gros commentaire dans PromiseConstructor. J'ai également corrigé votre interface Promise pour être un peu plus proche de ma proposition.

interface PromiseConstructor {
    new <T>(callback: (
        resolve:
        (thenableOrResult?: T | PromiseLike<T>) => void,
        reject: (error: any) => void
    ) => void): Promise<T, [T]>;
    new <...T>(callback: (
        resolve:
        (thenableOrResult?: [...T] | PromiseLike<[...T]>) => void,
        reject: (error: any) => void
    ) => void): Promise<[...T], ...T>;

    // all same type
    all<T>(promises: PromiseLike<T>[]):  Promise<T[], ...T[]>;
    join<T>(...promises: PromiseLike<T>[]):  Promise<T[], ...T[]>;

    // varying types
    all<...T>(promises: [...PromiseLike<T>]): Promise<[...T], ...T>;
    join<...T>(...promises: [...PromiseLike<T>]): Promise<[...T], ...T>;

    // all<...T>(promises: [...PromiseLike<T>]): Promise<[...T], ...T> should
    // expand to this:
    //
    // all<T1, T2, /* ... */>(promises: [
    //     PromiseLike<T1>,
    //     PromiseLike<T2>,
    //     /* ... */
    // ]): Promise<[T1, T2, /* ... */], T1, T2, /* ... */>;
    //
    // This should hold for all rest parameters, potentially expanding
    // exponentially like ...Promise<[Set<T>], ...Thenable<T>> which should
    // expand to something like this:
    //
    // Promise<[Set<T1>], Thenable<T1>, Thenable<T2> /* ... */>,
    // Promise<[Set<T2>], Thenable<T1>, Thenable<T2> /* ... */>,
    // // etc...
}

interface Promise<T, ...U> {
    // all same type
    then<V>(onFulfill: (values: T) => V): Promise<[V], V>;
    spread<V>(onFulfill: (...values: T) => V): Promise<[V], V>;

    // all same type, returns tuple
    then<...V>(onFulfill: (values: T) => [...V]): Promise<[...V], ...V>;
    spread<...V>(onFulfill: (...values: T) => [...V]): Promise<[...V], ...V>;

    // varying types
    then<V>(onFulfill: (values: [...U]) => V): Promise<[V], V>;
    spread<V>(onFulfill: (...values: [...U]) => V): Promise<[V], V>;

    // varying types, returns tuple
    then<...V>(onFulfill: (values: [...U]) => [...V]): Promise<[V], ...V>;
    spread<...V>(onFulfill: (...values: [...U]) => [...V]): Promise<[V], ...V>;
}

Si [...Foo<T>] développe en [Foo<T1>, Foo<T2>, /*... Foo<TN>*/] , alors [...Foo<T,U>] une erreur de syntaxe ou un développement combinatoire ?

@DerFlatulator

  1. Si exactement l'un des T ou U est un paramètre de repos, il se développe normalement. En supposant que T est un paramètre de repos, alors ce serait [Foo<T1, U>, Foo<T2, U>, /*... Foo<TN, U>*/] .
  2. Si les deux sont des paramètres de repos et que leur longueur peut être correctement déduite, il devrait s'agir d'un développement combinatoire (enfin... la longueur de T fois la longueur de U).
  3. Si les paramètres de repos ne sont pas non plus, il s'agit d'une erreur de syntaxe.

Notez que je m'oppose fortement à plus de 2 paramètres de repos pour des raisons pratiques, et que les paramètres de repos, s'ils doivent être fractionnés, ne devraient être fractionnés que méthode par méthode. Quelque chose comme ça:

interface Function<R, T, ...A> {
    // Split it up for just this method, since it's being resolved relative to the
    // method itself.
    bind[...A = ...X, ...Y](
        this: (this: T, ...args: [...X, ...Y]) => R,
        thisObject: T,
        ...args: [...X]
    ): (this: any, ...rest: [...Y]) => R
}

_(Si quelqu'un peut trouver une meilleure syntaxe, je suis tout ouïe. Je n'aime pas ça, mais je ne peux rien trouver qui n'entre pas en conflit visuel.)_

@isiahmeadows

Avec 2., dans quel ordre serait l'expansion ?

[
Foo<T1, U1>, Foo<T2, U1>, /*... */ Foo<TN,U1>,
Foo<T1, U2>, Foo<T2, U2>, /*... */ Foo<TN,U2>,
/* ... */
Foo<T1, UN>, Foo<T2, UN>, /*... */ Foo<TN,UN>
]

Ou à l'inverse :

[
Foo<T1, U1>, Foo<T1, U2>, /*... */ Foo<T1,UN>,
Foo<T2, U1>, Foo<T2, U2>, /*... */ Foo<T2,UN>,
/* ... */
Foo<TN, U1>, Foo<TN, U2>, /*... */ Foo<TN,UN>
]

Cette ambiguïté ne serait-elle pas source de confusion ? Peut-être que limiter à une dimension serait sage.


Juste une suggestion alternative pour la syntaxe divisée :

interface Function<R, T, ...A> {
    bind<[...X, ...Y] = [...A]>(
        this: (this: T, ...args: [...X, ...Y]) => R,
        thisObject: T,
        ...args: [...X]
    ): (this: any, ...rest: [...Y]) => R
}

@DerFlatulator

Je m'attendrais à la seconde. Et je doute que cela causerait trop de confusion, car tant que c'est systématiquement cela, les gens s'y habitueraient rapidement. C'est aussi un cas limite inhabituel qui ne serait vraiment rencontré en pratique que par des personnes qui savent ce qu'elles font, ou des personnes qui devraient remettre en question le besoin en premier lieu.

Je le regarde également pendant que vous développez le premier, puis le second pour chaque partie du premier. Comme ce pseudo-code :

for (let TT of T) {
  for (let UU of U) {
    expand(TT, UU);
  }
}

Itération sur certaines des idées ci-dessus...

interface Function<TReturn, TThis, ...TArgs> {
    bind<
        [...TBound, ...TUnbound] = [...TArgs],
        TNewThis
    >(
        thisObject: TNewThis,
        ...args: [...TBound]
    ): Function<TReturn, TNewThis, ...TUnbound>
}

Ici, [...TBound, ...TUnbound] = [...TArgs] est valide car la longueur de ...TBound est connue à partir de la longueur de args . Il permet également de changer le type de TThis .

Un problème avec cette approche est que vous ne pouvez lier this qu'une seule fois, par exemple :

interface IFoo { a: number }
interface IBar extends IFoo { b: boolean }
function f(a: number) { }

let x = f.bind(<IBar>{ a: 1, b: false }, 2); // inferred type: Function<number, IBar>
let y = x.bind(<IFoo>{ a: 1 }) // inferred type: Function<number, IFoo>

Le type inféré de y est incorrect, il devrait être Function<number, IBar> . Je ne sais pas si c'est un problème ou non, mais pour le résoudre, il faudrait introduire une logique dans la syntaxe <T> .

Option 1

interface Function<TReturn, TThis, ...TArgs> {
    bind<
        [...TBound, ...TUnbound] = [...TArgs],
        TNewThis = TThis is undefined ? TNewThis : TThis
    >(
        thisObject: TNewThis,
        ...args: [...TBound]
    ): Function<TReturn, TNewThis, ...TUnbound>;
}

Option 2

interface Function<TReturn, TThis, ...TArgs> {
    bind<
        [...TBound, ...TUnbound] = [...TArgs],
        TThis is undefined,
        TNewThis
    >(
        thisObject: TNewThis,
        ...args: [...TBound]
    ): Function<TReturn, TNewThis, ...TUnbound>;

    bind<
        [...TBound, ...TUnbound] = [...TArgs],
        TThis is defined
    >(
        thisObject: any,
        ...args: [...TBound]
    ): Function<TReturn, TThis, ...TUnbound>;
}

Cependant, cela dépasserait probablement le cadre de cette proposition.

Je ne pense pas que nous devrions autoriser ce type d'extensions en utilisant l'opérateur de propagation de type. Je considère l'opérateur de propagation comme un "suppresseur de crochets" qui est absolument aligné avec l'opérateur de propagation de tableau et l'opérateur de propagation d'objets/propriétés (proposition de l'étape 2). Comparez simplement :

let a =        [1, 2];
let b = [0, ...a     , 3];
//      [0, ...[1, 2], 3]
//      [0,     1, 2 , 3]  // removed brackets

let c =               { a: 1, b: "b" };
let d = { e: true, ...c               , f: 3 };
//      { e: true, ...{ a: 1, b: "b" }, f: 3 };
//      { e: true,      a: 1, b: "b"  , f: 3 };

Vous suggérez de l'étendre pour construire un nouvel ensemble de types :

<...T> = <A, B, C>
...U<T> = <U<A>, U<B>, U<C>>

C'est une opération entièrement différente. Si vous voulez, il pourrait être modélisé par des constructions d'ordre supérieur telles que :

<...(from R in T select U<R>)> // linq-like
<...(T[R] -> U<R>)> // ugly

@Igorbek Que diriez-vous d'utiliser un opérateur pour déterminer ce qui sera étendu ?

interface PromiseConstructor {
    all<
      ...T, 
      [...TThen] = ...(PromiseLike<@T> | @T)
    >(
      promises: [...TThen]
    ): Promise<[...T], ...T>;
}

...Foo<<strong i="9">@T</strong>, U> développe en [Foo<T1,U>, /*...*/, Foo<TN,U>] .

...(PromiseLike<@T> | @T) développe en
[PromiseLike<T1>|T1, /*...*/, PromiseLike<TN>|TN]

Quelques alternatives de syntaxe :

  • ...Foo<&T,U>
  • (T) Foo<T,U>
  • (...T => Foo<T,U>)
  • for (T of ...T) Foo<T,U>

Je suis d'accord avec @Igorbek ici. Au moins à ce stade, la cartographie des séquences de types ne semble pas être une priorité, étant donné que nous essayons toujours de résoudre le problème plus fondamental des paramètres de types variadiques.

Je n'ai pas beaucoup de problème à l'interdire (du moins au début), car le comportement pour cela est assez peu intuitif, et deux personnes différentes peuvent même s'attendre à deux choses très différentes. Je suis d'accord avec @Igorbek pour au moins maintenant, car TypeScript doit d'abord avoir un modèle de type d'ordre supérieur (c'est map ping un type dans un sens). Et les types d'ordre supérieur ne sont pas exactement quelque chose que vous pouvez simplement viser.

Donc définitivement :+1: pour avoir interdit ça, probablement pendant un bon bout de temps. Même si c'est agréable à avoir, c'est compliqué à mettre en œuvre et ce serait un hack complet à faire, car TypeScript n'utilise pas de système de type fonctionnel et sécurisé.

J'arrive un peu tard, mais je suis également d'accord avec @Igorbek . Réitérant mon commentaire du #1336 et empruntant des idées à l'empaquetage de paramètres C++ ayant des opérateurs "pack" et "unpack" clairs.

L'emballage des types dans un tuple semble cohérent avec l'utilisation de Typescript de l'opérateur spread :

let [x, y, ...rest] = [1, 2, 3, 4, 5] // pack
foo(...params) // unpack
let all = [1, 2, ...other, 5] // unpack

// keep in mind this is already implemented, which kind of similar to mapping types
function map(arr) { ... }
let spreadingmap = [1, 2, ...map(other), 5];

Ce qui rend <...T_values> = [T1, T2, T3, etc...] beaucoup plus facile à raisonner.

Alors que C++ utilise l'opérateur spread pour le compactage et les points de suspension pour le dépaquetage, l'utilisation de spread pour les deux est plus cohérente avec Typescript.

module Promise {
  function all<...T_values>(   // pack into a tuple of types, conceptually identical to rest parameters
      values: [ (<PromiseLike<T*>> ...T_values) ]  // unpack, cast, then repack to tuple
  ): Promise<T_values> // keep it packed since T_values is a tuple of whatever types
}

@isiahmeadows @JsonFreeman quel serait l'intérêt de tout cela sans cartographie ?

Aussi comme soulevé en #1336, que diriez-vous d'un Array.flatten variadique ?

@jameskeane Cette première moitié était l'idée initiale, mais elle ne couvre pas le cas d'un paramètre de repos intermédiaire (que certaines API ont):

function foo<...T>(a: Foo, b: Bar, ...rest: [...T, Baz]): Foo;

Il ne couvre pas non plus très bien Function.prototype.apply vs Function.prototype.call .

Quant au #1336, il pourrait être implémenté de la même manière via ceci :

angular.module('app').controller(['$scope', function($scope: ng.IScope) { /*etc...*/ }]);

interface IModule {
  controller(injectable: [...string[], () => any]);
}

J'ai rattrapé mon retard et j'ai réalisé que je supposais naïvement que les types de tuple étaient de longueur stricte; quel imo est le plus intuitif. Donc, en supposant que nous obtenions des types de tuples de longueur stricte (#6229), quels sont les problèmes ?

@isiahmeadows Dans votre exemple ci-dessus du cas du paramètre de repos du milieu, n'est-il pas résolu en ayant des tuples de longueur stricte? Je lis ...rest: [...T, Baz] la même manière que le déballage du spread arr = [...other, 123] . C'est le même problème que vous avez soulevé avec curry , n'est-ce pas ?

Quant à apply et call , ne sont-ils pas couverts par des types sécants ? (Ce n'est pas que je vois vraiment l'intérêt d'avoir les types sur l'interface Function toute façon).

// as in
const t: [any, string] & [number, any] = [1, "foo"]

interface Function<R, T, ...A> {
    bind<...Y, ...Z>(
        this: (this: T, ...args: A & [...Y, ...Z]) => R, // tricky bit, luckily intersecting tuples is pretty easy
        thisObject: T,
        ...args: Y
    ): (this: any, ...rest: Z) => R
}

@jameskeane

La proposition variadique actuelle suppose que #6229 est en fait accepté (c'est-à-dire que les tuples sont stricts par défaut).

Quant à func.apply , func.bind , func.call , et _.curry , le seul problème est avec func.bind , _.curry , et amis, ou plus généralement tout ce qui utilise une application partielle. Vous devez également pouvoir choisir le paramètre de repos à séparer, et cela ne peut vraiment être fait que méthode par méthode.

call et apply sont assez simples :

type Callable<R, T, ...A> = (this: T, ...args: [...A]) => R;

interface Function<R, T, ...A> {
    call(this: Callable<R, T, ...A>, thisArg: T, ...args: [...A]): R;
    apply(this: Callable<R, T, ...A>, thisArg: T, args: [...A]): R;
}

bind serait plus difficile. Les paramètres de division devraient être mis en correspondance selon les besoins, contrairement à ce qui est le cas actuellement, jusqu'à ce que la première moitié de division soit complètement décompressée. Cela doit être implémenté en tant que syntaxe, afin que le compilateur puisse le distinguer et discerner correctement le type sans rien évaluer.

// Function.prototype.bind
type Callable<R, T, ...A> = (this: T, ...args: [...A]) => R;
type Constructible<R, ...A> = new (...args: [...A]) => R;

interface Function<R, T, ...A> {
    // my proposed syntax for splitting a rest parameter
    bind[[...A] = [...X, ...Y]](
        this: Callable<R, T, ...A>
        thisArg: T,
        ...args: [...X]
    ): Callable<R, any, ...Y>;

    bind[[...A] = [...X, ...Y]](
        this: Constructible<R, ...A>
        thisArg: T,
        ...args: [...X]
    ): Constructible<R, ...Y>;

    bind[[...A] = [...X, ...Y]](
        this: Callable<R, T, ...A> & Constructible<R, ...A>
        thisArg: T,
        ...args: [...X]
    ): Callable<R, T, ...Y> & Constructible<R, ...Y>;
}

curry serait extrêmement difficile, car il faut savoir que f(1, 2, 3) === f(1, 2)(3) === f(1)(2, 3) === f(1)(2)(3) . Non seulement il doit y avoir la possibilité de diviser un paramètre de repos en deux comme dans bind , mais il doit également y avoir la possibilité d'effectuer une correspondance de modèle très primitive sur une base par méthode.

interface Curried<R, T, ...XS> {
    // none passed
    (): this;

    // all passed
    (this: T, ...args: [...XS]): R;
}

interface CurriedMany<R, T, X, ...YS> extends Curried<R, T, X, ...YS>  {
    // penultimate case, constraint that ...YS contains no parameters
    [[...YS] = []](arg: X): Curried<R, T, X>;

    // otherwise, split rest into ...AS and ...BS, with `A` used as the pivot
    // (basically, default case)
    [[...YS] = [...AS, A, ...BS]](
        ...args: [X, ...AS]
    ): CurriedMany<R, T, A, ...BS>;
}

function curry<R, T>(f: (this: T) => R): (this: T) => R;
function curry<R, T, X>(f: (this: T, arg: X) => R): Curried<R, T, A>;
function curry<R, T, X, ...YS>(
    f: (this: T, arg: X, ...args: [...YS]) => R
): CurriedMany<R, T, X, ...YS>;

Je ne crois pas que les ajouts pour curry rendraient Turing-complet, mais ce serait proche. Je pense que la principale chose qui l'empêche serait la possibilité de faire correspondre les spécialisations d'un type particulier (que C++, Scala et Haskell, trois langages avec des systèmes de types complets de Turing, ont tous).

@sandersn Je n'ai pas pu voir d'exemple ci-dessus, mais puis-je poser des questions sur les contraintes sur les paramètres variadiques?

Considérez l'exemple suivant :

interface HasKey<T> {
    Key(): T;
}

class Row<...T extends HasKey<X>, X> {
    // ...
}

_Par ailleurs, voir https://github.com/Microsoft/TypeScript/issues/7848 pour une discussion sur la possibilité de supprimer l'exigence selon laquelle X doit être répertorié_

Maintenant, il y a potentiellement une certaine ambiguïté ici quant à savoir si la contrainte est :

  1. (...T) extends HasKey<X> ou
  2. ...(T extends HasKey<X>)

Dans cet exemple, je suppose 2.

Ces sortes de contraintes (1 et/ou 2) seront-elles possibles ?

@myitcv 2 serait probablement la meilleure voie à suivre, mais il serait logique de simplement réutiliser la logique existante pour vérifier la contrainte.

Eh bien... je viens de réaliser quelque chose : comment seraient les tableaux contenant des types variadiques ? Ou plus précisément, quel type est arg ci-dessous ?

function processItems<...T>(...args: [...T]): void {
    for (const arg of args) { // Here
        process(arg);
    }
}

Je suppose que vous demandez quel est le type d'élément de args . Pour les tuples, je crois que c'est généralement le type d'union des éléments. Je ne peux pas penser à une meilleure façon de taper cela.

@sandersn pouvez-vous commenter quel est le statut de cette fonctionnalité ? J'ai l'impression qu'il y a eu beaucoup de discussions, mais il ne semble pas qu'il y ait de plan défini pour la fonctionnalité, n'est-ce pas ?

@JsonFreeman Je demandais précisément ce qu'était arg . À mon avis, cela devrait être any pour mon exemple d'origine, et Item<T> avec ci-dessous (avec le T borné par F) :

function processItems<...T extends Item<T>>(...args: [...T]): void {
    for (const arg of args) { // Here
        process(arg);
    }
}

C'est ainsi que les types peuvent être résolus localement. Vous ne connaissez pas les types à l'avance, et cela accélérera considérablement la compilation, puisque vous n'avez pas à calculer les types dans la fonction pour chaque appel. Notez que si vous n'avez besoin que du type d'un seul argument, typeof arg suffira et sera probablement plus court.

Oh désolé, pour l'exemple original, je voulais dire que le type devrait être T . En fait, pour votre deuxième exemple, je pense aussi que cela devrait être T.

Je voulais dire Item<any> dans la seconde... Désolé.

Quand j'ai dit que cela devrait être T, je supposais que T est un type, mais je suppose que tout l'intérêt de cette fonctionnalité est que T n'est pas un type (je pense). Alors oui, je suppose que cela devrait être any et Item<any> dans vos exemples.

Mais plus généralement, je suis curieux de savoir à quel point l'équipe envisage activement cette fonctionnalité pour l'après 2.0. Je n'ai pas d'opinion tranchée, je me demande juste.

La raison pour laquelle je ne pense pas que cela devrait nécessairement être T c'est parce que vous ne savez pas ce qu'est T . À moins, bien sûr, que vous vouliez que la variable T représente soit un type unique de la liste de types variadiques, soit lorsqu'elle est étendue, la liste elle-même, c'est- T dire que ...T , et [...T] est assignable à T[] .

Ou, pour clarifier ce que je veux dire dans tout le jargon peu clair, voici ce que je veux dire en termes de code :

// To put it into code
function foo<...T>(list: [...T]): void {
    // This is allowed
    let xs: T[] = list

    // This is allowed
    let list2: [...T] = list

    // This is not allowed
    let list1: [...T] = xs

    // This is allowed
    let item: ?T = null

    // This is not allowed, since it's not immediately initialized
    let other: T

    for (let arg of args) {
        // This is allowed
        let alias: T = arg

        // This is allowed
        let other: ?T = arg

        // This is allowed, since `item` is defined upwards as `?T`
        item = arg

        // This is allowed, since you're doing an unsafe cast from `?T` to `T`.
        alias = item as T
    }
}

Cela aurait probablement plus de sens, cependant, et ce serait beaucoup plus flexible.

C'est toujours sur notre liste de choses à avoir, mais il intéresse principalement les auteurs de bibliothèques et a une solution de contournement décente - _n_ surcharges - donc je ne travaille pas activement dessus. Si je devais deviner, je dirais que 2.1 est possible mais peu probable.

Si/quand nous nous engageons à prendre en charge correctement le repos/la propagation des objets (#2103), les types variadiques pourraient être suffisamment proches pour diffuser les types pour justifier de les faire tous en même temps. (Les types de propagation sont une variante des types d'objet qui ressemblent à { ...T, x: number, ...U, y: string, ...V } .)

Je veux juste mentionner que la solution n overloads contournement

@sandersn Une pull request pour "_n_ surcharges" serait-elle invitée pour bind , apply et call dans les fonctions, en utilisant la saisie this ? Je pense que ce serait un compromis temporaire acceptable pour beaucoup, et pourrait attraper pas mal de bugs dans le processus pour certains projets.

@isiahmeadows

La raison pour laquelle je ne pense pas que cela devrait nécessairement être T, c'est parce que vous ne savez pas ce qu'est T.

Il m'a semblé qu'il y avait accord que T est un type tuple des types variadiques. Dans votre exemple d'origine, le type de arg serait le même que le type d'élément tuple (comme @JsonFreeman l'a noté, "le type d'union des éléments") : pour l'instant, imaginez que le typescript prend en charge l'utilisation d'un tuple comme repos tapez (#5331).

function processItems<...T>(...args: T): void {
  for (const arg of args) { // Here - arg:number|string|boolean
    const other: ??? = arg; // I think the issue is, how to _represent_ this type?
  }
}
processItems(1, 'foo', false); // T is tuple [number, string, boolean]

Indépendamment de cette proposition, je pense qu'il devrait y avoir un moyen de représenter le "type d'élément" d'un tuple. Cela pourrait être une autre utilisation pour la propagation, c'est-à-dire comme ci-dessus ...T :: number|string|boolean ; que la diffusion du type de tuple donne son type d'élément.

for (const arg of args) {
  const cst: ...T = arg;
}

// also, even without variadic types...
type Record = [number, string];
function foo(args: Record) {
  for (const arg in args) {
    const cst: ...Record = arg;
  }
}

Dans cet esprit, vos autres exemples :

function foo<...T>(...list: T): void {
  let xs: T[] = [list, list] // array of the variadic tuple type

  // This is allowed
  let list5: (...T)[] = [...list]

  // This is *not* allowed
  let list2: [...T] = list

  // This is not allowed
  let list1: [...T] = xs

  // This **is** allowed
  // single element tuple, of variadic union
  // i.e. with number|string|boolean
  //      list4 = [1] or list4 = ['foo'] or list4 = [false]
  let list4: [...T] = [list[n]]

  // This **is**  allowed
  let other: T;

  // This is allowed
  let another: ...T;

  for (let arg of args) {
    another = arg; // allowed, if spreading the tuple is the union type

  }
}

Ne perdant pas de vue mon objectif initial, je voulais un Promise.all fortement typé...

declare module Promise {
  function all<...T>(promises: Promise<...T>[]): T; // means promises is an array of promises to the union type, not what I wanted.

  // Then we need something like, which is now very confusing
  function all<...T>(promises: [...Promise<T*>]): T; 
}}

@sandersn Maintenant que d'autres fonctionnalités demandées commencent à en dépendre, la priorité pourrait-elle être augmentée ? bind , call etc. la saisie dépend de cela, et la syntaxe de liaison ES si/quand elle sort en dépend, donc maintenant il y a plus de choses à faire que des auteurs de bibliothèques excentriques qui vous harcèlent tout le temps . :)

Non pas que ce soit particulièrement constructif à ajouter, mais je serais tellement heureux si ces deux fonctionnalités étaient intégrées à 2.1. Je connais au moins une bibliothèque (RxJS), où non seulement la base de code elle-même serait améliorée par ces fonctionnalités, mais la consommation de code serait également beaucoup moins gênante et sujette aux bogues (une personne sur trois débutant avec Angular 2 se fait mordre par des importations manquantes pour les opérateurs qui sont patchés dans le prototype observable). Ce serait vraiment une fonctionnalité révolutionnaire pour les personnes cherchant à écrire du code fonctionnel maintenable.

Cela pourrait-il être utilisé pour fournir une définition de type complète pour _.extend , dont le type de retour est l'intersection de tous ses paramètres ?

declare module underscore {
  function extend<A, B, C, D, ...>(a: A, b: B, c: C, d: D, ...): A&B&C&D&...;
}

Pas en l'état. Il a besoin d'une extension de la proposition qui donne des détails sur un nouvel opérateur pour les types variadiques — probablement appelé &. @kitsonk a proposé cet opérateur plus tôt, dans ce commentaire .
À l'heure actuelle, cette fonctionnalité est en dessous de quelques autres choses plus immédiatement importantes, donc je n'ai pas examiné cette proposition depuis un moment.

Bien qu'il ne fournisse pas de types variadiques complets, #10727 fait partie de la solution (et est susceptible de relever les défis que nous (@dojo) avons).

Bon à entendre ! Bien qu'il ne s'agisse toujours pas de types variadiques. :( Par exemple, cette semaine, lorsque j'ai essayé de taper Object.assign , je suis arrivé jusqu'ici :

interface Object {
  // binary version
  assign<T,U>(target: T, source: U): { ...T, ...U };
  // variadic version: bind a variadic kind variable ...T
  // and then spread it using SIX dots
  assign<...T>(...targets: ...T): { ......T };
}

Notez que la syntaxe "six points" est la propagation d'objets d'une variable de type tuple, dont nous n'avons pas vraiment discuté ci-dessus.

@sandersn

Avec Object.assign en particulier, il pourrait être tapé de cette façon, et techniquement capturer un sous-ensemble (bien qu'un peu trop faiblement inférablement faible), puisqu'il mute sa cible (il faudrait un point de référence pour cela) :

assign<T>(target: T, ...sources: Partial<T>[]): T;

Le problème avec cela est qu'il mute sa cible, changeant son type structurel en place.

@isiahmeadows alors l'inférence fixerait T pour être le type de target sans les types comptables de sources . Vous pouvez l'essayer maintenant avec la version non variadique :

declare function _assign<T>(target: T, source: Partial<T>): T;
_assign({}, { a: 10 }); // T is {}

Comme déjà mentionné, assign utilise _a spread type_ #10727 et peut être défini de cette façon :

// non variadic
declare const assign: {
  <T>(target: T): T;
  <T, S>(target: T, source: S): {...T, ...S};
  <T, S1, S2>(target: T, source1: S1, source2: S2): {...T, ...S1, ...S2};
};
// variadic
declare function assign<T, [...S]>(target: T, ...sources: [...S]): {...T, ...[...S]};

_Remarque : j'insiste toujours sur la syntaxe basée sur les tuples [...T] qui a beaucoup plus de sens pour moi._

@sandersn BTW, y a-t-il une mise à jour sur le moment où les types variadiques seront débarqués? Y a-t-il une chance de le voir en 2.2 ?
Et, concernant la syntaxe, acceptez-vous toujours les commentaires sur la syntaxe ou êtes-vous tous d'accord avec cela ?

La syntaxe et la sémantique de bas niveau n'ont pas encore de consensus clair.

Le mardi 13 décembre 2016, 13:26 Igor Oleinikov [email protected] a écrit :

@sandersn https://github.com/sandersn BTW, est une mise à jour sur quand
des types variadiques vont être débarqués? Y a-t-il une chance de le voir en 2.2 ?
Et, concernant la syntaxe, acceptez-vous toujours les commentaires sur la syntaxe ou
vous êtes tous d'accord avec ça ?

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-266819647 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AERrBIa5fE8PSk-33w3ToFqHD9MCFoRWks5rHuM5gaJpZM4GYYfH
.

Une idée de l'état d'avancement de ce problème ?

alors quelles sont les options auxquelles vous pensez? est-ce à l'ordre du jour de l'équipe ? c'est la seule partie faible du système de types que je rencontre à plusieurs reprises. J'ai deux cas d'utilisation. un simple et un complexe, mais plus général.

simple serait d'ajouter un supertype Tuple extends any[] qui ne peut être sous-typé que par des types de tuple. Comme les spreads doivent être des sous-types de any[] , cela fonctionnerait :

declare interface Plugin<A: Tuple, P> {
  (...args: A): P | Promise<P>
}

const p: Plugin<[string, { verbose: boolean }], int> =
  (dest, { verbose = false }) => 4

pour le moment, ...args: T[] n'est autorisé qu'à la fin des signatures.

le cas d'utilisation complexe aurait besoin que ...args: Tuple soit légal n'importe où dans une signature (ce qui ne pose aucun problème, car les tuples sont de longueur fixe) :

/**
 * Takes a function with callback and transforms it into one returning a promise
 * f(...args, cb: (err, ...data) => void) => void
 * becomes
 * g(...args) => Promise<[...data]>
 */
function promisify<A extends Tuple, D extends Tuple, E>
    (wrapped: (...args: A, cb: (error: E, ...data: D) => void) => void)
    : ((...args: A) => Promise<Data>) {
  return (...args) => new Promise((resolve, reject) =>
    wrapped(...args, (e, ...data) =>
      e ? reject(e) : resolve(data)))
}

const write: ((fd: number, string: string, position?: number, encoding?: string)
              => Promise<[number, string]>) =
  promisify(fs.write)

Ouais, je viens juste de commencer avec TypeScript hier et cela a déjà rendu impossible la saisie automatique de mes fonctions (je peux toujours le faire manuellement bien sûr) à cause d'un seul décorateur que j'utilise pour envelopper mes fonctions (ce qui est la première chose J'ai essayé de commencer par !) :

function portable(func) {
    return function(...args) {
        if (this === undefined) {
            return func(...args)
        } else {
            return func(this, ...args)
        }
    }
}

En fait, tout ce que fait le décorateur est de permettre à une fonction d'être appelée en tant que méthode afin qu'elle puisse être attachée en tant que méthode et fonctionner de manière identique, comme exemple de base voici un mauvais exemple qui corrige le prototype Array avec une version basique de flatMap :

function _flatMap<T, R>(
    array: T[],
    iteratee: (item: T) => R[]
): R[] {
    let result: R[] = []
    for (const item of array) {
        for (const value of iteratee(item)) {
            result.push(value)
        }
    }
    return result
}

const flatMap = portable(_flatMap)
Array.prototype.flatMap = flatMap

flatMap([1,2,3,4], x => [x, x])
// Is the same as
[1,2,3,4].flatMap(x => [x, x])
// Is the same as
flatMap.apply([1,2,3,4], [x => [x, x]])
// Is the same as
flatMap.call([1,2,3,4], x => [x, x])

Maintenant, espérons-le, il est évident que le type de flatMap (pas _flatMap ) est :

function flatMap<T, R>(this: T[], iteratee: (item: T) => R[]): R[]
function flatMap<T, R>(this: undefined, array: T[], iteratee: (item: T) => R[]): R[]

Cependant, je n'ai aucun moyen d'ajouter types à portable car je ne peux pas extraire le type de paramètres de _flatMap pour ensuite l'utiliser dans la définition de type de la fonction décorée, j'imagine qu'avec cette proposition, je pourrais écrivez quelque chose comme :

// First argument to func is required for portable to even make sense
function portable<T, R, ...Params>(func: (first: T, ...rest: Params) => R) {
    // The arguments of calling with this is undefined should be simply
    // exactly the same as the input function
    function result(this: undefined, first: T, ...rest: Params): R
    // However when this is of the type of the first argument then the type
    // should be that the parameters are simply the type of the remaining
    // arguments
    function result(this: T, ...rest: Params): R
    function result(...args) {
        if (this === undefined) {
            return func(...args)
        } else {
            return func(this, ...args)
        }
    }
    return result
}

Je voulais juste partager ceci car cela montre mes premières expériences avec TypeScript et montre peut-être un autre cas de l'importance des génériques variadiques.

@sandersn :

il a une solution de contournement décente - n surcharges

Bien que techniquement pas incorrect, je pense que cela ne reflète pas pleinement la réalité ici. Ouais, techniquement, l'absence de cela n'a pas empêché les surcharges de taper n'importe quelle fonction mentionnée dans ce fil; et pourtant, cet inconvénient mineur a signifié qu'aucune de ces solutions basées sur la surcharge n'a réussi jusqu'à présent dans lib.d.ts .

Et en fait, beaucoup dans ce fil se sont sentis suffisamment désespérés pour s'attaquer à leurs fonctions respectives pour proposer encore plus de syntaxe qui ne faisait pas partie à l'origine de cette proposition, y compris votre ...... , ainsi que ...*X , [...T = ...A, ...B, ...C] , [...PromiseLike<T>] , <[...X, ...Y] = [...A]> et <PromiseLike<T*>> .

Je pense que cela montre que nous essayons tous de résoudre les problèmes ici, nous partageons le sentiment général que nous avons besoin d'une syntaxe plus puissante comme celle-ci, et nous espérons que la route que nous choisirons ici nous aidera à les résoudre.

Note latérale : pour les R.path de Ramda, nous avions généré un typage de milliers de lignes de surcharges, qui manquaient toujours la prise en charge des tuples (les permutations auraient encore explosé beaucoup plus), et la compilation sur de vrais projets ne s'arrêtait pas plus. L'itération a récemment été découverte comme une alternative apparemment viable là-bas (#12290).

Soit dit en passant, à ma connaissance, vous n'aviez pas encore commenté la proposition présentée par @Artazor et @Igorbek. Quelles ont été vos pensées à ce sujet ?

J'aimerais discuter d'une implémentation de base comme celle-ci ici (plus #6606), nous pouvons faire à peu près n'importe quoi. Je vais proposer quelques solutions ici pour illustrer cela, mais je suis ouvert à d'autres questions.

Permettez-moi d'abord de passer en revue certains endroits où un opérateur ... pourrait être implémenté :

v ... pour | définition (capturer) | utiliser (diffuser)
-|-|-
fonction | type Fn = (...args: any[]) => {} | type Returns = typeof fn(...MyTuple); (#6606)
tableau | déstructuration des tuples au niveau du type. peut techniquement être émulé en utilisant index access + spread (voir à droite) + récursivité. | type Arr = [Head, ...Tail];
objet | déstructuration des objets au niveau du type. pas nécessaire, utilisez simplement Omit , voir #12215. | type Obj = { a: a, ...restObj }; (pas nécessaire, comme Overwrite , voir #12215)
génériques | définir type Foo<...T> pour faire Foo<1, 2, 3> (capture [1, 2, 3 ] dans T ). amusant, mais je ne sais pas quel cas d'utilisation nécessite cela. | définir type Bar<A,B,C> à faire Bar<...[1,2,3]> ( A = 1 etc.). idem, je ne connais pas de cas d'utilisation qui en ont besoin .
syndicats (prime) | ? | type Union = "a" | "b"; type MyTuple = ...Union; // ["a", "b"] (l'ordre n'est pas fiable mais permet l'itération d'unions/objets à travers des tuples. de toute façon, c'est hors de portée ici.)

Il n'y a donc que deux instances de niveau type ... qui sont immédiatement pertinentes ; plus précisément, les deux utilisés ici :

declare function f<U, T>(head: U, ...tail: T): [U, ...T];

Dans le contexte de #6606, un autre devient pertinent : la possibilité de décompresser un type de tuple pour une application de fonction, par exemple typeof f(...MyTuple) . Je pense que cela suffit pour résoudre les problèmes les plus difficiles dont j'ai entendu parler ici. Pour essayer de proposer quelques solutions ici :

@jameskeane :

Je pense qu'il devrait y avoir un moyen de représenter le "type d'élément" d'un tuple

Si vous souhaitez obtenir l'union de leurs éléments, consultez mon TupleToUnion .

Promise.all

// helpers: `mapTuple` needs #5453 to define, #6606 to use
type TupleHasIndex<Arr extends any[], I extends number> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
type Inc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // longer version in gist
declare function mapTuple<F extends (v: T) => any, Tpl extends T[], T>(f: F, tpl: Tpl): MapFn<F, Tpl, T>;
type MapFn<
    F extends (v: T) => any,
    Tpl extends T[],
    T,
    // if empty tuple allowed:
    // I extends number = 0,
    // Acc = []
    // otherwise:
    I extends number = 1,
    Acc = [F(Tpl[0])]
> = { 1: MapFn<F, Tpl, T, Inc[I], [...Acc, F(Tpl[I])]>; 0: Acc; }[TupleHasIndex<Tpl, Int>];

declare module Promise {
  function all<Promises extends Promise<any>[]>(promises: Promises): typeof mapTuple(<T>(prom: Promise<T>) => T, Promises);
}

@danvk :

_.extend

@sandersn :

Object.assign

Ce ne sont que des versions variadiques du mergeAll de Ramda. Pas besoin de six points !

@isiahmeadows :

Une idée de l'état d'avancement de ce problème ?
La syntaxe et la sémantique de bas niveau n'ont pas encore de consensus clair.

Si je comprends bien, vous vous inquiétiez principalement de savoir si l'approche proposée par quelques-uns des autres prendrait en compte les frappes plus difficiles telles que les curry et les bind vous avez mentionnés. Voici mon point de vue sur celui-ci, suite à leur proposition.
La stratégie est un peu similaire, trichant sur le fait qu'il est difficile de dire, extraire les exigences de type pour les paramètres i~j d'un type de fonction dans un type de tuple, en reportant la vérification de type des arguments à l'application de la fonction.

// helpers in https://gist.github.com/tycho01/be27a32573339ead953a07010ed3b824, too many to include

// poor man's version, using a given return value rather than using `typeof` based on the given argument types:
function curry<Args extends any[], Ret>(fn: (...args: Args) => Ret): Curried<Args, Ret>;
type Curried<
  ArgsAsked,
  Ret,
  ArgsPrevious = [] // if we can't have empty tuple I guess any[] might also destructures to nothing; that might do.
> = <
  ArgsGiven extends any[] = ArgsGiven,
  ArgsAll extends [...ArgsPrevious, ...ArgsGiven]
      = [...ArgsPrevious, ...ArgsGiven]
  >(...args: ArgsGiven) =>
    If<
      TupleHasIndex<ArgsAll, TupleLastIndex<ArgsAsked>>,
      Ret,
      Curried<ArgsAsked, Ret, ArgsAll>
    >;

// robust alternative that takes into account return values dependent on input params, also needs #6606
function curry<F>(fn: F): Curried<F>;
type Curried<
  F extends (...args: ArgsAsked) => any,
  ArgsAsked extends any[] = ArgsAsked,
  ArgsPrevious = []
> = <
  ArgsGiven extends any[] = ArgsGiven,
  ArgsAll extends [...ArgsPrevious, ...ArgsGiven]
      = [...ArgsPrevious, ...ArgsGiven]
  >(...args: ArgsGiven) =>
    If<
      TupleHasIndex<ArgsAll, TupleLastIndex<ArgsAsked>>,
      F(...[...ArgsPrevious, ...ArgsGiven]), // #6606
      Curried<ArgsAsked, Ret, ArgsAll>
    >;

// bind:
interface Function {
    bind<
        F extends (this: T, ...args: ArgsAsked) => R,
        ArgsAsked extends any[],
        R extends any,
        T,
        Args extends any[], // tie to ArgsAsked
        Left extends any[] = DifferenceTuples<ArgsAsked, Args>,
        EnsureArgsMatchAsked extends 0 = ((v: Args) => 0)(TupleFrom<ArgsAsked, TupleLength<Args>>)
        // ^ workaround to ensure we can tie `Args` to both the actual input params as well as to the desired params. it'd throw if the condition is not met.
    >(
        this: F,
        thisObject: T,
        ...args: Args
    ): (this: any, ...rest: Left) => R;
    // ^ `R` alt. to calc return type based on input (needs #6606): `F(this: T, ...[...Args, ...Left])`
}

Ouais, j'ai utilisé un tas de types d'aides - j'essayais juste de faire avec ce que nous avons (+ imaginez ce que nous pourrions faire avec juste un peu plus). Je ne suis pas tellement contre ...... , ...*X , [...T = ...A, ...B, ...C] , [...PromiseLike<T>] , <[...X, ...Y] = [...A]> , ou <PromiseLike<T*>> . Mais l'OMI, même seulement ... aide à résoudre un vrai problème en ce moment, et j'aimerais qu'il soit résolu.

Edit : j'ai résolu la contrainte d'argument pour bind .

Juste une question probablement stupide. Cela semble très prometteur pour pouvoir taper correctement les fonctions curry.
Mais pour certains projets du monde réel, on ne pouvait pas vouloir passer beaucoup de temps à taper un morceau de code fortement orienté programmation fonctionnelle.
Donc, je me demande, puisque --strict est activé par défaut dans _tsconfig.json_, s'il pourrait y avoir un moyen de désactiver la vérification de type pour une partie du code (en raison de la paresse ou du manque de temps).
Mais, comme je l'ai dit, c'est probablement une question stupide... ^_^

@yahiko00 un peu hors sujet, mais utilisez la section exclude dans tsconfig ou différents tsconfig s à différents niveaux de projet.

Je voudrais également faire une autre suggestion, pourrions-nous l'avoir pour que les & et | fonctionnent avec un seul argument de tuple avec cette syntaxe :

<...T>(...args:T): ...T&
// is the same as 
<t1, t2, t3>(...args:[t1, t2, t3]): t1 & t2 & t3;
// and
<....T>(...args:T): ...T|
// is the same as 
<t1, t2, t3>(...args:[t1, t2, t3]): t1 | t2 | t3;

La suggestion ci-dessus de @HyphnKnight serait très utile pour quelque chose que je fais aussi.

Je souhaite ajouter une clause de non-responsabilité indiquant que cette proposition n'est pas activement travaillée. Mais j'ai trouvé exactement le type d'article "d'art antérieur" que je voulais lire lorsque j'ai commencé à étudier ce problème : http://www.ccs.neu.edu/racket/pubs/esop09-sthf.pdf

Je vais laisser ça ici pour référence future.

J'ai ouvert quelques PR pour expérimenter ceci :

  • [ ] #17884 se propage dans les types de tuple (WIP)
  • [x] #17898 extraire les paramètres de repos (prêt)
  • [ ] #18007 se propage en appel de type (WIP)
const c = 'a' + 'b';

Peut résoudre le problème? Déduire le type de c est 'ab' non string

Une question connexe sur StackOverflow : dernier paramètre de fonction explicite dans TypeScript

@sandersn Votre proposition couvrirait ce cas, pour autant que je

Plus de 2 ans après la proposition initiale, faut-il encore garder espoir ?

Salut!
J'essaie de taper un générateur qui prend un nombre variable de tableaux et en mélange et associe des éléments pour créer un nouveau tableau.
Je souhaite utiliser ce générateur dans une boucle for...of , mais je n'arrive pas à saisir correctement les valeurs.
Code (il peut y avoir des erreurs puisque je ne l'ai pas encore exécuté, mais c'est ce que j'essaie de faire):

function* CombineEveryArgumentWithEveryArgument(...args: any[][]) {
    if (args.length < 1) {
        return [];
    }
    var haselements = false;
    for (var arg of args) {
        if (arg && arg.length > 0) {
            haselements;
        }
    }
    if (!haselements) {
        return [];
    }
    var indexes = [];
    for (var i = 0; i < args.length; i++) {
        indexes.push(0);
    }
    while (true) {
        var values = [];
        //One item from every argument.
        for (var i = 0; i < args.length; i++) {
            values.push(args[i][indexes[i]]);
        }
        if (indexes[0] + 1 < args[0].length) {
            yield values;
        }
        else {
            return values;
        }
        //Increment starting from the last, until we get to the first.
        for (var i = args.length; i > 0; --i) {
            if (indexes[i]++ >= args[i].length) {
                indexes[i] = 0;
            }
            else {
                break;
            }
        }
    }
}

Exemple d'utilisation :

for (let [target, child] of
    CombineEveryArgumentWithEveryArgument(targetsarray, childrenarray)) {

Je ne trouve aucun moyen de taper pour la cible et l'enfant sans créer une variable intermédiaire.

Quelque chose comme ça serait bien ?

function * generator<...T[]>(...args: T[]): [...T]

@Griffork la bonne pratique, jusqu'à ce que cette proposition soit mise en œuvre, consiste à créer de nombreuses surcharges pour les fonctions
par exemple, voir les types promise.all
https://github.com/Microsoft/TypeScript/blob/master/lib/lib.es2015.promise.d.ts#L41 -L113

Je trouve cette syntaxe très confuse :

function apply<...T,U>(ap: (...args:...T) => U, args: ...T): U {

cela me semble beaucoup plus naturel:

function apply<T, U>(ap: (...args: T) => U, args: T): U {

Au moment de l'exécution, un paramètre de repos est un tableau, et nous pouvons le faire actuellement dans TS :

function apply<T, U>(ap: (...args: T[]) => U, args: T[]): U {

Il semble donc logique de simplement supprimer la restriction de args étant un tableau de T et de permettre au compilateur TS de déduire un type de tuple pour T , par exemple

function apply(ap: (...args: [number, number]) => number, args: [number, number]): number {

J'ai vu qu'il y avait des inquiétudes concernant les tuples et je ne les comprends pas tous complètement, mais je voulais juste peser que dans la proposition actuelle, il est difficile de comprendre quand un développeur aurait besoin d'utiliser ... dans une position de type et les tuples sont beaucoup plus intuitifs.

... toujours du sens pour moi pour concaténer deux types de tuples, comme [...T, ...U] .

@felixfbecker
La proposition de

function apply<...T,U>(ap: (...args:T) => U, ...args: T): U {

Serait-ce que T est un type de tuple créé dynamiquement, donc si vous passez un string et un int dans la fonction, alors T est [string, int] .
Ceci est particulièrement intéressant si vous souhaitez exprimer dynamiquement un motif comme celui-ci :

function PickArguments<T>(a: T[]): [T];
function PickArguments<T, U>(a: T[], b: U[]): [T, U];
function PickArguments<T, U, V>(a: T[], b: U[], c: V[]): [T, U, V];
//More overloads for increasing numbers of parameters.

//usage:
var [a, b, c] = PickArguments(["first", "second", "third"], [1, 2, 3], [new Date()]);
var d = b + 1; //b and d are numbers.
var e = c.toDateString(); //c is a date (autocompletes and everything), e is a string.

Pour le moment, si vous voulez écrire une fonction qui prend un nombre variable d'arguments et les tapez de manière générique, vous devez écrire une surcharge générique pour chaque nombre d'arguments que cette fonction pourrait éventuellement être donnée. La proposition ...T nous permet essentiellement de faire en sorte que le compilateur génère automatiquement les définitions de fonction pour nous.

Ta proposition:

function apply<T, U>(ap: (...args: T) => U, args: T): U {

Force tous les paramètres à être traités comme le même type, et ils ne peuvent pas avoir de vérification de type plus spécifique. Par exemple, dans mon exemple ci-dessus, toutes les valeurs renvoyées seraient de type any .

Je trouve aussi que le supplément ... est très difficile à lire.
Comme l'idée de @felixfbecker , je ne vois pas la nécessité de faire :

function apply<...T, U>(ap: (...args: ...T) => U, args: ...T): U {...}

La première chose qui vient à l'esprit lors de la lecture de apply<...T, est qu'il s'agit d'un opérateur de propagation, mais qu'en réalité, il ne fait pas de propagation du tout.

@Griffork , dans votre exemple T serait toujours [string, int] .
C'est ce que @felixfbecker veut dire par "au lieu de permettre au compilateur TS de déduire un type de tuple pour T", du moins de la façon dont je le comprends.

Force tous les paramètres à être traités comme le même type, et ils ne peuvent pas avoir de vérification de type plus spécifique. Par exemple, dans mon exemple ci-dessus, toutes les valeurs renvoyées seraient de type any.

@Griffork Non, dans mon esprit, cela déduirait un type de tuple pour le tableau args , donnant à chaque paramètre son propre type par sa position dans le tuple. ...args: T[] les forcerait tous à être du même type T , mais ...args: T (qui est actuellement une erreur de compilation) déduirait un type de tuple pour T .

La première chose qui vient à l'esprit lors de la lecture de apply<...T, c'est qu'il s'agit d'un opérateur de propagation, mais qu'en réalité, il ne fait pas du tout de propagation.

@unional d' accord, c'est exactement de là que vient la confusion.

@union
Je l'ai lu comme l'opérateur de propagation aussi, je l'ai lu comme "propager ce type à chaque fois qu'il est utilisé".
Pour moi, en lisant ça

function apply<T, U>(ap: (...args: T) => U, args: T): U {

Je m'attendrais à ce que T soit un tableau de quelque chose (par exemple string[] ).

Et en lisant ceci :

function apply<T, U>(ap: (...args: T[]) => U, args: T[]): U {

Je m'attendrais à ce que tous les arguments soient assignables au type T (qui est un type, tel que string ).

Le but de la proposition ci-dessus était d'éviter de rendre implicitement des génériques capables de représenter une quantité arbitraire de types.

@felixfbecker
Éditer:
Ah d'accord. Je ne pense toujours pas que ce soit intuitif.

Je m'attendrais à ce que T soit un tableau de quelque chose (par exemple, string[]).

Un tuple est "quelque chose de tableau", c'est juste un tableau avec une longueur fixe et des types spécifiques pour chaque élément, par exemple [string, number] (vs (string | number)[] , qui n'est pas lié et ne déclare pas quel élément a quoi taper).

Eh bien, que tapez-vous si vous voulez réellement ce comportement ?

Je ne sais pas exactement à quel comportement vous faites référence, mais je suppose que "forcer tous les paramètres à être du même type", ce qui serait accompli par ...args: T[] .

Je l'ai lu comme l'opérateur de propagation aussi, je l'ai lu comme "propager ce type à chaque fois qu'il est utilisé".

C'est pourquoi je pense que c'est déroutant.
Lorsque vous diffusez, vous le faites simplement, vous ne déclarerez pas quelque chose qui est « propable » :

const a = { x: 1, y: 2 }
const b = { ...a }

// likewise
function appendString<T>(...args: T): [...T, string] {
  args.push('abc')
  return args
}

Oui. Si vous souhaitez déclarer qu'un argument de type générique doit être "propagable" (ce qui, selon la spécification ES, signifie simplement qu'il doit être itérable), nous avons déjà un moyen de l'exprimer dans TypeScript avec extends :

function foo<T extends Iterable<any>>(spreadable: T): [...T, string] {
  return [...spreadable, 'abc']
}

const bar = foo([1, true])
// bar is [number, boolean, string]

bien sûr, dans le cas d'un paramètre de repos, il est connu qu'il ne s'agit pas seulement d'un Iterable, mais d'un Array.

Lequel, ce que nous disons a déjà été proposé : https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -189703556

Mais le reste est trop long à consommer sur une seule place. ??

Si la concaténation de tuples arrive, alors nous pouvons implémenter les numéros d'église ! Hourra !

type TupleSuc<T extends [...number]> = [...T, T['length']];
type TupleZero = [];  // as proposed, we need empty tuple
type TupleOne = TupleSuc<TupleZero>;
type Zero = TupleZero['length'];
type One = TupleOne['length'];

Et si la récursivité de type conditionnel fonctionne, nous pouvons créer un tuple avec la longueur souhaitée :

type Tuple<N extends number, T = TupleZero> = T['length'] extends N ? T : Tuple<N, TupleSuc<T>>;
type TupleTen = Tuple<10>;
type Ten = TupleTen['length'];

Je ne présume pas avoir lu tout ce fil, mais si avoir ...T dans les paramètres génériques est déroutant,
pourquoi ne pas essayer de refléter davantage la syntaxe de déstructuration au niveau de la valeur, de sorte que l'utilisation de [...T] dans un
la position de l'argument au niveau du type déstructure le type s'il s'agit d'un tuple au niveau du type ? Cela nécessiterait également
permettant de taper les paramètres de repos avec des tuples, ce qui ferait l'équivalent suivant sur le site d'appel
en TypeScript :

const first = (a: number, b: string) => …;
const second = (...ab: [number, string]) => …;

first(12, "hello"); // ok
second(12, "hello"); // also ok

INB4 "mais c'est une émission dirigée par type" - non. Cela ne change rien à l'émission, le first en a toujours deux
arguments distincts émis, le second aura toujours un argument de repos. La seule chose que ça change c'est
que sur le site d'appel, TypeScript vérifiera que les paramètres de second correspondent, dans l'ordre, au tuple
[number, string] .

Quoi qu'il en soit, en supposant que nous admettions la syntaxe [...Type] , alors nous pourrions écrire apply comme suit :

function apply<
  [...ArgumentsT], // a type-level tuple of arguments
  ResultT
>(
  // the call site of `toApply` function will be used to infer values of `ArgumentsT`
  toApply:   (...arguments: ArgumentsT) => ResultT,
  arguments: ArgumentsT
) :
  ResultT
{
  // …
}

// NB: using my preferred formatting for complex type-level stuff; hope it's readable for you
// this is entirely equivalent to OP's notation version:
function apply<[...T], U>(ap: (...args: T) => U,  args: T): U {
  // …
}

// so at the call site of
const fn = (a: number, b: string, c: RegExp) => …;

// we have `ArgumentsT` equal to [number, string, RegExp]
apply(fn, [12, "hello" /s+/]); // ok, matches `ArgumentsT`
apply(fn, [12, /s+/]); // not ok, doesn't match `ArgumentsT`

La syntaxe [...Type] se comporterait entièrement comme une déstructuration au niveau de la valeur, permettant le fractionnement
et joindre des tuples au niveau du type, selon les besoins :

type SomeType  = [string, number, "constant"];
type OtherType = ["another-constant", number];

type First<[First, ..._]> = FirstT;
type Rest<[_, ...RestT]> = RestT;
type Concat<[...LeftT], [...RightT]> = [...LeftT, ...RightT];
type FirstTwo<[FirstT, SecondT, ..._]> = [FirstT, SecondT];

// has type `string`
const aString: First<SomeType> =
  "strrriiing";
// has type `[number, "constant"]
const numberAndConstant: Rest<SomeType> =
  [42, "constant"];
// has type `[string, number, "constant", "another-constant", number]`
const everything: Concat<SomeType, OtherType> =
  ["herpderp", 42, "constant", "another-constant", 1337];
// has type `[string, number]`
const firstTwo: FirstTwo<SomeType> =
  ["striiiing", 42];

Un exemple sur la façon de taper la fonction curry utilisant ceci :

type Curried<
  [...ArgumentsT]
  ResultT,
  ArgumentT      = First<ArgumentsT>,
  RestArgumentsT = Rest<ArgumentsT>
> =
  // just ye olde recursione, to build nested functions until we run out of arguments
  RestArgumentsT extends []
    ? (argument: ArgumentT) => ResultT
    : (argument: ArgumentT) => Curried<RestArgumentsT, ResultT>;

// NB. with more complex generic types I usually use generic defaults as a sort-of
// of type-level variable assignment; not at all required for this, just nicer to read IMO

function curry<
  [...ArgumentsT],
  ResultT
>(
  function: (...arguments: ArgumentsT) => ResultT
) :
  Curried<ArgumentsT, ResultT>
{
  // do the magic curry thing here
}

// or in the short indecipherable variable name style

function curry<[...T], U>(fn: (...args: T) => U): Curried<T, U>
{
  // …
}

// this should let you do this (using `fn` from before)
const justAddRegex = curry(fn)(123, "hello");

justAddRegex(/s+/); // ok, matches the arguments of `fn`
justAddRegex(123); // not ok, doesn't match the arguments of `fn`

Je suppose qu'il serait également utile de pouvoir dire qu'un argument de type est un tuple au niveau du type
de quelques sortes. La question serait alors de savoir comment – ​​étant donné que 2.7 (je pense ?) l'assignabilité des tuples prend
en compte la longueur de tuple - pour exprimer le concept de _tout tuple de niveau de type_. Mais peut-être quelque chose comme
[...] pourrait fonctionner ? Je n'ai pas d'opinion tranchée, mais ce serait bien si le concept était nommable.

// bikeshed me
type OnlyTuplesWelcome<ArgumentT extends [...]> = ArgumentT;

Dans ce cas, la syntaxe ci-dessus de [...ArgsT] pourrait être un raccourci pour ArgsT extends [...] ,
avoir l'utilisation de la déstructuration au niveau du type implique une contrainte sur le type pour être un tuple au niveau du type.

Les pensées?

@jaen :

(...ab: [number, string]) => …

Ouais, ça ressemble au #4130. J'ai essayé quelque chose au #18004, mais mon approche était un peu bidon (nœuds synthétiques).

En exprimant n'importe quel tuple, j'avais vu quelqu'un utiliser any[] & { 0: any } , ce qui, je suppose, fonctionne jusqu'à ce que le type de tuple vide fwiw. Je ne me suis pas beaucoup dérangé personnellement, la plupart du temps je me suis contenté de any[] .

RxJS en a besoin partout. Surtout pour Observable.prototype.pipe , pour lequel nous avons actuellement de nombreuses surcharges, mais on me demande toujours d'ajouter "juste un niveau de plus".

Pour seconder @benlesh, nous utilisons largement RXJS et en avons besoin pour les fonctions de canal.

Je suis l'auteur de ppipe , qui, comme les fonctions pipe dans RXJS, en a besoin. Je pense que je vois un modèle ici ^^

Je suis l'auteur de runtypes , et cette fonctionnalité est désespérément nécessaire pour exprimer les unions et les intersections. La seule solution de contournement (incomplète) est les surcharges gargantuesques :

https://github.com/pelotom/runtypes/blob/master/src/types/union.ts

??

J'avais réécrit les types de ramda , qui en ont également besoin pour, par exemple, pipe , les lentilles et le curry.
Nous avions besoin de codegen car le curry rendait le maintien direct des surcharges ingérable. Notre type path s'étendait sur plus d'un millier de lignes, auquel cas nous avons constaté que les performances du type surcharge étaient également devenues problématiques.

Le problème de déduction et d'application des arguments de repos a-t-il été résolu ?

function example(head: string, ...tail: number[]): number[] {
  return [Number(head), ...tail]
}

function apply<T, U>(fn: (...args: T) => U, args: T): U {
  return fn.apply(null, args)
}

Si le type de T dans apply(example, ['0', 1, 2, 3]) est déduit comme [string, number[]] , l'appel à apply lèverait une erreur.

Cela signifie que le type de T est vraiment

type T = [string, ...number[]]

ou

type T =
  {0: string} &
  {[key: Exclude<number, 0>]: number} &
  Methods

Une bête étrange en effet, mais étant donné à quel point ({0: string} & Array<number>)[0] va
se résout actuellement à string [1] il semble possible d'encoder sans trop de changement
au système de types.

[1] Est-ce un bug, devrait-il vraiment être string | number ?

Désolé de déranger 36 participants de ce numéro (conseil : utilisez ceci ), mais comment pouvons-nous surveiller si cela est toujours à l'étude, si cela est sur la feuille de route, etc.

C'est triste que personne n'y ait encore été affecté après 2 ans et demi, cela semble être une caractéristique assez importante :(

PS : j'ai lu quelques douzaines de commentaires, essayé Cmd+F etc, je n'ai pas trouvé cette information.

@brunolemos, il y a une référence aux types variadiques dans la dernière réunion de conception
https://github.com/Microsoft/TypeScript/issues/23045
Pour créer cette fonctionnalité, ils doivent d'abord créer des concepts plus primitifs et itérativement, et quand il y aura suffisamment de fondations, je suis sûr qu'ils l'ajouteront à n'importe quel jalon

je ne peux pas faire ça

type Last<T extends any[]> =
    T extends [infer P] ? P :
    ((...x: T) => any) extends ((x: any, ...xs: infer XS) => any) ? Last<XS> :

Il s'agit de #14174 mais en relation

@kgtkr pour référence , voir l » @fightingcat astuce pour obtenir le compilateur de négliger la récursion.

Merci

type Last<T extends any[]> = {
    0: never,
    1: Head<T>,
    2: Last<Tail<T>>,
}[T extends [] ? 0 : T extends [any] ? 1 : 2];

Hum, j'ai une question. J'ai un code comme celui-ci pour gérer les mixins :

export const Mixed = <

    OP = {}, OS = {}, // base props and state
    AP = {}, AS = {}, // mixin A props and state
    BP = {}, BS = {}, // mixin B props and state
    // ...and other autogenerated stuff
>(

    // TODO: Find a way to write that as ...args with generics:
    a?: ComponentClass<AP, AS>,
    b?: ComponentClass<BP, BS>,
    // ...and other autogenerated stuff

) => {

    type P = OP & AP & BP;
    type S = OS & AS & BS;
    const mixins = [a, b];

    return class extends Component<P, S> {
        constructor(props: P) {
            super(props);
            mixins.map(mix => {
                if (mix) {
                    mix.prototype.constructor.call(this);
                    // some state magic...
                }
            });
        }
    };
};

Je l'utilise comme suit :

class SomeComponent extends Mixed(MixinRedux, MixinRouter, MixinForm) {
     // do some stuff with mixed state
}

Cela fonctionne comme prévu - avec une saisie appropriée, une gestion des états, etc., mais existe-t-il un moyen de réécrire de manière plus courte sans attendre un type variadique ? Parce que je me sens un peu stupide à propos de celui-ci en ce moment.

Avec 3.0, il est maintenant possible de déclarer les arguments de repos en tant que tuple

declare function foo(...args: [number, string, boolean]): void;

Mais est-il possible d'obtenir l'inverse pour obtenir le type d'arguments tuple de la fonction donnée?

Quelque chose comme Arguments<foo> serait bien.

@whitecolor et ça ?

type Arguments<F extends (...x: any[]) => any> =
  F extends (...x: infer A) => any ? A : never;

avec TS 3.0, nous pouvons le faire maintenant

function compose<X extends any[], Y extends any[], Z extends any[]>(
  f: (...args: X) => Y,
  g: (...args: Y) => Z
): (...args: X) => Z {
  return function (...args) {
    const y = f(...args);
    return g(...y);
  };
}

mais nous avons un petit problème, nous devons déclarer des fonctions qui renvoient un événement tupples pour des paramètres uniques et également traiter void d'une manière ou d'une autre et nous devons déclarer le type de retour sinon il est déduit comme un tableau :)

https://www.typescriptlang.org/play/index.html#src =%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A%0D%0Afunction%20foo0() %3A%20void%20%7B%0D%0A%20%0D%0A%7D%0D%0A%0D%0Afunction%20bar0()%3A%20void%20%7B%0D%0A%0D%0A%7D %0D%0A%0D%0Afonction%20foo1(a%3A%20string)%3A%20%5Bstring%5D%20%7B%0D%0A%20%20return%20%5Ba%5D%3B%0D%0A% 7D%0D%0A%0D%0Afonction%20bar1(a%3A%20string)%3A%20%5Bstring%5D%20%7B%0D%0A%20%20return%20%5Ba%5D%3B%0D%0A %7D%0D%0A%0D%0Afunction%20foo2(a1%3A%20string%2C%20a2%3A%20boolean)%3A%20%5Bstring%2C%20boolean%5D%20%7B%0D%0A%20% 20retour%20%5Ba1%2C%20a2%5D%3B%0D%0A%7D%0D%0A%0D%0Afonction%20foo21(a1%3A%20string%2C%20a2%3A%20booléen)%3A%20%5Bstring %5D%20%7B%0D%0A%20%20retour%20%5Ba1%5D%3B%0D%0A%7D%0D%0A%0D%0A%0D%0Afonction%20bar2(a1%3A%20string%2C %20a2%3A%20boolean)%3A%20%5Bstring%2C%20boolean%5D%20%7B%0D%0A%20%20return%20%5Ba1%2C%20a2%5D%3B%0D%0A%7D% 0D%0A%0D%0A%0D%0Afonction%20composer%3CX%20étend%20any%5B%5D%2C%20Y%20étend%20any%5B%5D%2C%20Z%20étend%20any%5B%5D%3E( %0D%0A%20%20f%3A%20(... args%3A%20X)%20%3D%3E%20Y%2C%0D%0A%20%20g%3A%20(...args%3A%20Y)%20%3D%3E%20Z%0D%0A )%3A%20(...args%3A%20X)%20%3D%3E%20Z%20%7B%0D%0A%20%20retour%20fonction%20(...args)%20%7B% 0D%0A%20%20%20%20const%20y%20%3D%20f(...args)%3B%0D%0A%20%20%20%20return%20g(...y)%3B% 0D%0A%20%20%7D%3B%0D%0A%7D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0Aconst%20baz0%20%3D%20composer(%0D%0A %20%20foo0%2C%0D%0A%20%20bar0%0D%0A)%3B%0D%0A%0D%0Aconst%20baz21%20%3D%20compose(%0D%0A%20%20foo21%2C%0D %0A%20%20bar1%0D%0A)%3B%0D%0Aconst%20baz2%20%3D%20compose(%0D%0A%20%20foo2%2C%0D%0A%20%20bar2%0D%0A)% 3B%0D%0A%0D%0A%0D%0Aalert(baz2('a'%2C%20false))%0D%0Aalert(baz21('a'%2C%20true))%0D%0Aalert(baz0())

@maciejw
Utilisez des types conditionnels :

function compose<X extends any[], Y extends any, Z extends any>(
  f: (...args: X) => Y,
  g: Y extends any[] ? (...args: Y) => Z : () => Z
): (...args: X) => Z {
    return function (...args) {
        const y = (f as any)(...args);
        return (g as any)(...y);
    } as any;
}

bien sûr, mais c'est un peu bidon avec ces as any :) Je pense que ce serait cool si le système de type supportait cela sans hacks

Eh bien, les types dans le corps de la fonction peuvent être plusieurs choses, il n'y a pas grand-chose à faire à ce sujet - vous pouvez faire quelque chose comme (f as (...args: any[]) => Y) place mais je pense que cela réduit la clarté sans raison réelle

Si les types variadiques se concrétisent, je serais en mesure de généraliser du code TypeScript que j'ai écrit pour mon propre projet, qui me permettrait de définir complètement la forme de mon API REST et d'appliquer cette forme aux types du serveur Node correspondant et de JavaScript. bibliothèque cliente pour cela.

Généraliser cela me permettrait de simplifier mon propre code, ainsi que de faire de même pour des API arbitraires, et d'aller plus loin et probablement de générer des définitions Swagger pour d'autres clients de langage aussi... pourrait être utile pour d'autres ! Je rêve juste à haute voix haha

@kgtkr : ça a l'air super ! :)

Le type Pipe plante le terrain de jeu TS pour moi (bien que d'autres fonctionnent bien), je suppose qu'il a besoin du dernier TS?

TS montre également des erreurs de profondeur de récursivité -- on dirait que @isiahmeadows a ouvert #26980 pour cela.

@tycho01

Le type Pipe plante le terrain de jeu TS pour moi (bien que d'autres fonctionnent bien), je suppose qu'il a besoin du dernier TS?

Cela se bloque aussi pour moi, et j'ai dû pirater les outils de développement pour le forcer à lancer une erreur pour en sortir.

TS montre également des erreurs de profondeur de récursivité -- on dirait que @isiahmeadows a ouvert #26980 pour cela.

C'est pour quelque chose de connexe, mais différent : lever une contrainte avec des types conditionnels pour deux raisons :

  • Pour faciliter le travail plus complexe comme l'itération de liste. Cela poserait également le cadre pour ouvrir des choses comme les mathématiques entières au niveau du type sans planter le compilateur ou se retrouver dans un désordre ad-hoc.
  • Pour résoudre plus correctement le problème de ce qui rend le système de type Turing de TS complet, il peut donc être potentiellement réifié avec des outils qui l'exploitent ou supprimé en appliquant une terminaison prouvable sur la route.

Si la suppression des types indexés n'est pas suffisante pour que le système de types ne soit pas Turing-complet tel quel, n'hésitez pas à y déposer un commentaire afin que je puisse le mettre à jour en conséquence. (Je ne propose pas de le supprimer, bien sûr. Je propose juste de mieux le gérer en interne pour avertir les gens des boucles infinies potentielles.)

À la réflexion, ce type Pipe ressemble à une façon vraiment alambiquée de faire (vs...: Params<T[0]>) => ReturnType<Last<T>> . Toute itération au-delà (à part les vérifications de paramètres intermédiaires) devient probablement plus utile avec les types de retour dépendants de l'entrée.

@tycho01 Il essaie de taper des choses comme ceci , où le type est essentiellement celui-ci:

   f1,     f2,   ...,   fm,     fn    -> composed
(a -> b, b -> c, ..., x -> y, y -> z) -> (a -> z)

Vous devez itérer les paramètres individuellement pour le taper correctement, car les paramètres des paramètres suivants dépendent des valeurs de retour des paramètres précédents, et vous devez également tenir compte du premier paramètre et de la valeur de retour de fin (ce que @kgtkr ne fait pas) . Ma version "améliorée" dans # 26980 a fait une optimisation pour utiliser un accumulateur à la place, donc j'itérais beaucoup moins les arguments, mais cela l'a également rendu plus correct.


Si vous regardez le mien dans # 26980, c'est un peu plus clair ce qu'il est censé faire (moins de chasse aux numéros), et c'est en partie pourquoi j'ai déposé cette demande de fonctionnalité.

@tycho01 @kgtkr BTW, j'ai mis à jour ce bogue avec un extrait de code PipeFunc corrigé , copié ici pour plus de commodité :

type Last<L extends any[], D = never> = {
    0: D,
    1: L extends [infer H] ? H : never,
    2: ((...l: L) => any) extends ((h: any, ...t: infer T) => any) ? Last<T> : D,
}[L extends [] ? 0 : L extends [any] ? 1 : 2];

type Append<T extends any[], H> =
    ((h: H, ...t: T) => any) extends ((...l: infer L) => any) ? L : never;

type Reverse<L extends any[], R extends any[] = []> = {
    0: R,
    1: ((...l: L) => any) extends ((h: infer H, ...t: infer T) => any) ?
        Reverse<T, Append<R, H>> :
        never,
}[L extends [any, ...any[]] ? 1 : 0];

type Compose<L extends any[], V, R extends any[] = []> = {
    0: R,
    1: ((...l: L) => any) extends ((a: infer H, ...t: infer T) => any) ?
        Compose<T, H, Append<R, (x: V) => H>>
        : never,
}[L extends [any, ...any[]] ? 1 : 0];

export type PipeFunc<T extends any[], V> =
    (...f: Reverse<Compose<T, V>>) => ((x: V) => Last<T, V>);

Celui-ci ne se bloque pas dans la cour de récréation, BTW. En fait, il vérifie le type et il le fait assez rapidement.

Je ne l'ai pas encore testé sur un type potentiel _.flow ou _.flowRight , mais cela devrait fonctionner comme point de départ.

@tycho01
obligatoire
typescript@suivant
3.0.1/3.0.2 ne fonctionne pas

Grâce à ce fil, j'ai fait ça

Les gens, s'il vous plaît, arrêtez de publier des informations à peine pertinentes pour la discussion de ce problème. Il y a beaucoup de gens qui suivent cette discussion, parce que nous voulons des genres variadiques. J'ai reçu plus de 10 e-mails au cours des deux derniers jours qui ne sont pas pertinents pour les raisons pour lesquelles je suis ce problème.
Je suppose qu'il y en a d'autres d'accord avec moi. Jusqu'à présent, j'espérais simplement que cela s'arrêterait, car je ne voulais pas contribuer au spam. Mais sérieusement, ça suffit.
PS Désolé pour cette notification, à tous ceux qui en ont marre que moi

@Yuudaari Je ferai remarquer que la saisie de Lodash _.flow , Ramda de _.compose , etc. , est l' une des choses pour ce bug de conduite, et une frappe réussie fait partie de résoudre ce problème. En fait, c'est l'une des raisons énumérées dans la description originale du problème.

Vraiment, à ce stade, je suis d'avis que 99% des problèmes qui existent aujourd'hui avec variadics sont liés à l'ergonomie, pas à la fonctionnalité. Nous pouvons taper Function.prototype.bind et Promise.all parfaitement avec un mélange de types indexés, de types conditionnels et de récursivité (vous pouvez faire un Append répété en itérant la liste pour Function.prototype.bind , et Promise.all serait une simple itération + Append ), c'est juste très gênant et passe-partout de le faire.

N'essayant pas d'ajouter au bruit ici, expliquant simplement que les choses qui commencent ici sont techniquement pertinentes car elles concernent certaines des raisons pour lesquelles le bogue existe, même si ce ne sont pas celles qui vous préoccupent personnellement.

Je pense que les gens qui attendaient des annonces ici ont raté la grande nouvelle - il s'est avéré que les fonctions maintenant possibles Concat<T, U> exactement comme [...T, ...U] .

Le sous-thème Pipe concerne la démonstration de la fonctionnalité que nous avons demandée ici. Il s'agit d'atteindre le point de ce fil, aujourd'hui.

Je pense que cela signifie que nous ne serions pas plus mal à l'aise de fermer ce fil, alors c'est peut-être le bon moment pour demander - qu'est-ce que les gens attendent encore de cette proposition ?

[c'est] juste que c'est très gênant et passe-partout

La plupart des types qui l'utilisent utiliseront eux-mêmes la récursivité, de sorte que ceux qui les écrivent les connaîtront certainement, tandis que les utilisateurs finaux utiliseront probablement simplement des bibliothèques avec des types prédéfinis et écriront leur front-end, sans avoir à savoir que l'itération TS existe.

À ce stade, peut-être que cette proposition améliorera principalement les performances ?

Premièrement, est-ce que l'utilisation d'objets de carte pour amener le système de types à faire de la récursivité est même prévue ? Cela me semble assez bidon. Si j'utilise une fonctionnalité comme celle-là (je le suis, mais ça n'a pas d'importance), ne va-t-elle pas être sujette à casser plus tard ?

Deuxièmement, l'utilisation de ces solutions de contournement n'est tout simplement pas... conviviale. Ce n'est pas très lisible (surtout pour ceux qui ne l'ont pas écrit), et du coup ça a l'air misérable à maintenir.

Pourquoi voudrais-je me rabattre sur une proposition qui ajoute les mêmes fonctionnalités, de manière intentionnelle, lisible et maintenable, simplement parce qu'il existe une solution de contournement pour elles ?

Je ne pense pas que l'existence de cette solution de contournement fasse que cette proposition soit considérée comme du sucre syntaxique, mais même si c'était le cas, pourquoi ne voudrais-je pas de sucre syntaxique pour ce gâchis ?

@Yuudaari

Modifier : ajouter un lien pour le contexte.

Premièrement, est-ce que l'utilisation d'objets de carte pour amener le système de types à faire de la récursivité est même prévue ? Cela me semble assez bidon. Si j'utilise une fonctionnalité comme celle-là (je le suis, mais ça n'a pas d'importance), ne va-t-elle pas être sujette à casser plus tard ?

Jetez un œil au bug que j'ai récemment signalé : #26980. Vous n'êtes pas le seul à remettre en question le modèle. C'est un peu hors sujet ici, mais n'hésitez pas à y répondre.

Notez qu'il y a un peu de mathématiques impliquées dans la façon de déterminer si quelque chose de récursif se termine (l'une des principales raisons pour lesquelles il est si nuancé en premier lieu).

Deuxièmement, l'utilisation de ces solutions de contournement n'est tout simplement pas... conviviale. Ce n'est pas très lisible (surtout pour ceux qui ne l'ont pas écrit), et du coup ça a l'air misérable à maintenir.

Pourquoi voudrais-je me rabattre sur une proposition qui ajoute les mêmes fonctionnalités, de manière intentionnelle, lisible et maintenable, simplement parce qu'il existe une solution de contournement pour elles ?

Je ne pense pas que l'existence de cette solution de contournement fasse que cette proposition soit considérée comme du sucre syntaxique, mais même si c'était le cas, pourquoi ne voudrais-je pas de sucre syntaxique pour ce gâchis ?

Il existe un moyen simplifié d'itérer des tuples dans le cas courant de ce qui est effectivement Array.prototype.map , mais c'était fondamentalement inutile pour mes besoins (j'avais besoin d'un accumulateur).

Personnellement, j'aimerais du sucre syntaxique pour ceux-ci :

  1. Concaténer deux listes via [...First, ...Second] .
  2. Ajout de valeurs via [...Values, Item] .
  3. Extraire le dernier élément via T extends [...any[], infer Last] .
  4. Extraire la queue via T extends [A, B, ...infer Tail] .

Combinez cela avec # 26980, et je pourrais transformer les types ci-dessus en ceci:

type Compose<L extends any[], V, R extends any[] = []> =
    L extends [infer H, ...infer T] ?
        Compose<T, H, [...R, (x: V) => H]> :
        R;

export type PipeFunc<T extends any[], V> =
    T extends [...any[], infer R] ?
        (...f: Compose<T, V>) => ((x: V) => R) :
        () => (x: V) => V;

Mais c'est à peu près tout. Je ne vois pas beaucoup d'utilité pour tout autre sucre syntaxique, car la plupart de tout ici ne traite que des tuples, et les objets ont déjà en grande partie tout ce dont vous auriez besoin pour des opérations similaires.

Premièrement, est-ce que l'utilisation d'objets de carte pour amener le système de types à faire de la récursivité est même prévue ? Cela me semble assez bidon. Si j'utilise une fonctionnalité comme celle-là (je le suis, mais ça n'a pas d'importance), ne va-t-elle pas être sujette à casser plus tard ?

Je pense que le mot officiel est quelque chose comme "ne le fais pas". @ahejlsberg a dit :

C'est intelligent, mais cela pousse définitivement les choses bien au-delà de leur utilisation prévue. Bien que cela puisse fonctionner pour de petits exemples, cela évoluera horriblement. La résolution de ces types profondément récursifs consomme beaucoup de temps et de ressources et pourrait à l'avenir contrecarrer les gouverneurs de récursivité que nous avons dans le vérificateur.

Ne le fais pas !

☹️

@jcalz Raison de plus pour que le #26980 existe ?

Quand j'ai commencé à utiliser TS juste à la pause de cette année, mon envie était d'écrire _juste ça_ ! ( ...T ) dans l'espoir que ce serait la syntaxe pour les tuples de typevar variadiques. Eh bien, j'espère que cela arrivera :)

Je viens de trouver une nouvelle utilisation pour [...T, ...U] : taper correctement les constructeurs HTML. Pour un exemple concret, les enfants de <video> doivent être les suivants :

  • Si l'élément a un attribut src :

    • Zéro ou plus d'éléments <track>

  • Si l'élément n'a pas d'attribut src :

    • Zéro ou plus d'éléments <source>

  • Zéro ou plusieurs éléments selon le modèle de contenu du parent, sauf qu'aucun élément descendant audio ou video n'est autorisé.

Cela équivaut essentiellement à ce type, mais il n'y a aucun moyen d'exprimer cela dans TypeScript aujourd'hui :

type VideoChildren<ParentModel extends string[]> = [
    ...Array<"track">, // Not possible
    ...{[I in keyof ParentModel]: P[I] extends "audio" | "video" ? never : P[I]},
]

3,5 ans :/

Cas d'utilisation:

type DrawOp<...T> = (G: CanvasRenderingContext2D, frame: Bounds, ...args: any[]) => void;
const drawOps: DrawOp<...any>[] = [];

function addDrawOp<...T>(fn: DrawOp<...T>, ...args: T) {
    drawOps.push(fn);
}

Je ne vois que des surcharges mentionnées brièvement dans la section des questions ouvertes de la proposition, mais certainement quelque chose que j'ai rencontré et qui serait excellent pour voir une solution ou une proposition, par exemple :

  function $findOne(
    ctx: ICtx,
    filter: FilterQuery<TSchema>,
    cb: Cb<TSchema>,
  ): void;
  function $findOne<T extends keyof TSchema>(
    ctx: ICtx,
    filter: FilterQuery<TSchema>,
    projection: Projection<T>,
    cb: Cb<Pick<TSchema, T>>,
  ): void;
  function $findOne(
    ctx: ICtx,
    filter: FilterQuery<TSchema>,
    projection: undefined,
    cb: Cb<TSchema>,
  ): void;
  function $findOne<T extends keyof TSchema>(
    ctx: ICtx,
    filter: mongodb.FilterQuery<TSchema>,
    projection: Projection<T> | Cb<TSchema> | undefined,
    cb?: Cb<Pick<TSchema, T>>,
  ): void {

  promisify($findOne) // this can't infer types correctly

actuellement, cela ne fonctionne pas du tout et tape simplement le promisify comme (ctx: ICtx, filter: FilterQuery<TSchema>) => Promise<TSchema[]> ce qui perd les informations de ces signatures.

AFAICT, la seule vraie solution à cela consiste essentiellement à créer une fonction de promesse et à spécifier manuellement tous les types possibles pour cette variante - la seule façon de fournir réellement une signature honorée à partir de la variante enveloppée est de ne pas spécifier de surcharges et de spécifier simplement la signature d'implémentation, mais il n'y a aucun moyen de faire savoir à l'appelant à quel type de retour il doit s'attendre en fonction des arguments qu'il passe si vous spécifiez la signature de cette manière.

ceci est exacerbé par le fait que les paramètres de repos ne peuvent être que le dernier paramètre (c'est- (cb, ...args) dire que (...args, cb) , donc même si la signature est en interne de type union, vous ne pouvez pas réellement répandre les choses correctement - par exemple, ce serait assez simple si cb était toujours le premier argument pour taper promisify comme function promisify<T, V extends any[]>(fn: (cb: (err: Error | null, res?: T) => void, ...args: V)): (...args: V) => T et vous pourriez au moins obtenir des types d'union pour les signatures avec la même réponse de retour, mais parce que c'est le dernier paramètre afaic il n'y a pas grand chose à faire ici

@Qix- Votre scénario est activé par #24897. A été implémenté dans TS 3.0.

@ahejlsberg Ouf ! Génial, merci ️

L'attente a été longue... Mais il est possible d'écrire des genres variadiques aujourd'hui. TS est suffisamment mature pour écrire des types complexes qui fonctionnent. J'ai donc pris le temps d'écrire des types pour curry, concat , composer et pipe pour Ramda.

Et sont maintenant livrés avec le ts-toolbelt .

Cependant, cette proposition est un bon sucre syntaxique pour rendre les manipulations de tuples communes beaucoup plus faciles.

l'avez-vous déjà sur medium.com ? URL ?

Il y a l'article original sur Medium mais le bonus n'y est pas inclus, il est dans le repo. Il explique aussi comment j'ai créé tous les petits outils pour arriver à composer, pipe & curry :smile:

@pirix-gh mais ce ne sont pas des génériques variadiques comme dans cette proposition

declare function m<...T>(): T

m<number, string>() // [number, string]

@goodmind Oui, ce n'est pas le cas, c'est plus émulé. Vous pouvez donc émuler le ... comme ceci :

declare function m<T extends any[], U extends any[]>(): Concat<T, U>

m<[number, string], [object, any]>() // [number, string, object, any]

Est le même que:

declare function m<...T, ...U>(): [...T, ...U]

m<number, string, object, any>() // [number, string, object, any]

En attendant, en attendant cette proposition :hourglass_flowing_sand :

@pirix-gh pouvez-vous s'il vous plaît aider avec la fonction d'emballage comme

type fn = <T>(arg: () => T) => T
let test1: fn
let res1 = test1(() => true) // boolean

type fnWrap = (...arg: Parameters<fn>) => ReturnType<fn>
let test2: fnWrap
let res2 = test2(() => true) // {}

J'essayais d'utiliser l'approche de composition mais j'ai échoué, pouvez-vous suggérer une manière appropriée s'il vous plaît ?

Cela se produit parce que lorsque vous extrayez les paramètres/retours de fn qui dépendent des génériques, TS les déduit de leur type le plus proche (dans ce cas, T sera any ). Il n'y a donc aucun moyen d'y parvenir pour le moment. Notre meilleur espoir est d'attendre que cette proposition se combine avec https://github.com/Microsoft/TypeScript/pull/30215. Donc, vous devrez écrire des surcharges de type.

Ou, peut-être pourrions-nous trouver un moyen de préserver/déplacer les génériques de telle manière que nous puissions faire :

declare function ideal<...T>(a: T[0], b: T[1], c: T[2]): T

ideal('a', 1, {}) // T = ['a', 1, {}]

De cette façon, nous reconstruirions fn partir de morceaux. La partie manquante aujourd'hui est la partie générique comme l' a souligné

@pirix-gh Si je ne me trompe pas, vous pouvez simplement le faire pour obtenir ce que vous avez là-haut :

declare function MyFunction<A, B, C, Args extends [A, B, C]>(...[a, b, c]: Args): Args

const a = MyFunction(1, 'hello', true);
// typeof a = [number, string, boolean]

@ClickerMonkey Pas exactement, car ce que j'ai proposé fonctionne pour un nombre illimité d'arguments. Mais peut-être qu'on pourrait aussi faire ça, avec ce que tu as proposé (je ne l'ai pas vu dans la proposition) :

declare function MyFunction<A, B, C, ...Args>(...[a, b, c]: Args): Args

const a = MyFunction(1, 'hello', true);
// typeof a = [number, string, boolean]

@pirix-gh Les arguments de type A , B et C dans votre exemple ne sont pas utilisés.

-declare function MyFunction<A, B, C, ...Args>(...[a, b, c]: Args): Args
+declare function MyFunction<...Args>(...[a, b, c]: Args): Args

Même si les types variadiques étaient implémentés, les deux exemples précédents généreraient probablement une erreur de compilation, quel est l'intérêt des types variadiques si vous ne voulez que trois arguments.

Vos exemples devraient montrer pourquoi variadique est nécessaire, s'ils peuvent être effectués avec le code TS existant, cela n'aide pas du tout la cause.

@goodmind Oui, ce n'est pas le cas, c'est plus émulé. Vous pouvez donc émuler le ... comme ceci :

declare function m<T extends any[], U extends any[]>(): Concat<T, U>

m<[number, string], [object, any]>() // [number, string, object, any]

Est le même que:

declare function m<...T, ...U>(): [...T, ...U]

m<number, string, object, any>() // [number, string, object, any]

En attendant, en attendant cette proposition ⏳

Où avez-vous obtenu le Concat<> ?

Edit : peu importe, j'ai trouvé le code source.

@pirix-gh, j'ai donc essayé de le faire avec vos suggestions mais je n'ai pas pu le comprendre.

~ Le problème est que j'essaie d'étendre les paramètres du ctor d'une classe et cela fonctionne au point que j'ai un tableau de types mais je ne peux pas les diffuser pour les paramètres de ctor.~

Class Test {
  constructor(x: number, y: string) {}
}
let ExtendedClass = extendCtor<[number, string], [number]>(Test);

let instance = new ExtendedClass(1, '22', 2);

Mise à jour : peu importe que cela fonctionnait également en utilisant une propagation dans la fonction ctor.

voici le lien de la solution

Le seul problème est que TS plante presque à chaque fois :|
et c'est ce que TypeScript dit Type instantiation is excessively deep and possibly infinite.ts(2589)

Mise à jour 2 :
Je l'ai réalisé en mettant le nouveau type au début, mais ce serait quand même bien de pouvoir fusionner ces types.

// ...
type CtorArgs<T, X> = T extends (new (...args: infer U) => any) ? [...U, X] : never;
// To be used as CtorArgs<typeof Test, string>
// ...
let instance = new MyClass1('22', 2, 'check');

par opposition à :

let MyClass1 = extendClass<typeof Test, string>(Test);

let instance = new MyClass1('check', '22', 2);

Lien vers la solution finale.

Si je comprends bien, Object.assign peut être déclaré quelque chose comme ce qui suit pour prendre pleinement en charge les arguments variadiques.

type Assign<T, U extends any[]> = {
  0: T;
  1: ((...t: U) => any) extends ((head: infer Head, ...tail: infer Tail) => any)
    ? Assign<Omit<T, keyof Head> & Head, Tail>
    : never;
}[U['length'] extends 0 ? 0 : 1]

interface ObjectConstructor {
  assign<T, U extends any[]>(target: T, ...source: U): Assign<T, U>
}

Y a-t-il une raison pour laquelle il est déclaré d'une manière différente dans le lib.d.ts TypeScript ?

Il n'utilise pas de types conditionnels récursifs car ceux-ci ne sont pas pris en charge (voir # 26980 pour une discussion à ce sujet, ou ce commentaire nous disant de ne pas le faire). Si l'on est prêt à utiliser les types de retour d'intersection actuels, il y a #28323.

@jcalz J'ai créé un test lourd qui montre le type Minus en action. Il effectue le Minus 216000 fois en moins de 4 secondes. Cela montre que TS peut très bien gérer les types récursifs. Mais c'est assez récent.

Pourquoi? C'est grâce à Anders :tada: (https://github.com/microsoft/TypeScript/pull/30769). Il m'a permis de passer des types conditionnels aux conditions indexées (comme un commutateur). Et en fait, il a amélioré les performances de x6 pour le ts-toolbelt. Tellement, un grand merci à lui.

Donc techniquement, nous pourrions réécrire le type de

import {O, I, T} from 'ts-toolbelt'

// It works with the same principles `Minus` uses
type Assign<O extends object, Os extends object[], I extends I.Iteration = I.IterationOf<'0'>> = {
    0: Assign<O.Merge<Os[I.Pos<I>], O>, Os, I.Next<I>>
    1: O
}[
    I.Pos<I> extends T.Length<Os>  
    ? 1
    : 0
]

type test0 = Assign<{i: number}, [
    {a: '1', b: '0'},
    {a: '2'},
    {a: '3', c: '4'},
]>

La lib sécurise également la récursivité avec Iteration qui empêchera tout débordement de TypeScript. En d'autres termes, si I dépasse 40 alors il déborde et Pos<I> est égal à number . Arrêtant ainsi la récursivité en toute sécurité.

Un type récursif similaire que j'ai écrit ( Curry ) est livré avec Ramda , et il semble qu'il fonctionne bien.

D'ailleurs je t'ai remercié (@jcalz) sur la page du projet pour tous tes bons conseils.

Je ne sais pas si le #5453 est le meilleur endroit pour avoir cette discussion... devrions-nous en parler au #26980 ou y a-t-il un endroit plus canonique ? En tout cas , j'aimerais avoir un moyen officiel et soutenu pour ce faire qui ne peut imploser sur les versions ultérieures dactylographiées. Quelque chose qui est inclus dans leurs tests de base afin que s'il casse, ils le réparent. Même si les performances sont testées pour être bonnes, je me méfierais de le faire dans n'importe quel environnement de production sans un mot officiel de quelqu'un comme @ahejlsberg.

Quelque chose qui est inclus dans leurs tests de base afin que s'il casse, ils le réparent.

Je pense que nous avons quelque chose d'assez proche utilisé en interne

@weswigham pardonne-moi d'être dense, mais pouvez-vous me montrer comment le type en surbrillance est récursif ? La chose qui m'inquiète est de la forme

type Foo<T> = { a: Foo<Bar<T>>, b: Baz }[Qux<T> extends Quux ? "a" : "b" ]

ou l'une des variantes que j'ai vues. S'il me manque quelque chose et que cela a reçu une sorte de feu vert, s'il vous plaît, faites-le moi savoir (et apprenez-moi à l'utiliser !)

Oh, juste - c'est différent à cet égard, ouais. Je viens de dire le modèle "objet immédiatement indexé pour sélectionner les types" et j'ai réalisé que nous avions _ce_.

J'ai une question.

Dans quelle mesure les éléments proposés ici sont-ils encore pertinents ? Ce numéro a été ouvert il y a 4 ans et j'ai l'impression que beaucoup de choses ont changé depuis lors.

D'après mon commentaire ici,
https://github.com/microsoft/TypeScript/issues/33778#issuecomment-537877613

J'ai dit,

TL ; DR, types de tuples, arguments de repos, types de tableaux mappés, inférence de tuples pour les arguments de non repos, alias de type récursif = pas vraiment besoin de prise en charge des arguments de type variable

Mais je suis curieux de voir si quelqu'un a un cas d'utilisation qui ne peut tout simplement pas être activé par les outils existants

Jusqu'à ce que nous obtenions une version officiellement bénie de Concat<T extends any[], U extends any[]> cela reste d'actualité. Je ne pense pas que la prochaine fonctionnalité de référence de type récursif nous donne cela, mais je serais heureux qu'on (avec autorité) dise le contraire.

N'avons-nous pas déjà des implémentations de Concat<> ?

Ou est-ce que la phrase clé ici est « officiellement béni » ?

Parce que mon affirmation est que vous pouvez en gros faire tout (ou presque tout ?) que vous pourriez vouloir en ce moment, même si ce n'est pas tout à fait "officiellement béni".

Mais je suppose que "officiellement béni" devrait toujours être préféré... Bon point. Je suis trop habitué à (ab)utiliser ces alias de type récursif

Je préférerais généralement une syntaxe réelle et élégante de sorte que chaque fois que je fais quelque chose comme ça, je n'ai pas à continuer à expliquer à mes coéquipiers (souvent juniors) ce qui se passe pour les types spécifiés de manière confuse que l'abus du statu quo nécessite. Cette confusion nuit à ma capacité à évangéliser TypeScript, ou du moins à ses utilisations, dans mon organisation.

Gros 👍 à ce sujet !

Cette fonctionnalité est si importante.

@AnyhowStep

Parce que mon affirmation est que vous pouvez fondamentalement tout faire (ou presque tout ?) que vous pourriez vouloir en ce moment

Je ne vois aucune preuve que la saisie d'un paramètre de propagation avec plusieurs paramètres simples à côté soit facilement réalisable aujourd'hui dans TS.

@matthew-dean n'est pas tout à fait vrai. Voici un exemple que vous pouvez réaliser dans une certaine mesure.

D'après ce que je comprends, TS cherche à taper autant de programmes JS vanille que possible. Voici une énigme :

const f = <T extends any[]>(...args: T): T => args;
const g = <T extends any[]>(...a: T): WhatExactly<T> => {
    return f(3, ...a, 4, ...a, 5);
}
g(1, 2);

Je m'attendrais à ce que le type ne soit pas plus complexe que [number, ...T, number, ...T, number] . Si nous devons écrire 20 lignes d'un code étrange qui abuse d'un bogue, vérifiez que le type de retour est correct dans la dernière ligne, ce problème n'est pas résolu.

@polkovnikov-ph Le type est actuellement déduit comme : [number, ...any[]] , ce qui n'est pas utile.

J'aimerais également noter que nous n'avons pas à respecter la dixième règle de Greenspun pendant 15 ans comme C++ l'a fait, car C++ a déjà parcouru tous les Head<> s et Cons<> pour nous, et a conçu une syntaxe de modèle variadique très pratique et propre. Nous pouvons économiser (des centaines d'années) du temps de développement et en tirer les meilleurs éléments.

Par exemple, les types variadiques ont un genre différent en C++, vous ne pouvez donc pas utiliser de variables de type variadique là où le type est attendu, contrairement à un type qui extends any[] dans TS. Cela permet à C++ de mapper/zipper des tuples en mentionnant une variable de type variadique à l'intérieur d'une expression entourée d'un opérateur de points de suspension. C'est à peu près une alternative de tuple aux types d'objets mappés.

type Somethify<...T> = [...Smth<T>]
type Test1 = Somethify<[1, 2]> // [Smth<1>, Smth<2>]

type Zip<...T, ...U> = [...[T, U]]
type Test2 = Zip<[1, 2], [3, 4]> // [[1, 3], [2, 4]]

type Flatten<...T extends any[]> = [......T]
type Test3 = Flatten<[[1, 2], [3, 4]]> // [1, 2, 3, 4]

Veuillez mentionner que la syntaxe des points de suspension proposée au lieu de extends any[] est utilisée dans l'exemple non seulement pour des raisons esthétiques, mais parce que

type A<T> = any[]
type B<T extends any[]> = [...A<T>]
type C = B<[1, 2]>

est déjà un programme TS valide. C finit par être any[] au lieu de [any[], any[]] que le type variadique mappé générerait.

@DanielRosenwasser Je m'excuse de vous avoir

En passant, le simple fait d'avoir des opérations de propagation au niveau du type pour les types de tuple serait d'une grande aide pour mon équipe, même si le manque de types variadiques signifie qu'ils ne peuvent pas être utilisés avec des paramètres de type. Dans notre domaine problématique, les "tableaux avec une certaine structure" sont très courants. Cela nous simplifierait grandement les choses si cette opération fonctionnait :

type SharedValues = [S1, S2, S3];
type TupleOfSpecificKind = [V1, ...SharedValues, V2];

@sethfowler si vous avez des exemples de ce que vous voulez exprimer, cela nous est toujours utile. Sinon, vous pourriez être intéressé par https://github.com/microsoft/TypeScript/issues/26113

@DanielRosenwasser Bien sûr, je peux rendre les choses un peu plus concrètes. Je vais garder le détail en dehors de cela, mais à un niveau élevé, vous pouvez considérer notre projet comme générant un flux d'opérations graphiques et d'autres événements similaires qui sont envoyés à un serveur distant. Pour des raisons d'efficacité, nous devons représenter ces opérations en mémoire dans un format directement traduisible dans leur forme sérialisée. Les types de ces événements finissent par ressembler à ceci :

type OpLineSegment = [
  StrokeColor,
  FillColor,
  number,  // thickness
  number, number, number,  // X0, Y0, Z0
  number, number, number  // X1, Y1, Z1
];
type OpCircle = [
  StrokeColor,
  FillColor,
  number, number, number,  // X, Y, Z of center
  number // radius
];
type OpPolygon = (StrokeColor | FillColor | number)[];  // [StrokeColor, FillColor, repeated X, Y, Z]]
type OpFan = (StrokeColor | FillColor | number)[];  // StrokeColor, FillColor, repeated X, Y, Z up to 10x

Nous aimerions pouvoir exprimer ces types plus comme ceci :

type Colors = [StrokeColor, FillColor];
type Vertex3D = [number, number, number];

type OpLineSegment = [...Colors, number /* thickness */, ...Vertex3D, ...Vertex3D];
type OpCircle = [...Colors, ...Vertex3D, number /* radius */];
type OpPolygon = [...Colors, ...Repeated<...Vertex3D>];
type OpFan = [...Colors, ...RepeatedUpToTimes<10, ...Vertex3D>];

Nous avons un grand nombre de ces commandes, donc le simple fait d'avoir une propagation au niveau du type se traduirait par un code considérablement plus maintenable. Avoir des genres variadiques afin que nous puissions écrire des fonctions au niveau du type comme Repeated<> et RepeatedUpToTimes<> (qui évalueraient des unions récursivement définies de types de tuples dans cet exemple) irait encore plus loin pour simplifier les choses.

Il serait également extrêmement utile d'avoir un support pour des choses comme la concaténation de type sûr de types de tuples (comme discuté dans l'OP). Afin d'utiliser les types ci-dessus, nous devons actuellement construire le tuple entier dans une seule expression littérale de tuple. Nous ne pouvons pas le construire en plusieurs parties et les concaténer ensemble maintenant. En d'autres termes, les opérations ci-dessous ne fonctionnent pas aujourd'hui, mais nous souhaitons vraiment qu'elles le fassent.

const colors: Colors = getColors();
const center: Vertex3D = getCenter();

// Doesn't work! Produces a homogenous array.
const circle1: OpCircle = [...colors, ...center, radius];

// Doesn't work; can't write this function today.
const circle2: OpCircle = concat(colors, center, radius);

// We need to do this today; it's quite painful with more complex tuple types.
const circle3: OpCircle = [colors[0], colors[1], center[0], center[1], center[2], radius];

Espérons que ces exemples sont utiles!

Vous pouvez facilement écrire un type Concat<> et créer un type Concat3<> utilisant Concat<> .

Puis,

type OpCircle = Concat3<Colors, Vertex3D, [number] /* radius */>;

À partir de ce qui précède, vous pouvez écrire une fonction concat avec des surcharges pour 2,3,4,5,6, etc. nombre d'arguments.

Il est même possible d'écrire un impl Concat<> qui prend un tuple de tuples et concatène les tuples. Un type var-arg Concat<>.


Ce n'est pas une chose qui ne peut pas être faite aujourd'hui. Cela peut être fait, même si cela vous oblige à écrire un type récursif et une fonction d'assistance.

Chaque fois que j'utilise ces types récursifs dans vscode, TS veut tuer le processeur ou il se bloque ! C'est le problème principal, j'ai l'impression que TS devient trop lourd sans raison valable.

Peut-être que les personnes qui écrivent les types ne font pas assez pour l'optimiser ?

Je ne veux pas spammer ou faire glisser les anciennes conversations qui n'ajouteront pas beaucoup de valeur à ce fil, mais je me souviens à un moment donné d'avoir entendu que le calcul des types conditionnels récursifs est coûteux en javascript (je l'ai trouvé ici dans ce fil )

Cela dit (je sais que cela peut sembler fou), il est peut-être temps de réécrire TS dans un autre langage pour pouvoir donner un coup de pouce au système de types car TS a déjà tellement grandi qu'il est raisonnable d'en demander un meilleur.

Vous pouvez facilement écrire un type Concat<> et créer un type Concat3<> utilisant Concat<> .

Pourriez-vous s'il vous plaît fournir une implémentation pour le type Concat<> que vous décrivez ? C'est facile d'écrire Cons<> , mais Concat<> n'est pas si facile (pour moi) et j'aimerais voir ce que vous envisagez.

Concernant Concat3<> , Concat4<> , etc., l'espoir est qu'à long terme nous n'aurons pas besoin d'écrire des dizaines de variantes comme celles-ci, car nous aurons des types variadiques. 🙂 Si une bonne mise en œuvre d'entre eux est possible aujourd'hui, cependant, ce serait une mesure palliative raisonnable.

Pour une concaténation régulière de deux tuples,
https://github.com/AnyhowStep/ts-trampoline-test (utilise des trampolines pour concaténer de très gros tuples, dont la plupart des gens n'auront pas besoin)

Concat3 serait juste Concat, C>

Le VarArgConcat serait,
VarArgConcat<TuplesT extends readonly (readonly unknown[])[], ResultT extends readonly unknown[] = []>

Tant que le tuple n'est pas vide, VargArgConcat<PopFront<TuplesT>, Concat<ResultT, TuplesT[0]>>

Si TuplesT est vide, renvoie ResultT

Bien sûr, la récursivité naïve entraînera des erreurs de profondeur maximale avec des tuples d'une longueur décente. Donc, soit utilisez la technique de récursivité dans ts-toolbelt, soit utilisez des trampolines avec copier-coller à la profondeur souhaitée


Ce référentiel auquel j'ai lié utilise Reverse<> pour implémenter Concat<> . J'ai copié-collé le code d'un autre projet sur lequel je travaille.

Je suis d'accord que ce serait une fonctionnalité extrêmement utile.

Disons que nous avons un type T :

type T = {
  tags: ["a", "b", "c"];
};

Et nous voulons créer un nouveau type, avec une balise supplémentaire "d" , ajoutée au tuple T["tags"] . Les utilisateurs peuvent d'abord essayer de créer cet utilitaire ( WithTag<NewTag, ApplyTo> ) comme suit :

type WithTag<
  Tag extends string,
  Target extends {tags: string[]}
> = Target & {
  tags: [Tag, ...Target["tags"]];
};

Tenter cela renvoie actuellement l'erreur A rest element type must be an array type . Les utilisateurs peuvent penser qu'échanger string[] contre Array<string> fait une différence, mais ce n'est pas le cas. L'utilisation d'une condition + never :

type WithTag<
  Tag extends string,
  Target extends {tags: string[]}
> = Target & {
- tags: [Tag, ...Target["tags"]];
+ tags: Target["tags"] extends string[] ? [Tag, ...Target["tags"]] : never;
};

Terrain de https://www.typescriptlang.org/play?#code/C4TwDgpgBA6glsAFgFQIYHMA8AoKU3pQQAewEAdgCYDOU1wATnOegDS76oPoTBGkUaUAN7AM1AFx1GzdAG0AugF9sAPigBeTt15QAZCI5j0kqHIKsoAOhtodwOQCJj1RwoUBubEq -ZQkKABJTUM8FyknVEdLRwAjaKhHAGM3Lx9sP3BoZBD4JAJMR0oEwNVfAHoAKkrcSqgAUWJIJLJKKAADZHaoYAB7KFjoXoAzHsRoYd6AGynegHdZHqyrWqhV4VWe8QjHKJj4mJSY4s9VlShK8qA

Problèmes connexes : :

Malheureusement, cette stratégie ne fonctionne pas pour les paramètres de repos. Ceux-ci sont simplement transformés en tableaux :

function i(a: number, b?: string, ...c: boolean[]): number {
}
let curried = curry(i, 12);
curried('foo', [true, false]);
curried([true, false]);

Ici, curry : ...([string, boolean[]] | [boolean[]]) => number .
Je pense que cela pourrait être pris en charge s'il y avait un cas particulier pour les fonctions avec un paramètre de repos de tuple, où le dernier élément du tuple est un tableau.
Dans ce cas, l'appel de fonction permettrait à des arguments supplémentaires du type correct de correspondre au tableau.
Cependant, cela semble trop complexe pour en valoir la peine.

Cela pose deux problèmes :

  1. Le code est incorrect. curried() n'accepte pas de tableau. Remplir le paramètre de repos c avec [true, false] pourrait être fait par curried('foo', ...[true, false]) mais cela échouera dans TypeScript avec cette suggestion. Nous pouvons être en mesure de ne pas fournir de solution de frappe dans certains cas, mais il est déconseillé de fournir une erreur à quelqu'un !
  2. Sans le vouloir, vous avez combiné des paramètres optionnels et de repos, et révélé un bug avec votre proposition. curried() ne peut pas être appelé sans b mais avec c . Cela conduira à une mauvaise conduite. TypeScript sait que curried() est (...items: [string, boolean[]] | [boolean[]]) mais ce n'est tout simplement pas vrai . Parce que JavaScript est sans frappe, passer [true, false] à c (en supposant que nous ayons résolu le problème ci-dessus) avec curried([true, false]) ne définira pas b à undefined (ou sa valeur par défaut) et c à [true, false] , mais définira b à true et c à [false] !

Je propose les correctifs suivants :

  1. Pour le deuxième (et plus simple) problème, la solution est simple : ne pas déduire de tuple union pour le dernier argument optionnel (c'est- [number, string, boolean[]] | [number, boolean[]] dire [number, string, boolean[]] | [number] - c'est-à-dire un cas pour la signature complète, y compris tous les facultatifs et les repos, un pour chaque facultatif sauf le dernier , et un sans le dernier et le reste.
  2. Le premier problème est plus délicat : vous avez déjà dit que vous pensiez que c'était trop complexe pour en valoir la peine. Je pense que cela vaut la peine étant donné la popularité des paramètres de repos, mais c'est _nécessaire_ à cause du premier problème (victoire ! 😄). Je pense que ce serait bien si nous exposions l'interface pour tuple-with-last-rest-array (je pense à la syntaxe [t1, t2, t3, ...arr] ), mais ce n'est pas nécessaire. On peut rester avec ça en interne (haha, il faudra quand même savoir comment afficher le type dans un IDE 😈).

Mais après toutes les plaintes et provocations, belle proposition ! Merci 👍 (juste pour vous apaiser, c'est le premier problème de GitHub auquel j'ai répondu avec trois emojis - 👍, 🎉 et ❤️).

Ce serait vraiment utile dans l'injecteur angulaire qui doit actuellement utiliser any https://github.com/angular/angular/issues/37264

Dans cet exemple, A, B, C pourraient être représentés comme un seul type générique variadique ...A . Mais je n'ai aucune idée de la façon dont cela correspondrait à quelque chose où chaque élément du générique variadique serait enfermé dans un autre type ( Type ). peut-être avec un type d'aide ? Ou la syntaxe devrait-elle autoriser quelque chose comme ...Type<A> ?

export declare interface TypedFactoryProvider<T, A, B, C> {
    provide: Type<T | T[]> | InjectionToken<T | T[]>;
    multi?: boolean;
    useFactory: (a: A, b: B, c: C) => T;
    deps: [Type<A>, Type<B>, Type<C>];
}

(contexte : une implémentation de Provider injecterait des instances de deps dans cette fonction d'usine dans cet ordre. Un typage strict garantirait que le développeur sait ce qui serait injecté et dans quel ordre.)

Lorsque cela est fait, n'oubliez pas de mettre à jour le deuxième paramètre de String.prototype.replace, afin qu'il ait enfin une saisie correcte dans Typescript !

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter

@Griffork Vous vous rendez compte que cela nécessiterait d'analyser l'expression régulière pour déterminer le nombre de groupes de capture dont elle dispose, n'est-ce pas ?

Ce serait vraiment utile dans l'injecteur angulaire qui doit actuellement utiliser any angular/angular#37264

Dans cet exemple, A, B, C pourraient être représentés comme un seul type générique variadique ...A . Mais je n'ai aucune idée de la façon dont cela correspondrait à quelque chose où chaque élément du générique variadique serait enfermé dans un autre type ( Type ). peut-être avec un type d'aide ? Ou la syntaxe devrait-elle autoriser quelque chose comme ...Type<A> ?

export declare interface TypedFactoryProvider<T, A, B, C> {
  provide: Type<T | T[]> | InjectionToken<T | T[]>;
  multi?: boolean;
  useFactory: (a: A, b: B, c: C) => T;
  deps: [Type<A>, Type<B>, Type<C>];
}

(contexte : une implémentation de Provider injecterait des instances de deps dans cette fonction d'usine dans cet ordre. Un typage strict garantirait que le développeur sait ce qui serait injecté et dans quel ordre.)

@AlexAegis

Je pense qu'il serait tapé quelque chose comme ceci:

export declare interface TypedFactoryProvider<T, ...P> {
  provide: Type<T | T[]> | InjectionToken<T | T[]>;
  multi?: boolean;
  useFactory: (...providers: ...P) => T;
  deps: [...Type<P>];
}

Ce problème est maintenant résolu par #39094, prévu pour TS 4.0.

Si cela arrive avec 4.0, nous avons maintenant une raison de le nommer 4.0 😃
C'est vraiment une nouveauté majeure

C'est bien! La seule chose "à gauche" est la même pour les types de chaîne littéraux

@sandersn J'essaie de réfléchir à la façon dont cette syntaxe serait utilisée dans des choses comme RxJS , où les paramètres de la méthode pipe sont en quelque sorte dépendants les uns des autres,

comme dans pipe(map<T, V>(...), map<V, U>(...), filter(...), ...) . Comment le taperiez-vous d'une manière qui n'est pas ce qu'ils font maintenant ? (Les dizaines de lignes de différentes longueurs variadiques tapant)

@gioragutt utilisant le PR que @ahejlsberg a soumis, je pense que cela fonctionnerait mais je peux me tromper 😄

type Last<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;

interface UnaryFunction<T, R> { (source: T): R; }

type PipeParams<T, R extends unknown[]> = R extends readonly [infer U] ? [UnaryFunction<T, U>, ...PipeParams<R>] : [];

function pipe<T, R extends unknown[]>(...fns: PipeParams<T, R>): UnaryFunction<T, Last<R>>;

@tylorr Ne fonctionne pas tout à fait, en raison d'une erreur de type circulaire.

Cependant, la solution de contournement habituelle fonctionne .

type Last<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;

interface UnaryFunction<T, R> { (source: T): R; }

type PipeParams<T, R extends unknown[]> = {
    0: [],
    1: R extends readonly [infer U, ...infer V]
    ? [UnaryFunction<T, U>, ...PipeParams<U, V>]
    : never
}[R extends readonly [unknown] ? 1 : 0];

declare function pipe<T, R extends unknown[]>(...fns: PipeParams<T, R>): UnaryFunction<T, Last<R>>;

@isiahmeadows Cela ne semble pas fonctionner pour moi. ??
Exemple de terrain de jeu .

J'ai quelque chose de plus proche du travail mais cela ne déduira pas les types.
exemple de terrain de jeu

j'ai dû changer
R extends readonly [unknown] ? 1 : 0
à
R extends readonly [infer _, ...infer __] ? 1 : 0

Pas certain de pourquoi

@tylorr @treybrisbane Peut être lié : https://github.com/microsoft/TypeScript/pull/39094#issuecomment -645730082

De plus, dans les deux cas, je recommande fortement de partager cela dans la demande d'extraction dans laquelle se trouve le commentaire.

Les types de tuples variadiques sont un ajout formidable au langage, merci pour l'effort !

Il semble que des constructions comme curry pourraient également en bénéficier (juste testé avec le terrain de jeu de mise en scène ):

// curry with max. three nestable curried function calls (extendable)
declare function curry<T extends unknown[], R>(fn: (...ts: T) => R):
  <U extends unknown[]>(...args: SubTuple<U, T>) => ((...ts: T) => R) extends ((...args: [...U, ...infer V]) => R) ?
    V["length"] extends 0 ? R :
    <W extends unknown[]>(...args: SubTuple<W, V>) => ((...ts: V) => R) extends ((...args: [...W, ...infer X]) => R) ?
      X["length"] extends 0 ? R :
      <Y extends unknown[]>(...args: SubTuple<Y, X>) => ((...ts: X) => R) extends ((...args: [...Y, ...infer Z]) => R) ?
        Z["length"] extends 0 ? R : never
        : never
      : never
    : never

type SubTuple<T extends unknown[], U extends unknown[]> = {
  [K in keyof T]: Extract<keyof U, K> extends never ?
  never :
  T[K] extends U[Extract<keyof U, K>] ?
  T[K]
  : never
}

type T1 = SubTuple<[string], [string, number]> // [string]
type T2 = SubTuple<[string, number], [string]> // [string, never]

const fn = (a1: number, a2: string, a3: boolean) => 42

const curried31 = curry(fn)(3)("dlsajf")(true) // number
const curried32 = curry(fn)(3, "dlsajf")(true) // number
const curried33 = curry(fn)(3, "dlsajf", true) // number
const curried34 = curry(fn)(3, "dlsajf", "foo!11") // error

La fonction générique ne fonctionne pas avec le curry ci-dessus.

Je ne crois pas que ce PR résout ce problème particulier tbh.

Avec le PR ça marche

function foo<T extends any[]>(a: [...T]) {
  console.log(a)
}

foo<[number, string]>([12, '13']);

Mais ce problème aimerait voir une implémentation pour cela pour autant que je vois:

function bar<...T>(...b: ...T) {
  console.log(b)
}

bar<number, string>(12, '13');

Il y a beaucoup d'équerres là-bas, ça a l'air un peu redondant.

@AlexAegis Je ne suis pas sûr de voir beaucoup de valeur dans les "paramètres de type repos" comme ça. Tu peux déjà faire ça :

declare function foo<T extends any[]>(...a: T): void;

foo(12, '13');  // Just have inference figure it out
foo<[number, string]>(12, '13');  // Expclitly, but no need to

Ne pensez pas que nous voulons vraiment un tout nouveau concept (c'est-à-dire des paramètres de type repos) juste pour que les crochets puissent être évités dans les rares cas où l'inférence ne peut pas le comprendre.

@ahejlsberg je vois. Je demandais parce que certaines bibliothèques (RxJS comme mentionné) utilisaient des solutions de contournement pour fournir cette fonctionnalité. Mais c'est fini.

bar<T1>(t1: T1);
bar<T1, T2>(t1: T1, t2:T2);
bar<T1, T2, T3>(t1: T1, t2:T2, t3: T3, ...t: unknown) { ... }

Alors maintenant, ils s'en tiennent à cela ou demandent aux utilisateurs de taper les crochets, ce qui est un changement décisif, et pas si intuitif.

La raison pour laquelle j'ai utilisé cet exemple est qu'ici il est simple de définir le type de ce tuple. Un crochet ici, un là

foo<[number, string]>([12, '13']);

Ici, il n'est pas si évident que le tuple se réfère à ce paramètre de repos si vous le regardez de l'extérieur

foo<[number, string]>(12, '13'); 

Mais oui, comme vous l'avez dit, si nous laissons l'inférence le comprendre, ces cas triviaux ne nécessitent aucune modification de la part de l'utilisateur. Mais nous ne savons pas s'ils les ont définis explicitement ou non, c'est à eux de décider, donc cela compte toujours comme un changement décisif. Mais c'est la préoccupation de la lib et non de ce changement.

Cela dit, je trouve juste étrange que s'il existe des paramètres de repos, définis de l'extérieur un par un, qui sont un seul tableau à l'intérieur différencié par ... , ne puissent pas être rendus génériques de la même manière: un par un à l'extérieur, un seul tableau à l'intérieur, différencié par ... .

Les divergences de syntaxe mineures ne valent pas vraiment le coût de support pour un
type. L'utilisation de types serait une décision de conception correcte lorsque TS planifiait
prise en charge des paramètres de repos, mais je suppose que maintenant cela pourrait conduire à plus
confusion à la fois pour les développeurs de langages et les utilisateurs. Nous avions besoin d'une solution pour
ce problème, et Anders a fait son travail exceptionnellement bien en évitant que
complexité en s'en tenant à [...T] au lieu de T . Chapeau bas !

(Pourrions-nous maintenant examiner un bogue qui unifie le type d'intersection à
la variable inférée dans le type conditionnel renvoie le type d'intersection le plus à droite
argument, ou cette union de tableaux n'est pas un tableau d'union s'il vous plaît? Nous avons encore
ont des éléments majeurs dans le système de type.)

Le vendredi 19 juin 2020 à 10 h 41, Győri Sándor [email protected] a écrit :

@ahejlsberg https://github.com/ahejlsberg Je vois. je demandais parce que
certaines bibliothèques (RxJS comme mentionné) ont utilisé des solutions de contournement pour fournir cela
Fonctionnalité. Mais c'est fini.

bar(t1 : T1) ;barre(t1 : T1, t2 :T2) ;barre(t1 : T1, t2 : T2, t3 : T3, ...t : inconnu) { ... }

Alors maintenant, soit ils s'en tiennent à cela, soit les utilisateurs tapent les crochets,
ce qui n'est pas si intuitif.

La raison pour laquelle j'ai utilisé cet exemple est qu'ici c'est simple
que j'ai défini le type de ce tuple. Un crochet ici, un là

foo<[nombre, chaîne]>([12, '13']);

Ici, il n'est pas si évident que le tuple se réfère à ce paramètre de repos si
tu le regardes de l'extérieur

foo<[nombre, chaîne]>(12, '13');

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646490130 ,
ou se désinscrire
https://github.com/notifications/unsubscribe-auth/AAWYQIMTTB6JEPSQFUMTMDTRXMJD5ANCNFSM4BTBQ7DQ
.

Je suis bien sûr loin de son calibre, mais je suis respectueusement en désaccord avec @ahejlsberg .
D'après mon expérience, une grande partie de la complexité du dactylographe vient du fait que de nombreuses fonctionnalités (intéressantes et utiles, bien sûr) sont dans un cas particulier en tant que leurs propres concepts.

Cette complexité n'est cependant pas intrinsèquement fonction du nombre de fonctionnalités !
Au lieu de cela, le langage pourrait être conçu autour de concepts plus larges et plus généraux à partir desquels ces cas particuliers pourraient ensuite être déduits de manière triviale, ou implémentés dans la bibliothèque std (type).

Le concept le plus général de ce type serait bien sûr d'implémenter entièrement des types dépendants, à partir desquels tout le reste pourrait alors être dérivé, mais il n'est pas nécessaire d'aller aussi loin :
Comme C++ et, dans une moindre mesure, Rust l'ont montré, quelques concepts cohérents à grande échelle vous offrent une tonne de fonctionnalités gratuitement.
C'est similaire à ce qu'OCaml et Haskell (et je suppose que F# ?) ont fait au niveau de la valeur, juste au niveau du type.

La programmation au niveau du type n'a rien à craindre tant qu'elle est conçue dans le langage au lieu d'être ajoutée pour fournir des fonctionnalités spécifiques.
Les fonctionnalités en C++ 14/17 sont très intuitives à l'exception de leur syntaxe, qui est purement due à un bagage historique.

Des concepts généraux auraient pu être ajoutés dans la conception originale. Après conception
erreur a déjà été commise, la cohérence ne peut être ajoutée sans risquer d'énormes
rétro-incompatibilité. Je suis d'accord avec les soupçons concernant la conception du langage en tant que
un tout (TS est assez loin des normes établies par le monde universitaire, personne ne peut
pas d'accord avec ça). Il y a beaucoup de bugs et d'incohérences qui sont
à la base de millions de bases de code de production. Le simple fait que
les développeurs sont capables de proposer des ajouts utiles au langage
sans corriger accidentellement ces bugs est, à mon humble avis, génial
et mérite le respect. TS a les mêmes complexités de conception que C++ ici, mais son
système de type expressif aggrave la situation.

Le vendredi 19 juin 2020 à 12 h 47, Bennett Piater [email protected] a écrit :

Je suis bien sûr loin de son calibre, mais je suis respectueusement en désaccord
avec @ahejlsberg https://github.com/ahejlsberg .
D'après mon expérience, une grande partie de la complexité du tapuscrit vient de lafait que beaucoup de fonctionnalités (intéressantes et utiles bien sûr) sontcas particulier comme leurs propres concepts.

Cette complexité n'est pas intrinsèquement fonction du nombre de caractéristiques
bien que!
Au lieu de cela, le langage pourrait être conçu autour d'un langage plus large et plus global.
concepts à partir desquels ces cas particuliers pourraient alors être trivialement déduits, ou
implémenté dans la bibliothèque std (type).

Le concept le plus général de ce type serait bien entendu de mettre pleinement en œuvre
types dépendants, à partir desquels tout le reste pourrait alors être dérivé, mais
aller aussi loin n'est pas nécessaire :
Comme C++ et, dans une moindre mesure, Rust ont montré, à grande échelle,
des concepts cohérents vous offrent une tonne de fonctionnalités gratuitement.
C'est similaire à ce qu'OCaml et Haskell (et je suppose que F# ?) ont fait sur
le niveau de la valeur, juste au niveau du type.

La programmation au niveau du type n'a rien à craindre tant qu'elle est
conçu dans la langue au lieu d'être ajouté pour fournir des
caractéristiques.
Les facilités en C++ 14/17 sont très intuitives à part leur syntaxe,
ce qui est purement dû au bagage historique.

-
Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646543896 ,
ou se désinscrire
https://github.com/notifications/unsubscribe-auth/AAWYQIMWYLGGCWPTDBZJR4TRXMX4RANCNFSM4BTBQ7DQ
.

@polkovnikov-ph Je suis content que nous soyons d'accord sur le problème en question :)

Quant à la solution, je pense qu'il serait toujours intéressant d'envisager d'évoluer progressivement vers un système de types plus soigneusement conçu. Les versions majeures sont une chose après tout, et l'alternative est de se retrouver dans le cluster * * c'est-à-dire C++ 20 - une tentative admirable d'ajouter des fonctionnalités encore plus bien conçues au-dessus de 2 couches de tentatives précédentes qui ne peuvent pas être supprimées, dans un syntaxe qui n'est déjà pas analysable de manière déterministe.

Tout cela est hors sujet de ce fil et est discuté ici . Alors je vais essayer d'être franc :

Il a fallu des décennies aux universitaires pour trouver une approche correcte du sous-typage : le système de type mlsub a été créé il y a seulement 6 ans, bien après la sortie de TypeScript. Cela pourrait être cette base pour les classes, les interfaces, les types d'union et d'intersection avec des concepts globaux.

Mais n'oubliez pas qu'il existe des types conditionnels. Je ne connais aucun article leur donnant une sémantique formelle ou décrivant un système de types minimal avec des types conditionnels avec des preuves de progression/préservation. Je pense que cela pourrait avoir quelque chose à voir avec le fait que les scientifiques hésitent toujours à imprimer leurs tentatives infructueuses. Si votre proposition suppose que ces versions incompatibles majeures seront faites dans les années 2040, lorsque le monde universitaire se sera familiarisé avec les types conditionnels, je peux être d'accord.

Sinon, "un système de types soigneusement conçu" devrait supprimer les types conditionnels du langage, et je ne pense pas que quiconque soit à la hauteur de la tâche de convertir 60% de DefinitelyTyped pour utiliser l'alternative choisie pour les remplacer. (Et puis recommencez plusieurs fois, car ce n'est pas le seul problème.)

Je crains que la seule solution viable ne soit de créer un langage de programmation distinct qui ressemblerait d'une manière ou d'une autre à TS, et d'une manière ou d'une autre (pas seulement en étant plus agréable à écrire du code) d'inciter les développeurs à l'utiliser. Ryan a été assez bruyant en recommandant cette approche pour l'amélioration du TS auparavant.

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