Typescript: 提案:Variadic Kinds -- 为可变参数函数指定特定类型

创建于 2015-10-29  ·  265评论  ·  资料来源: microsoft/TypeScript

可变类型

为可变参数函数指定特定类型

该提议让 Typescript 为采用可变数量参数的高阶函数提供类型。
像这样的函数包括concatapplycurrycompose以及几乎所有包装函数的装饰器。
在 Javascript 中,这些高阶函数应该接受可变参数函数作为参数。
在 ES2015 和 ES2017 标准中,随着程序员开始对数组和对象使用扩展参数和剩余参数,这种用法将变得更加普遍。
该提案使用基于高阶种类的单一、非常通用的类型策略来解决这些用例。

该提案将完全或部分解决几个问题,包括:

  1. #5331 -- 元组作为休息的类型......参数
  2. #4130 -- 编译器在使用扩展运算符时错误地报告参数/调用目标签名不匹配
  3. #4988 -- 元组应该可以用 Array.prototype.slice() 克隆
  4. #1773 -- 可变泛型?
  5. #3870 -- 交叉类型泛型中的休息类型。
  6. #212 -- bind、call 和 apply 是无类型的(需要 #3694 的 this-function 类型)。
  7. #1024 -- 带泛型的类型化 ...rest 参数

我将在我的 Typescript-Handbook 的分支上更新这个提议:sandersn/ TypeScript-Handbook@76f5a75868de3fb1ad4dbed5db437a8ab61a2698
我在 sandersn/ TypeScript@f3c327aef22f6251532309ba046874133c32f4c7有一个正在进行的实现,它目前已经实现了提案的简单部分。
它取代了我之前的提案#5296 的第 2 部分。
编辑:添加了关于可分配性的部分。 我不再确定它是否严格取代 #5296。

使用curry预览示例

带有两个参数的函数的curry很容易用 Javascript 和 Typescript 编写:

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

并在带有类型注释的 Typescript 中:

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

但是,可变参数版本很容易用 Javascript 编写,但不能在 TypeScript 中指定类型:

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

这是使用此提案中的可变参数类型键入curry的示例:

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

我在这里使用的可变参数元组类型的语法与 Javascript 中用于值的传播和剩余语法相匹配。
这更容易学习,但可能会使区分类型注释和值表达式变得更加困难。
类似地,连接的语法看起来像元组构造,即使它实际上是两个元组类型的连接。

现在让我们看一个调用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')();

在第一次通话中,

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

在第二次通话中,

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

句法

可变参数类型变量的语法是...T ,其中 _T_ 是一个标识符,按照惯例是单个大写字母,或者T后跟PascalCase标识符。
可变类型变量可用于许多句法上下文:

可变类型可以绑定在类型参数绑定的常用位置,包括函数和类:

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

并且它们可以在任何类型注释位置被引用:

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

可变类型变量,就像类型变量一样,非常不透明。
与类型变量不同,它们确实只有一个操作。
它们可以与其他类型或实际元组连接。
用于此的语法与元组扩展语法相同,但在类型注释位置:

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

元组类型是可变参数类型的实例,因此它们继续出现在以前允许类型注释的任何地方:

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

语义

可变参数类型变量表示任意长度的元组类型。
由于它代表一组类型,我们使用术语“种类”来指代它,遵循它在类型理论中的用法。
因为它代表的类型集是任意长度的元组,所以我们用“可变参数”来限定“种类”。

因此,声明可变元组类型的变量允许它采用任何 _single_ 元组类型。
与类型变量一样,种类变量只能声明为函数、类等的参数,然后允许它们在主体内部使用:

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

调用参数类型为可变参数类型的函数将为该类型分配特定的元组类型:

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

分配元组类型...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` 是允许的。
例如,可以向元组添加新元素:

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

与类型变量一样,通常可以推断可变类型变量。
cons的调用可以被注释:

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

例如, cons必须推断两个变量,类型 _H_ 和类型 _...Tail_。
在最里面的调用中, cons("foo", ["baz", false])H=string...Tail=[string,boolean]
在最外面的调用中, H=number...Tail=[string, string, boolean]
分配给 _...Tail_ 的类型是通过将列表文字作为元组键入来获得的——也可以使用元组类型的变量:

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

此外,当与类型连接时,可以推断可变类型变量:

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

这里, l被推断为[number, string, boolean]
然后H=number...Tail=[string, boolean]

类型推断的限制

无法推断连接类型,因为检查器无法猜测两种类型之间的边界应该在哪里:

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

检查者无法决定是否分配

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

一些明确的调用是此限制的牺牲品:

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

解决方法是添加类型注解:

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

可能会出现类型参数和函数体之间不可检查的依赖关系,如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);

此函数可以键入,但n和种类变量之间存在依赖关系: n === ...T.length必须为真,类型才能正确。
我不确定这是否是实际上应该允许的代码。

类和接口的语义

类和接口的语义相同。

TODO:语义中可能存在一些特定于类的皱纹。

元组和参数列表之间的可分配性

元组类型可用于提供一种类型以在其范围内休息函数的参数:

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

在此示例中, f: (a: number, b:string) => string的参数列表必须可分配给为类型...T实例化的元组类型。
推断的元组类型是[number, string] ,这意味着(a: number, b: string) => string必须可分配给(...args: [number, string]) => string

作为副作用,即使函数没有元组类型,函数调用也可以通过将元组扩展到其余参数来利用这种可分配性:

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

为可选和其余参数生成的元组类型

由于元组不能直接表示可选参数,当一个函数被分配给一个由元组类型类型化的函数参数时,生成的元组类型是元组类型的联合。
查看一下h被柯里化后的类型:

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

这里...T=([number] | [number, string]) ,所以curried: ...([number] | [number, string]) => number可以按照您的预期调用。 不幸的是,此策略不适用于其余参数。 这些只是变成了数组:

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

在这里, curried: ...([string, boolean[]] | [boolean[]]) => number
我认为如果有一个带有元组 rest 参数的函数的特殊情况,元组的最后一个元素是一个数组,那么这可以得到支持。
在这种情况下,函数调用将允许正确类型的额外参数与数组匹配。
然而,这似乎太复杂了,不值得。

对打字稿其他部分的扩展

  1. Typescript 不允许用户编写空元组类型。
    然而,这个提议要求可变参数类型可以绑定到一个空元组。
    所以 Typescript 需要支持空元组,即使只是在内部。

    例子

在当前的 Typescript 中,这些示例中的大多数都可以作为固定参数函数,但是根据这个提议,它们可以写为可变参数。
有些,如consconcat ,可以为当前 Typescript 中的同构数组编写,但现在可以使用元组类型为异构元组编写。
这更接近于典型的 Javascript 实践。

返回连接类型

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]

连接类型作为参数

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"

可变参数函数作为参数

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

TODO: f返回...U而不是U吗?

装饰器

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

开放问题

  1. 元组到参数列表的可分配性故事是否成立? 它在 optional 和 rest 参数周围尤其不稳定。
  2. 推断类型是否与可选参数情况一样是元组的并集? 因为bindcallapply是在 Function 上定义的方法,它们的类型参数需要在函数创建时绑定,而不是bind调用站点(例如)。 但这意味着具有重载的函数不能采用或返回特定于其参数的类型——它们必须是重载类型的联合。 此外,Function 没有直接指定类型参数的构造函数,因此实际上无法向bind等人提供正确的类型。 TODO:在此处添加示例。 请注意,此问题不一定是可变参数函数所独有的。
  3. 其余参数是否应该特殊情况以保留其良好的调用语法,即使它们是从元组类​​型生成的? (在这个提议中,元组类型的函数必须将数组传递给它们的其余参数,它们不能有额外的参数。)
Fix Available In Discussion Suggestion

最有用的评论

此问题现已由 #39094 修复,预定用于 TS 4.0。

所有265条评论

+1,这对于 TypeScript 中的函数式编程非常有用! 这将如何与可选或其余参数一起使用? 更具体地说, compose函数可以用于带有剩余参数或可选参数的函数吗?

好点子。 我认为您可以将允许的最小元组类型分配给可选参数函数,因为元组只是允许其他成员的对象。 但这并不理想。 我会看看我是否能找出compose例子,然后我会更新提案。

实际上联合类型可能会更好地工作。 就像是

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

尽管如此,这仍然会破坏休息参数。

@ahejlsberg ,我认为您对元组类型的工作方式有一些想法。

所以:+1:关于这个。 有关信息,这与(并将实现)#3870 相关。 我们尝试在 TypeScript 中实现组合类型 API ,但不得不解决此提案中提到的一些限制。 这肯定会解决其中的一些问题!

似乎有时您可能想要“合并”这样的元组类型而不是持久化它们,尤其是像 compose 这样的东西。 例如:

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

此外,在您的许多示例中,您一直在使用原语。 您如何看待更复杂的工作,尤其是在存在冲突的情况下?

不幸的是,这个提议没有解决 #3870 或类型组合,因为元组类型的唯一组合运算符是[T,...U] 。 你也可以把它写成T + ...U (这更能说明类型发生了什么),但是 #3870 和你的类型组合库需要T & ...U 。 我认为这可能是可能的,但我需要首先从 #3870 中了解@JsonFreeman@jbondc的想法。 如果我能弄清楚它应该如何运作,我将扩展该提案。

注意:我决定使用[...T, ...U]语法,因为它看起来像等价的值传播语法,但T + ...U更能说明类型发生了什么。 如果我们最终得到两者,那么+&可能是要使用的运算符。

大:+1:在这个!

+1 真棒! 它将允许表达这些东西更具表现力和轻量级。

我在 #3870 中的观点在这里似乎是一个问题。 具体来说,我担心为可变类型参数推断类型参数。

类型参数推断是一个相当复杂的过程,它随着时间的推移发生了微妙的变化。 当参数与参数匹配以推断类型类型参数时,不能保证推断候选的顺序,也不能保证推断出多少候选(对于给定的类型参数)。 这通常不是问题,因为呈现给用户的结果(在大多数情况下)不会暴露这些细节。 但是如果你从推理结果中创建一个元组类型,它肯定会暴露推理的顺序和计数。 这些细节不打算被观察到。

这有多严重? 我认为这取决于推理的工作方式。 下面的结果是什么:

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

@jbondc-似乎是个好主意。 我会记住它但不在这里探索它,因为我认为我们应该一次引入一个新的类型运算符。 &+创建了新类型,但&创建了一个交集类型,而+创建了一个新的元组类型(这就是为什么我更喜欢语法[T,...U]而不是T + ...U ,因为[T,U]已经为类型这样做了)。

@JsonFreeman我认为用重复的种类参数做两件事之一是可以的:

  1. 联合类型: f(['hello', 1], [1, false]): [string | number, number | boolean]
  2. 不允许推断重复的元组类型参数,特别是如果类型参数推断证明很复杂。 像这样的东西:
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

我认为真正的库(比如链接到的反应式扩展@Igorbek )通常只有一个元组类型的参数,所以即使 (1) 和 (2) 都不是特别可用,它也不会对现实世界的代码产生太大影响。

在上面的例子中, curry是最难推断的——你必须跳过f: (...args:[...T,...U]) => V ,推断...ts:...T ,然后返回并将...U设置为什么从f的参数中消耗...T离开。

我已经开始对此进行原型设计(sandersn/TypeScript@1d5725d),但还没有那么远。 知道这是否有效吗?

我会错误地禁止任何语义不清楚的事情(例如对相同的扩展类型参数的重复推断)。 这也减轻了我上面的担忧。

我想不出一个好的输入咖喱的机制。 正如您所指出的,您必须跳过第一个函数的参数列表来使用...T参数,然后查看剩下的内容。 如果扩展类型参数在其列表中不是最终的,则必须有一些策略来推迟对扩展类型参数的推断。 它可能会变得混乱。

也就是说,我认为这值得一试。 对该功能的需求很高。

我认为您必须跳过出现在同一上下文中的多个元组类型(例如,像(...T,string,...U) => V这样的顶级或像[...T,...U,...T] )。 然后您可以对跳过的种类进行多次传递,消除已经推断的种类并重新跳过仍然不明确的种类。 如果在任何时候没有单一种类可用于推理,请停止并返回错误。

是的。 复杂的。

您或许可以从类似的问题中汲取灵感。 它实际上有点类似于推断并集或交集的问题。 当推断包含作为推断上下文成员的类型参数的联合类型时,如在function f<T>(x: T | string[]) ,您不知道是否推断 T。联合类型的预期表现形式可能是string[] 。 所以打字稿首先推断所有其他成分,然后如果没有推断,则推断 T。

在交集的情况下,它甚至更难,因为您可能必须在不同的交集成分之间拆分参数的类型。 Typescript 根本不会对交集类型进行推断。

如果你只允许传播元组,如果它是其序列中的最后一个类型呢? 所以[string, ...T]会被允许,但[...T, string]不会?

如果我理解正确,这实际上可以解决 TypeScript 中的 mixin 故事。 我的理解正确吗?

也许。 能给我举个例子吗? 我不熟悉混合模式。

可变参数类型变量的语法是 ...T,其中 T 是一个标识符,按照惯例是单个大写字母,或者 T 后跟 PascalCase 标识符。

我们可以将类型参数标识符的情况留给开发人员吗?

@aleksey-bykov +1。 我看不出不应该是这样的原因。

具有 Haskell 背景的开发人员会很感激。

对不起,这句话可以被歧义地解析。 我的意思是“或”来严格解析:“按照惯例(单个大写字母 || T 后跟 PascalCase 标识符)”。 我不是提议限制标识符的情况,只是指出惯例。

不过,就其价值而言,_I_ 有 Haskell 背景,而且我不喜欢打破我所用语言的惯例。

抱歉出轨了。 我最后一个好奇的问题(如果你不介意我问的话)可能会被打破的 TypeScript 的“约定”是什么,谁会担心?

@sandersn

这应该进行类型检查,假设T & ...U表示T & U & V & ... (这是直观的行为)。

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

或者在定义文件中:

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

@aleksey-bykov 我正在谈论的约定是类型参数标识符的情况。 谁在担心? 必须阅读他们以前从未见过的新 Typescript 代码的人——约定帮助新读者更快地理解新代码。

@sandersn @aleksey-bykov 的印象是以下内容在语法上是无效的:

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

@isiahmeadows &|对种类的操作不在本提案中,尽管如果我没有,我应该将它们添加到未解决的问题/未来的工作中。 现在唯一建议的运算符是串联: [THead, ...TTail]

一个区别是串联仍然产生元组类型,而&|分别产生交集和联合类型。

@sandersn我在 TypeScript 中的assign示例对此进行更改将是微不足道的。

虽然:

  1. 交集类似于连接,尽管它更像是连接字典而不是连接列表。 可变类型可能会在现有机器之上实现。
  2. 联合就像交集,除了它只保留公共部分。 再一次,可变参数类型可能会在现有机器之上实现。

@isiahmeadows交集通常不是字典的串联。 这仅适用于对象类型的交集,但不适用于联合的交集。 联合也不同于仅仅采用对象的共同属性。 两者的更好特征在于包含它们的一组值。

@sandersn我对可变参数类型的类型参数推断有点困惑。 这里应该推断出什么?

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

结果是[string, number, number[]]吗? 这意味着您必须依赖类型参数推断以从左到右的顺序添加候选,这不是一个微不足道的假设。 这也是类型系统第一次向用户展示推理候选列表。

我知道这是一个实验性/早期的提议,但我们可以讨论...T其余参数的语法。 从我的角度来看,它并没有真正起作用。
所以建议的语法是:

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

让我们与其余参数的现有语法进行比较:

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

所以捕获其余参数的a参数的类型是number[] ,所以我们可以清楚地理解它是一个数组。 以此类推,我可以推断提案中的...T代表一个数组。 但这不是很明显。
接下来,假设我们可以定义更多限制性的休息参数:

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

所以现在,我们仍然看到a是一个元组(它是一个数组)。

我的建议是使用更一致的方式将...T概念表示为“某种抽象的有序类型列表”。 并以与我们使用扩展运算符相同的方式使用它:

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

所以...a在变量的情况下,就是1, "1"

我通过...T概念定义其余参数的语法:

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

对我来说,这更有意义。

@Igorbek我一直在假设declare function f<...T>(...a: ...T);已经这样工作了。 但我没有看到declare function f(...a: [number, string]);有多大用处。

要更清楚。

最初建议的其余参数语法:

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

如果我能做到这一点

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

那么我将能够做到这一点:

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

所以a[...T] (我们是这样返回的),但是我们在签名中将它定义为...T
我们可以说...T[...T]是相同的,但它不适用于变量。
对于变量:

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

如果我们将相同的应用于标准休息参数

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

anumber[] (按返回类型),与签名中定义的相同。

@isiahmeadows是的, function f(...a: [number, string])不起作用。 我刚刚开始思考我们如何处理休息参数。

所以,更进一步。 为了显式定义类型参数,提出了以下语法:

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

变成:

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

所以这也可能有效:

function g<T1, T2, T3>()

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

@JsonFreeman这就是我的原型的工作方式,是的。 但我对类型推断算法不够熟悉,无法理解它为什么起作用。 换句话说,问题不在于从左到右的推理是否是一个_平凡的_假设,而是一个正确的假设。 对于身份案例,答案是肯定的,但我不知道您是否可以构建答案是否定的案例。

您还可以通过一个公开的类型推断候选集的示例吗? 就像我说的,我不太了解推理算法的工作原理,所以举个例子可以帮助我理解你的意思。

甚至更好:

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

我建议在类型标识符前加上 [] 前缀来表示其余的类型参数。

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

它比...T短 1 个字符,并且(在我看来)减少了视觉噪音。

@aleksey-bykov 我实际上对此持相反意见。 它不符合现有的 rest 参数语法,所以我相信它也不太清楚。

[...T] / T作为休息数组参数类型对我来说似乎要好得多。 再次与数组及其 sprad 运算符进行比较:

| 数组 | 类型(来自提案)| 类型(我的更新)|
| --- | --- | --- |
| var x = [1,2] | 没有| T = [T1, T2] |
| [0, ...x] === [0,1,2] | [T0, ...T] === [T0, T1, T2] | [T0, ...T] === [T0, T1, T2] |
| f(x) === f([1, 2]) | 没有| 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> |

来自提案

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
}

从我的更新

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

更新现在看起来更好 IMO。 表示类型的列表听起来很不错,但即使是类型化的 Lisps 也没有那么远(同形类型,有人吗?:smile:)。

我得到了纯洁的魅力,但我也在考虑务实的方面。 列表本身也相对容易实现,但它不适合语言的其余部分。 这几乎就像在 Java(语言)中实现 monads 或在 C 中实现 lambdas 的无数尝试一样 - 它们总是令人难以置信的丑陋和骇人听闻。

@sandersn我可以尝试通过公开候选人名单来解释我的意思。 类型参数推断为每个类型参数生成一个候选列表。 然后它检查是否有任何候选人是所有其他候选人的超类型,如果是,则该候选人是赢家。 所以在下面的例子中:

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

参数将被输入,然后推断到每个参数。 将生成两个候选对象,即(string | number)[]string[] 。 但是第一个会赢,因为它是第二个的超类型。 结果,用户永远不会观察到string[]曾经出现在图片中。 T有一个推论,所有其他候选者都是不可见的。 这意味着有两件事对用户是不可见的,即候选的顺序和候选的多重性。

如果您依赖候选列表作为由...T表示的元组中的元素列表,则存在多重性问题:

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

考虑到我所理解的提案的意图,我认为您希望为 T 推断[number, number] 。 但是由于包含签入行https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts#L6256number候选将只添加一次,而T将被推断为[number] 。 这就是我所说的多重性问题。

至于顺序,是从左到右。 但是有多次传递,如果参数包含上下文类型的函数表达式,则参数将被重新处理。 如果有 n 个参数包含上下文类型的函数表达式,则有 n + 1 次传递参数。 一个例子是 Array.prototype.reduce,其中的 initialValue 参数在回调之前被有效地输入和推断,尽管它在右边。 因此,类似以下内容可能是提案的问题:

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

直观地, T 应该是[(x: any) => any, number] ,但如果您依赖于添加候选人的顺序,它将是[number, (x: any) => any] 。 这是因为类型参数推断通常是从左到右的,但受上下文类型影响的函数被推迟到最后。

我所解释的多重性和顺序问题都是出现候选列表的实例。 @ahejlsberg肯定也是一个询问这个问题的好人,他确实可以帮助解释、确认或反驳我所说的任何事情。

@JsonFreeman为什么你认为这是一个问题?
它可以通过为每个其余的事实参数虚拟引入额外的泛型类型并推断具有固定参数长度的函数来实现。
例如,

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]

顺便说一句,我们可以推断x => x{ <T>(x: T): T; }吗?

@Igorbek我认为您关于制造类型参数的建议(至少作为直觉,无论它是如何实现的)是正确的方法。 您可以推断T的类型序列,其中序列中的每个元素都有一个索引和一个候选列表(这是实现您提到的内容的另一种方法)。

但是,我的观点是,如果您只是将推理候选列表重新用作推理元组,我认为这不会自然发生。 它需要明确的机制来使正确的事情发生。

对于您关于{ <T>(x: T): T; }观点,这不能很好地概括为键入诸如x => foo(x) ,其中 foo 是某个函数。 您需要知道x才能对foo进行重载解析。

从类型检查器推理规则的战​​斗中迈出一小步。
我有一个关于语法的评论/建议。 我认为有两个一致但相互排斥的选择:

1. 正式的休息类型参数

如果我们选择这种形式:

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

那么我们应该像这样使用它

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

因此,它将是真正的休息形式类型。 它们应该只在形式类型参数部分的最后一个位置使用。

2. 元组类型的休息参数

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

在这种情况下,我们在形式类型参数部分总是有固定数量的插槽。

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

如果满足任一条件,我们推断 T 应该是元组类型:

  1. T 用于其余参数,如 (...args: T) => T
  2. T 用于像 [...T] 或 [number, ...T, string] 这样的传播组合

因此,我们不需要在形式类型参数部分使用省略号(我们甚至可以在没有任何类型检查器的情况下_语法地推断它)

在这种情况下,我们也可以写

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

但它是多余的。

就我个人而言,我希望看到后者在 TypeScript 中实现。 @JsonFreeman ,@sandersn?

@Artazor我认为这归结为表现力,我认为这两种方法不一定等效。 第二个包括在元组类型中传播休息类型参数的能力,而第一个似乎没有。

我认为对于泛型类型引用,这只是决定在何处以及在语法上如何使用 rest 类型参数的问题。 这需要为采用类型序列(元组、签名、泛型类型引用)的所有类型构造函数决定。

对于泛型签名,由于类型参数推断,它更加复杂。 如果您有以下情况怎么办:

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

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

foo 返回什么? 我的观点只是人们希望泛型规则对于泛型类型和泛型签名是相同的,但是如果您使泛型类型更具表现力,则类型参数推断需要一种方法来处理它。 这可能只是正式识别难以进行类型参数推断的情况,并要求用户在这些情况下传递显式类型参数。

在我看来,我认为您的选项 1 更好。 我个人不认为使用元组类型作为其余参数。 我认为应该只允许 rest 参数是数组类型或 rest 类型参数,因为它应该具有可变长度。 我也喜欢 rest 类型参数的概念是一个抽象的类型序列,与类型系统中已经存在的东西无关。

我对元组的哲学是它们代表长度已知的数组值的子集。 这些数组值是真正的运行时实体。 我不喜欢将它们用作一种类型系统设备来表示抽象类型序列(例如签名中的参数序列)的想法。 但是是否允许在元组中传播休息类型参数是另一回事。

我喜欢元组提案,因为它更强大,解决了更多用例,我可以将元组作为休息参数传播也非常直观,因为元组只是数组,当调用带有休息参数的函数时,我可以传播数组。 然后类型系统会更好地匹配我对代码的理解。

@JsonFreeman在您的情况下 foo 将返回[string, number, number]因为这将从...args推断出,推断的 cb 类型将是(string, number, number) => void并且传递的回调将忽略最后一个参数在 TS 和 JS 中都很常见。

我不喜欢使用它们作为一种类型系统设备来表示抽象类型序列的想法

它们正是如此,JS 不知道元组,只知道 TS。 对于 TS,元组是一系列类型。

我也喜欢基于元组的方法。 特别是如果我们可以有这样的兼容函数签名:

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

后者我们无法用当前的语法表达,但如果我们采用 #6229 就可以。
所以对我来说,使用元组和统一元组来表达更多似乎是一个正确的方法。 如果没有更具表现力的元组,就很难有像[...T, ...T]这样的东西,因为 T 作为元组有一个开放的长度。

@JsonFreeman举个例子, @Pajn完全符合我的理解——推断这些类型没有任何明显的问题。

@JsonFreeman我最好使用该语法

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

嗯,可能它可能会引入一些歧义:

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]

我可以支持在元组中传播休息类型参数的想法。 但我不确定我是否希望将休息类型参数隐式解释为元组。 如果允许其余类型参数分布在所有类型序列位置(元组、参数列表、类型参数), @Pajn的示例仍然有效。

@Igorbek您对第一个示例中的歧义是正确的。 你的第三个例子也有问题。 给定像number, string这样的序列,签名有 2 种可能的实例化。 即(arg1: number, arg2: string) => [number, string]以及(arg1: [number, string]) => [number, string] (为了示例而采用隐式元组解释)。

关于隐式元组解释的另一个奇怪的事情是:假设你有一个休息类型参数 T 被实例化为number, string 。 现在假设您将它们作为类型参数传递Foo<T> 。 这是否被解释为Foo<[number, string]>Foo<...T>Foo<number, string> ? 对此有一个争论,因为它将扩展运算符扩展到类型系统。 但我仍然宁愿元组版本表示为Foo<[...T]>

说我疯了,但我感觉到使用的想法存在一些根本缺陷
元组。 如果你试图将一个元组类型分散到太多
参数? 像这样?

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

此外,如果类型参数的类型错误或用于
不寻常的,可能是错误的地方?

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

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

第一个示例与 Function#apply 直接相关(并且可能是一个
错误),第二个是不明显的错误,将无法编译,
并且使用 Intellisense 进行检测非常重要。

在Sun,2016年2月28日,03:04贾森-弗里曼[email protected]写道:

隐式元组解释的另一个奇怪之处是:说
你有一个休息类型参数 T 被实例化为数字,字符串。
现在说你将它们作为类型参数传递,Foo. 是不是这样
解释为 Foo<[number, string]> 而 Foo<...T> 是 Foo 字符串>?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -189817561
.

@JsonFreeman

你的第三个例子也有问题。 给定像number, string这样的序列,签名有 2 种可能的实例化。 即(arg1: number, arg2: string) => [number, string]以及(arg1: [number, string]) => [number, string] (为了示例而采用隐式元组解释)。

从我的第三个例子来看,很明显它只能解释为(...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]>;

隐式元组解释的另一个奇怪之处是:假设您有一个休息类型参数T被实例化为number, string

T不能实例化为number, string因为它是一个真正的元组。 它必须是[number, string]

现在假设您将它们作为类型参数传递Foo<T> 。 这是否被解释为Foo<[number, string]>Foo<...T>Foo<number, string>

真的。 但是,对于我们正在讨论的这个特定用例(为其余参数捕获定位类型),拥有<...T>似乎是多余的。 尽管如此,假设我们拥有它。

对此有一个争论,因为它将扩展运算符扩展到类型系统。 但我仍然宁愿元组版本表示为Foo<[...T]>

有两种情况我们可能会使用该语法:

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

因此,对于用法,它只不过是像|&[]这样的类型运算符。 但是在声明中,语法可能被解释为T extends any[]或所有元组的任何基本类型,以表明它必须是元组类型。

@isiahmeadows

如果你试图将一个元组类型分散到太多
参数? 像这样?

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

此外,如果类型参数的类型错误或用于
不寻常的,可能是错误的地方?

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

我仍然没有看到将这些东西表示为元组的好处。 此外,我认为它们应该声明为<...T>而不是<T> 。 正如我之前所说,我不认为元组类型是用于类型系统中任意长度类型序列的合适设备。 我仍然不相信这是人们想要的表现力所必需的。

我同意它可能更具表现力,但是在类型参数位置使用“spread”运算符将限制我们只能捕获一次剩余参数,就像我们不能拥有两次剩余参数一样。 因此,给定<...T><A, B, C>T会将它们捕获为[A, B, C] 。 我们将无法表达<...T, ...U>因为它会模棱两可 - [A, B, C], [][A, B], [C]或 ... 等等。

假设我想表达一个具有以下行为的函数:

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

好的,我现在知道你是怎么想的了。 听起来您提议的实际上与我的想法不同。 与其添加用于捕获类型参数序列的新构造,您只希望元组类型是可扩展的,因为它们已经表示了一个类型序列。 这样就可以以更透明的方式传递不同长度的多个元组。

在javascript中,它更像是function foo([...rest]) { }而不是function foo(...rest) { }

这对我来说更有意义,谢谢你的解释。 我认为这是一个合理的方法。

@JsonFreeman没错!

@JsonFreeman问题:为什么[1, 2]满足[number] ? 这对我来说似乎很奇怪。 实际工作会非常令人惊讶。 它根本不是类型安全的。

不过,并不是说我反对将元组用于可变参数类型(老实说,我是中立的)。

@isiahmeadows以何种方式[1, 2]不能替代[number] ? 这绝对是一个亚型。 这与{ x: 1, y: 2 }是有效的{ x: number }

好的。 我会部分承认,但要考虑到 Function.prototype.apply,它接受一组参数。

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

如果调用者在太多参数上抛出 TypeError,那么传递太多将导致运行时错误,而不是像它应该的那样编译错误。

任何 JS 函数在传递太多参数时抛出 TypeError 不是很罕见吗? 有哪些例子?

@isiahmeadows作为一个抽象的例子,我理解你担心的错误是:

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.

那是对的吗?

@sandersn ,我认为 TypeError 在太多参数上违反了 JS 的精神,因为我们通常传递函数的形式参数少于将传递给该函数的实际参数。 我们根本不使用它们。 例如Array.prototype.forEach

函数柯里化呢? 这可能更常见,使用 Ramda
和 lodash/fp。

在星期一,2016年2月29日,13:45阿纳托利Ressin [email protected]写道:

@sandersn https://github.com/sandersn ,我也认为 TypeError
许多论点违反了 JS 的精神,因为我们
通常传递函数的形式参数比实际参数少
传入这个函数。 我们根本不使用它们。 例如
Array.prototype.forEach


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190327066
.

@isiahmeadows我会说基于arguments.length柯里化非常不稳定并且运行时容易出错。 真正的柯里化是额外论证的:

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

当我将带有固定签名的函数作为回调传递到某个地方时,我会以以下方式考虑它:“我的函数需要_至少_这些参数”

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

这在函数式编程中很常见。 并省略最后一个
争论相当普遍。

2016 年 2 月 29 日星期一,13:52 Anatoly Ressin [email protected]写道:

@isiahmeadows https://github.com/isiahmeadows我想说的是柯里化
基于 aruments.length 非常不稳定并且运行时容易出错。
真正的柯里化是额外论证的:

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


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190330620
.

如果您想将其作为回调传递给map (处理列表
列表),你可能想要咖喱它。

2016 年 2 月 29 日星期一,Isiah Meadows [email protected]写道:

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

这在函数式编程中很常见。 并省略最后一个
争论相当普遍。

2016 年 2 月 29 日星期一,13:52 Anatoly Ressin通知@github.com
写道:

@isiahmeadows https://github.com/isiahmeadows我想说的是柯里化
基于 aruments.length 非常不稳定并且运行时容易出错。
真正的柯里化是额外论证的:

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


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190330620
.

我认为主要是关于:

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]

这就是为什么我提出了#6229

[1, 2]满足[number]是一个值得提出和辩论的有效问题。 但是它与可扩展元组功能有什么关系呢?

元组的可变参数应用是否应该忽略额外的参数
或不。 这个重载的函数应该更详细地说明我的担忧。

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)

在星期一,2016年2月29日,18:47贾森-弗里曼[email protected]写道:

[1, 2] 是否满足 [number] 的问题是一个有效的问题
和辩论。 但是它与可扩展元组功能有什么关系呢?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190453352
.

这就是为什么,出于实际原因,我更喜欢休息参数之类的
可变参数类型。

2016 年 2 月 29 日星期一,19:00,Isiah Meadows [email protected]写道:

元组的可变参数应用是否应该忽略额外的
争论与否。 这个重载的函数应该详细说明我的
忧虑。

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)

2016 年 2 月 29 日星期一,18:47 Jason Freeman通知@ github.com
写道:

[1, 2] 是否满足 [number] 的问题是一个有效的问题
和辩论。 但是它与可扩展元组功能有什么关系呢?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190453352
.

@JsonFreeman那是因为类型和数组/元组的扩展运算符。 如果允许以“给定类型ABT = [A]形式传播类型运算符,那么[...T, B]将构造[A, B] ” (这是隐式提出的)然后它不会与数组/元组扩展运算符对齐。 给定var a: [A]var b: B ,表达式[...a, b]的类型不能被证明是[A, B] 。 根据元组的当前规则,可以证明它是[A, A|B]
这对你有意义吗? 或者我可以创建比较表来突出显示不匹配。

@Igorbek我明白你在说什么。 它最终源于这样一个事实,即编译器对它所处理的类型有完美的了解,但对值的了解并不完美。 特别是,在您的示例中,值a长度未知,而类型[A]长度已知。 这是我最初对为此目的使用元组类型感到不舒服的原因之一。 但我不确定这是一个严重的问题。

@isiahmeadows我明白你在问什么,但为什么这个问题对休息类型参数更清楚? 如果你的参数比类型参数多,同样的问题可以问。

类型安全的解决方案将与其余的解决方案更加一致
语言,如果它模仿了参数语法。

我的观点是,如果你有效地传播了一个休息参数,你会得到
正是参数类型,仅此而已。 柯里化函数有返回
类型取决于参数类型。 所以如果你应用一个太多的论点
部分应用咖喱函数,你会得到一个完全不同的
类型。 处理像元组这样的休息类型会导致运行时错误,
这从来都不是好事。

在星期二,2016年3月1日,06:07贾森-弗里曼[email protected]写道:

@Igorbek https://github.com/Igorbek我明白你在说什么。
最终源于编译器拥有完善的知识
它处理的类型,但不完全了解这些值。 在
特别是,在您的示例中,值 a 的长度未知,而
类型 [A] 具有已知长度。 这是我最初的原因之一
为此目的使用元组类型感到不舒服。 但我不确定
这是一个严重的问题。

@isiahmeadows https://github.com/isiahmeadows我明白你在问什么
大约,但为什么这个问题对休息类型参数更清晰?


直接回复此邮件或在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment -190667281
.

@isiahmeadows你能给出柯里化问题的示例代码吗?

我仍然认为,即使您使用了 rest 类型参数(我完全赞成),您也必须明确决定不允许过多的参数,但我同意@isiahmeadows 的观点,您可能应该这样做。

@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

我也会添加我的变体。 让我们看看提案中的可变咖喱示例:

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

于是,我开始使用它:

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无论它们是表示为休息类型参数还是元组类型,听起来您都反对将它们传播到元组位置的能力。

@Igorbek我认为你的例子很相似,因为问题不在于如何表示可变参数类型序列。 将它们散布在元组中的能力会导致问题。

@JsonFreeman我更反对这种行为:

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)

这澄清了吗?

@isiahmeadows它应该可以实际工作

@JsonFreeman
我觉得不应该。 这是我最大的反对意见。 如果是的话,我觉得它有潜在的危险。

问题: ret的推断返回类型应该是什么?

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

这实际上非常重要。

最后一个例子看起来肯定是行不通的。 本质上,如果 function.bind 真的能正常工作,你需要一种机制来对齐类型序列。 您将需要类似于统一的东西,其中要绑定的参数类型与原始函数的参数匹配,然后剩下的在返回类型中。

也就是说,在提议或讨论的内容中似乎没有任何东西可以处理这个问题(无论是否允许额外的元组参数),尽管我可能遗漏了一些东西。

我认为最大的问题是某种元组模式匹配,其中每个参数的类型与传播类型匹配,需要使用类型参数(如 LiveScript/CoffeeScript 的参数)来解决这个问题。 否则可能是不可能的。 至于它有多复杂,祝你好运。 :微笑:

@JsonFreeman

或者更准确地说,它需要非严格的(在渴望与懒惰的意义上)类型检查才能工作。 我也认为这可能是一个比可变参数类型更有用的扩展,无论如何,因为它几乎为许多其他更有用的东西打开了大门,比如自递归类型。

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

值得庆幸的是,非严格类型检查应该是一个纯粹的非破坏性更改,因为只有以前无法检查的代码现在可以工作。

这可能是阻止Function.prototype.bind正确键入的最大因素,除了它需要非常复杂的类型签名这一事实之外。

这是一个有趣的联系。 我不相信他们是相关的。 递归类型问题是泛型缓存策略和编译器中类型别名的表示的结果。 所有信息都在那里,只是编译器的设计造成了障碍。

对于元组模式匹配,您不能总是知道有多少参数与元组匹配。 如果您将数组扩展到bind的参数中,您不知道结果回调中还剩下多少。

@JsonFreeman 话虽如此,您认为作为采用参数的步骤,传播运营商提案#6229 需要首先考虑吗?

@JsonFreeman

并且不严格的类型检查将允许足够的懒惰,以便更容易地使用Function.prototype.bind解决该问题。 由于这种懒惰,您可以使用以下方法完成该类型(这将需要元组语法来对它们进行排序,除非在类型声明中可以使用多个其余参数):

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

为什么这需要非严格类型检查来推断? 您必须逐步推导出其余类型以检查该功能。 鉴于以下情况,以下是它必须检查的方式:

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

这就是非严格类型检查的工作原理。 它根据需要推断类型,而不是它看到它的瞬间。 您可以(并且应该)将两个传递组合在一起,这样错误的类型就会失败。 例子:

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

在这种情况下,预期参数应该是该轮中推断的第一个参数,进行深度优先迭代。 在这种情况下,该搜索中推断出的第一个是字符串,并且符号不能分配给字符串,因此失败了。


正因为如此,并尝试输入Function.prototype.apply ,我对使用元组应用休息类型的看法发生了变化。

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

其他一些注意事项:

  1. 需要有一种方法将数组和元组作为休息类型参数进行传播。

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

  1. 构造函数和可调用函数需要有两种不同的类型,函数是两者的结合。 可调用对象应实现可调用接口,类构造函数应实现可构造接口,ES5 函数应实现两者的并集。
  2. Function.prototype.bind和朋友应该检查该函数的所有重载。 如果有多个工作,它应该返回所有这些工作的联合。

但是,您示例中的那些类型参数并不是绑定签名的类型参数。 它们属于函数类型。 但是,是的,这个想法是,如果您可以使用两个休息参数,或者在元组中传播两个休息类型参数,您将能够编写这个。

为了让 bind 的签名足够灵活, ...X...Y之间的边界需要在每次调用的基础上决定。 这需要推断。 但是,如果签名要单独使用...X ,那将是一个问题。 在这种情况下,边界将不会被确定。 例如:

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

对于 Function 类型来说,重载是一个相当大的问题。 我认为您不想在每个参数上采用联合类型元素,因为这将允许在重载之间混合和匹配参数。 这是你的意思吗?

@JsonFreeman

_TL;DR:跳到水平换行符。 我有一个新的、更实用的想法。_

  1. 是的,我知道它们确实属于Function本身。
  2. 那种问题就是为什么我说非严格类型匹配(在 Haskell 的意义上)是必要的。 您不能正常地急切地解析类型,因为那需要迭代的、惰性的搜索才能完成。 可以通过算法确定,但您必须跟踪通常不需要在 C++ 中跟踪的内容。
  3. 如果这两个参数是孤立的(就像在你的例子中一样),编译器应该抱怨。 并且可以通过对接口/任何内容中的每个可变参数进行类型级依赖分析来检测这种情况。 它也不是微不足道的,但可以在阅读类型声明本身时进行验证(实际上,不久之后)。

尽管我也认为仅根据所讨论的方法定义这些类型的情况可能更可行。 检测您提到的那些潜在问题也会更容易、更快。

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
}

还有一个潜在的其他问题将更难解决(以及为什么我认为它应该限制为 2 个,而不是_n _ 个分区):

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(), {}, []]
)

_对不起,如果我在这里对 CS 理论太深入了..._

是的,我认为这是正确的想法。 它并不漂亮,但我想不出任何其他方式来正确键入bind ,知道Function的类型参数。 最终的事情是必须推断出边界。 而且我同意它应该限制为 2 个桶,以便您必须推断 1 个边界,而不是一些任意数量的边界,这可能会组合爆炸。

可能还有更多我们没有想到的问题。

@JsonFreeman另一个问题是curry 。 我还没有想出可以正确输入的东西。 我还需要一段时间才能做到。 我必须做一些严肃的 Haskell 类型的黑客攻击才能想出这样一个过程。

考虑如何使用一些 Bluebird 功能来使用 think kind 的提案。

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

我们对上面的all<...T>(promises: [...PromiseLike<T>]): Promise<...T>;有解决方案吗?

@DerFlatulator

请参阅我在 PromiseConstructor 中的重要评论。 我还更正了您的 Promise 界面,使其更接近我的建议。

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

如果[...Foo<T>]扩展为[Foo<T1>, Foo<T2>, /*... Foo<TN>*/] ,那么[...Foo<T,U>]是语法错误还是组合扩展?

@DerFlatulator

  1. 如果正好TU是休息参数,则它会正常扩展。 假设T是一个休息参数,那么它将是[Foo<T1, U>, Foo<T2, U>, /*... Foo<TN, U>*/]
  2. 如果两者都是其余参数,并且可以正确推断出它们的长度,则它应该是组合展开式(好吧……T 的长度乘以 U 的长度)。
  3. 如果两者都不是其余参数,则是语法错误。

请注意,出于实际原因,我强烈反对超过 2 个剩余参数,并且剩余参数如果需要拆分,则只能在每个方法的基础上拆分。 像这样的东西:

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
}

_(如果有人能想出更好的语法,我全听。我不喜欢它,但我想不出任何不视觉冲突的东西。)_

@isiahmeadows

2. 扩展的顺序是什么?

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

或者相反:

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

这种歧义不会引起混淆吗? 也许限制在一维是明智的。


只是拆分语法的一个替代建议:

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

我期待第二个。 而且我怀疑它会引起太多混乱,因为只要始终如一,人们很快就会习惯它。 这也是一个不寻常的边缘情况,只有知道自己在做什么的人或首先应该质疑需求的人才会在实践中真正遇到这种情况。

我也在查看它,因为您正在扩展第一个,然后为第一个的每个部分扩展第二个。 像这个伪代码:

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

迭代上面的一些想法......

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

这里, [...TBound, ...TUnbound] = [...TArgs]是有效的,因为...TBound的长度是从args的长度知道的。 它还允许更改TThis的类型。

这种方法的一个问题是您只能绑定this一次,例如:

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>

y的推断类型不正确,应该是Function<number, IBar> 。 我不确定这是否是一个问题,但解决它需要在<T>语法中引入逻辑。

选项1

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

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

然而,这可能超出了本提案的范围。

我认为我们不应该通过使用类型扩展运算符来允许这种扩展。 我将扩展运算符视为“括号去除器”,它与数组扩展运算符和对象/属性扩展运算符(第 2 阶段提案)完全一致。 比较一下:

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

您建议扩展它以构建新的类型集:

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

这是完全不同的操作。 如果您愿意,它可以通过更高阶的结构建模,例如:

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

@Igorbek如何使用运算符来确定将扩展的内容?

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

其中...Foo<<strong i="9">@T</strong>, U>扩展为[Foo<T1,U>, /*...*/, Foo<TN,U>]

...(PromiseLike<@T> | @T)扩展为
[PromiseLike<T1>|T1, /*...*/, PromiseLike<TN>|TN]

一些语法替代方案:

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

我在这里同意@Igorbek 。 至少在这个阶段,类型的映射序列似乎不是一个优先事项,因为我们仍在尝试解决更基本的可变类型参数问题。

我对禁止它没有太大问题(至少最初是这样),因为这样做的行为非常缺乏营养,两个不同的人甚至可能期望两种截然不同的东西。 至少现在我同意@Igorbek ,因为 TypeScript 首先需要有一个高阶类型模型(从某种意义上说,这就是map ping 一个类型)。 高阶类型并不是您可以随意使用的东西。

所以肯定 :+1: 禁止这样做,可能会持续很长一段时间。 尽管拥有它很好,但实现起来却很复杂,而且完全是一种黑客行为,因为 TypeScript 不使用功能性、类型安全的类型系统。

来得有点晚,但我也同意@Igorbek 。 重申我在 #1336 中所做的评论,并从 C++ 参数打包中借鉴了具有明确“pack”和“unpack”运算符的想法。

将类型打包到元组中似乎与 Typescript 对扩展运算符的使用一致:

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

这使得<...T_values> = [T1, T2, T3, etc...]更容易推理。

虽然 C++ 使用扩展运算符进行打包,使用省略号进行解包,但两者都使用扩展与 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如果没有映射,这一切有什么意义?

同样在 #1336 中提出,可变参数Array.flatten怎么样?

@jameskeane前半部分是最初的想法,但它没有涵盖中间休息参数的情况(某些 API 具有):

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

它也没有很好地涵盖Function.prototype.applyFunction.prototype.call

至于#1336,可以通过以下方式类似地实现:

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

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

我已经赶上了,并意识到我天真地假设元组类型是严格的长度; 哪个imo最直观。 所以假设我们得到严格的长度元组类型 (#6229),有什么问题?

@isiahmeadows在你上面的中间休息参数案例的例子中,它不是通过严格的长度元组来解决的吗? 我正在阅读...rest: [...T, Baz]与展开解包arr = [...other, 123] 。 这与您用curry提出的问题相同,对吗?

至于applycall ,它们不是由相交类型覆盖的吗? (并不是说我真的看到Function接口上的类型的

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

当前的可变参数提议假设 #6229 实际上最终被接受(即默认情况下元组是严格的)。

至于func.applyfunc.bindfunc.call_.curry ,唯一的问题是func.bind_.curry 、和朋友,或者更普遍的任何使用部分应用程序的东西。 您还需要能够选择要分离的其余参数,并且它只能在每个方法的基础上完成。

callapply非常简单:

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会更困难。 分割参数必须根据需要匹配,不像现在这样急切,直到第一个分割的一半完全解包。 这应该作为语法来实现,以便编译器可以区分它并正确识别类型而无需评估任何内容。

// 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会非常困难,因为它必须知道f(1, 2, 3) === f(1, 2)(3) === f(1)(2, 3) === f(1)(2)(3) . 不仅必须能够像在bind那样将 rest 参数分成两个,还必须能够在每个方法的基础上执行非常原始的模式匹配。

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

我不相信curry会使其图灵完备,但它会很接近。 我认为阻止它的主要因素是能够匹配特定类型的特化(C++、Scala 和 Haskell,三种具有图灵完备类型系统的语言都具有)。

@sandersn我看不到上面的例子,但我可以问一下对可变参数的约束吗?

考虑以下示例:

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

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

_顺便说一句,请参阅https://github.com/Microsoft/TypeScript/issues/7848以讨论可能会放弃需要列出X要求_

现在,关于约束是否为:

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

在这个例子中,我假设 2。

这些类型的约束(1 和/或 2)是否可能?

@myitcv 2 可能是最好的选择,但重用现有的逻辑来检查约束是有意义的。

嗯...我刚刚意识到一些事情:包含可变参数类型的数组将如何? 或者更具体地说,下面的arg是什么类型?

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

我猜你在问args的元素类型是什么。 对于元组,我相信这通常是元素的联合类型。 我想不出更好的输入方式。

@sandersn你能评论一下这个功能的状态吗? 我觉得已经有很多讨论了,但听起来似乎对该功能没有明确的计划,对吗?

@JsonFreeman我在问什么是arg 。 在我看来,对于我的原始示例,它应该是any Item<T> ,下面是

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

这样可以在本地解析类型。 您事先不知道类型,这将极大地加快编译速度,因为您不必为每次调用在函数中计算类型。 请注意,如果您只需要单个参数的类型, typeof arg就足够了,而且可能会更短。

哦对不起,对于最初的例子,我的意思是类型应该是T 。 实际上,对于您的第二个示例,我也认为它应该是 T。

我的意思是Item<any>在第二...对不起。

当我说它应该是 T 时,我假设 T 是一种类型,但我想这个特性的全部意义在于 T 不是一种类型(我认为)。 所以是的,我想在你的例子中应该是anyItem<any>

但更广泛地说,我很好奇团队在 2.0 之后考虑此功能的积极程度。 我没有强烈的意见,只是想知道。

我认为它不一定是T的原因是因为你不知道T是什么。 当然,除非您的意思是让可变参数T表示可变参数类型列表的单一类型,或者在展开时表示列表本身,即T是传递给的所有参数的子类型参数...T[...T]可分配给T[]

或者,为了澄清我在所有不清楚的行话中的意思,以下是我在代码方面的意思:

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

不过,这可能更有意义,而且会更加灵活。

它仍然在我们最好的清单上,但它主要是图书馆作者感兴趣的,并且有一个不错的解决方法 - _n_ 重载 - 所以我没有积极地研究它。 如果我不得不猜测,我会说 2.1 是可能的,但不太可能。

如果/当我们承诺正确支持对象休息/传播(#2103),那么可变参数类型可能足够接近传播类型,以证明一次完成所有这些是合理的。 (传播类型是对象类型的变体,看起来像{ ...T, x: number, ...U, y: string, ...V } 。)

只想提一下n overloads解决方法不适用于类或接口,这是我对这个特性特别感兴趣的地方。

@sandersn 是否会使用this键入在函数中为bindapplycall邀请“_n_ 重载”的拉取请求? 我认为这对许多人来说是可以接受的临时妥协,并且可能会在某些项目的过程中捕获相当多的错误。

@isiahmeadows

我认为它不一定是 T 的原因是因为你不知道 T 是什么。

在我看来,大家一致认为 T 是可变参数类型的元组类型。 在您的原始示例中, arg将与元组元素类型相同(正如@JsonFreeman指出的,“元素的联合类型”):现在想象一下打字稿支持使用元组作为休息类型 (#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]

除了这个提议之外,我认为应该有一种方法来表示元组的“元素类型”。 这可能是点差的另一种用途,即如上...T :: number|string|boolean ; 传播元组类型会导致它的元素类型。

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

考虑到这一点,您的其他示例:

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

  }
}

没有忘记我最初的目标,我想要一个强类型的Promise.all ...

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现在其他请求的功能开始依赖于此,优先级可能会提高吗? bindcall等打字依赖于此,而 ES 绑定语法是否/何时出现取决于此,所以现在有更多的依赖于此而不是古怪的库作者一直唠叨你. :)

并不是说这是特别有建设性的添加,但如果这两个功能能够进入 2.1,我会很高兴。 我知道至少有一个库 (RxJS),在这些库中,这些功能不仅可以改进代码库本身,而且使用代码也不会那么笨拙且容易出现错误(每个第三个开始使用 Angular 2 的人都会被缺少导入所困扰对于修补到可观察原型中的运算符)。 对于希望编写可维护功能代码的人来说,这确实是一个突破性的特性。

这是否可以用于为_.extend提供完整的类型定义,其返回类型是其所有参数的交集?

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

并非如此。 它需要对提案进行扩展,以提供有关可变参数类型的新运算符的详细信息——可能称为 &。 @kitsonk早些时候在此评论中提出了此运算符。
现在这个功能低于其他几个更重要的东西,所以我有一段时间没有研究这个提案。

虽然没有提供完整的可变参数类型,但 #10727 是解决方案的一部分(并且可能解决我们 (@dojo) 面临的挑战)。

很高兴听到! 虽然它实际上仍然不是可变参数类型。 :( 例如,本周当我尝试输入Object.assign ,我得到了这么远:

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

请注意,“六个点”语法是元组类型变量的对象扩展,我们上面没有真正讨论过。

@sandersn

特别是Object.assign ,它可以这样输入,并且在技术上捕获一个子集(虽然有点太弱了),因为它改变了它的目标(你必须有一个参考点):

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

问题在于它改变了目标,改变了它的结构类型。

@isiahmeadows然后推断会将T固定target类型,而没有sources会计类型。 您现在可以尝试使用非可变参数版本:

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

如前所述, assign使用 _a 点差类型_ #10727 并且可以这样定义:

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

_注意:我仍然坚持基于元组的语法[...T]对我来说更有意义。_

@sandersn顺便说一句,是否有关于何时登陆可变参数类型的更新? 有机会在2.2看到吗?
而且,关于语法,你们是否仍然接受有关语法的反馈,或者你们都同意这一点?

语法和低级语义还没有明确的共识。

2016 年 12 月 13 日,星期二,13:26,Igor Oleinikov [email protected]写道:

@sandersn https://github.com/sandersn顺便说一句,是什么时候更新
可变参数类型将被登陆? 有机会在2.2看到吗?
并且,关于语法,您是否仍然接受有关语法的反馈或
你们都同意吗?


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-266819647
或静音线程
https://github.com/notifications/unsubscribe-auth/AERrBIa5fE8PSk-33w3ToFqHD9MCFoRWks5rHuM5gaJpZM4GYYfH
.

知道这个问题的状态是什么吗?

那么你在考虑的选项是什么? 这是团队的议程吗? 这是我反复遇到的类型系统的唯一薄弱部分。 我有两个用例。 一个简单的和一个复杂的,但更一般的。

简单的方法是添加一个只能由元组类型进行子类型化的Tuple extends any[]超类型。 由于点差需要是any[]子类型,这将起作用:

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

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

目前, ...args: T[]只允许出现在签名的末尾。

复杂的用例需要...args: Tuple在签名内的任何地方都是合法的(这没有问题,因为元组是固定长度的):

/**
 * 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)

是的,我昨天才开始使用 TypeScript,这已经使得我无法自动键入我的函数(当然我仍然可以手动完成),因为我使用一个装饰器来包装我的函数(这是第一件事)我试着开始!):

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

实际上,装饰器所做的所有事情都是允许将函数作为方法调用,以便它们可以作为方法附加并以相同的方式工作,作为一个基本示例,这是一个修补Array原型的坏示例使用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])

现在希望很明显flatMap (不是_flatMap )的类型是:

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

但是我无法将types到可移植的,因为我无法从_flatMap提取参数类型然后在装饰函数的类型定义中使用,我想通过这个建议我可以写一些类似的东西:

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

我只是想分享这个,因为它展示了我对 TypeScript 的初步体验,也许还展示了为什么可变泛型很重要的另一个案例。

@桑德斯恩

它有一个不错的解决方法 - n 重载

虽然技术上没有错,但我觉得这并不能完全反映这里的现实。 是的,从技术上讲,缺少 this 并不能阻止重载键入此线程中提到的任何函数; 然而,这种轻微的不便意味着到目前为止还没有任何基于重载的解决方案进入lib.d.ts

事实上,该线程中的许多人都对处理各自的功能感到绝望,以提出更多原本不属于该提案的语法,包括您的...... ,以及...*X[...T = ...A, ...B, ...C][...PromiseLike<T>]<[...X, ...Y] = [...A]><PromiseLike<T*>>

我认为这表明我们都在努力解决这里的问题,我们普遍认为我们需要像这样更强大的语法,并希望我们在这里选择的任何道路都能帮助我们解决这些问题。

旁注:对于 Ramda 的R.path我们生成了数千行重载的类型,但仍然缺少元组支持(排列会爆炸式增加更多),并且只会导致实际项目的编译不会终止了。 最近发现迭代是一个看似可行的替代方案(#12290)。

顺便说一句,你还没有对@Artazor和@Igorbek 提出的提案发表评论。 你对此有何想法?

我想在这里讨论一个像这样的基本实现(加上#6606),我们几乎可以做任何事情。 我将在这里提供一些解决方案来说明这一点,但我愿意接受进一步的问题。

首先,我去了一些地方,一个...运营商可以实现:

v ...为 | 定义(捕获)| 使用(传播)
-|-|-
功能 | type Fn = (...args: any[]) => {} | type Returns = typeof fn(...MyTuple); (#6606)
数组 | 类型级元组解构。 技术上可以使用索引访问+传播(见右)+递归来模拟。 | type Arr = [Head, ...Tail];
对象 | 类型级对象解构。 没有必要,只需使用Omit ,参见#12215。 | type Obj = { a: a, ...restObj }; (不需要,与Overwrite ,见#12215)
泛型 | 定义type Foo<...T>来执行Foo<1, 2, 3> (将[1, 2, 3 ] 捕获到T )。 有趣,但不知道什么用例需要这个。 | 定义type Bar<A,B,C>来执行Bar<...[1,2,3]>A = 1等)。 同上,不知道需要这个的用例。
工会(奖金)| ? | type Union = "a" | "b"; type MyTuple = ...Union; // ["a", "b"] (顺序不可靠但允许通过元组迭代联合/对象。无论如何,这里超出范围。)

所以只有两个类型级别的...实例是直接相关的; 具体来说,这里使用的两个:

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

在#6606 的上下文中,另一个变得相关:为函数应用程序解包元组类型的能力,例如typeof f(...MyTuple) 。 我认为这些足以解决我在这里提到的更棘手的问题。 尝试在这里提供一些解决方案:

@詹姆斯基恩

我认为应该有一种方法来表示元组的“元素类型”

如果您想获得它们元素的联合,请参阅我的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);
}

@丹夫克

_.extend

@桑德斯恩

Object.assign

这些都只是 Ramda 的mergeAll可变参数版本。 不需要六个点!

@isiahmeadows

知道这个问题的状态是什么吗?
语法和低级语义还没有明确的共识。

如果我理解正确,您主要担心其他一些人提供的方法是否会考虑处理更严格的类型,例如您提到的currybind 。 根据他们的建议,这是我对那个特定的看法。
该策略有点相似,绕过了很难说的事实,通过将参数的类型检查推迟到函数应用程序,将参数 i~j 的类型要求从函数类型提取到元组类型。

// 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])`
}

是的,我使用了一堆辅助类型——只是试图利用我们拥有的东西(+想象一下我们可以用更多的东西做什么)。 我不太反对.........*X[...T = ...A, ...B, ...C][...PromiseLike<T>]<[...X, ...Y] = [...A]><PromiseLike<T*>> 。 但是 IMO,即使只是...可以帮助解决现在的实际问题,我很乐意看到它得到解决。

编辑:我解决了bind的参数约束。

只是一个可能很愚蠢的问题。 这看起来非常有希望能够正确输入咖喱函数。
但是对于一些现实世界的项目,人们可能不想花太多时间来键入大量面向函数式编程的代码。
所以,我想知道,由于--strict在 _tsconfig.json_ 中默认启用,是否有办法禁用部分代码的类型检查(因为懒惰或缺乏时间)。
但是,正如我所说,这可能是一个愚蠢的问题......^_^

@yahiko00有点偏离主题,但在tsconfig使用exclude部分或在不同项目级别使用不同的tsconfig

我还想提出另一个建议,我们是否可以让&|使用具有以下语法的单个元组参数:

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

@HyphnKnight的上述建议对我正在做的事情也非常有用。

我想添加一个免责声明,即该提案并未得到积极处理。 但是当我第一次开始研究这个问题时,我找到了我想要阅读的“现有艺术”类型的论文: http :

我将把它留在这里以供将来参考。

我打开了几个 PR 来试验这个:

  • [ ] #17884 以元组类型(WIP)传播
  • [x] #17898 提取剩余参数(就绪)
  • [ ] #18007 传播类型调用(WIP)
const c = 'a' + 'b';

能解决问题吗? c推断类型是'ab'不是string

StackOverflow 上的一个相关问题: Explicit last function parameter in TypeScript

@sandersn你的提议将涵盖这种情况,据我所知,这是正确的吗?

最初的提议已经过去 2 年多了,我们还应该抱有希望吗?

你好!
我正在尝试键入一个生成器,该生成器采用可变数量的数组并混合和匹配它们的元素以创建一个新数组。
我想在for...of循环中使用此生成器,但无法正确键入值。
代码(可能有错误,因为我还没有运行它,但这是我正在尝试做的):

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

用法示例:

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

在不创建中间变量的情况下,我无法想出任何方法来为目标和孩子打字。

这样的事情会好吗?

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

@Griffork正确的做法,在这个提议被实施之前,是为函数创建许多重载
例如,请参阅 promise.all 类型
https://github.com/Microsoft/TypeScript/blob/master/lib/lib.es2015.promise.d.ts#L41 -L113

我觉得这个语法很混乱:

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

这对我来说感觉更自然:

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

在运行时,rest 参数是一个数组,我们目前可以在 TS 中执行此操作:

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

因此,删除argsT数组的限制,而是让 TS 编译器能够推断T的元组类型似乎是合乎逻辑的,例如

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

我看到有人对元组提出了一些担忧,我并不完全理解它们,但我只是想权衡一下,在当前的提案中,很难理解开发人员何时需要使用...在类型位置和元组更直观。

...对我来说连接两个元组类型仍然有意义,比如[...T, ...U]

@felixfbecker
的提案

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

T是一个动态创建的元组类型,所以如果你传递一个string和一个int到函数中,那么T就是[string, int]
如果您想动态表达这样的模式,这尤其有趣:

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.

目前,如果您想编写一个接受可变数量参数并通用类型的函数,则必须为该函数可能给出的每个参数数量编写一个泛化重载。 ...T提案本质上允许我们让编译器为我们自动生成函数定义。

您的建议:

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

强制将所有参数视为同一类型,并且不能进行更具体的类型检查。 例如,在我上面的例子中,所有返回的值都是any

我还发现额外的...很难阅读。
就像@felixfbecker 的想法一样,我认为没有必要这样做:

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

阅读apply<...T,时首先想到的是它是一个传播运算符,但它实际上根本不做传播。

@Griffork ,在你的例子中T仍然是[string, int]
这就是@felixfbecker 的意思,“而是让 TS 编译器能够推断 T 的元组类型”,至少是我理解的方式。

强制将所有参数视为同一类型,并且不能进行更具体的类型检查。 例如,在我上面的例子中,所有返回的值都是 any 类型。

@Griffork不,在我看来,它会为args数组推断一个元组类型,通过每个参数在元组中的位置为其赋予自己的类型。 ...args: T[]会强制它们都是相同的类型T ,但是...args: T (目前是一个编译错误)会推断T的元组类型。

阅读 apply<...T 时首先想到的是它是一个扩展运算符,但它实际上根本不进行扩展。

@unional同意,这正是混淆的根源。

@unional
我也把它读为传播运算符,我把它读为“每次使用时传播这种类型”。
对我来说,读这个

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

我希望T是一个数组(例如string[] )。

并阅读:

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

我希望所有 args 都可以分配给T类型(这是一种类型,例如string )。

上述提议的要点是避免隐式地使泛型能够表示任意数量的类型。

@felixfbecker
编辑:
哦好的。 仍然不认为这是直观的。

我希望 T 是一个数组(例如 string[])。

元组是“某种数组”,它只是一个具有固定长度和每个元素特定类型的数组,例如[string, number] (vs (string | number)[] ,它是未绑定的并且不声明哪个元素具有什么类型)。

那么如果你真的想要这种行为,你会输入什么?

不确定您指的是什么行为,但我假设“强制所有参数为相同类型”,这将由...args: T[]

我也把它读为传播运算符,我把它读为“每次使用时传播这种类型”。

这就是为什么我认为它令人困惑。
当你传播时,你只是这样做,你不会声明一些“可传播”的东西:

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

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

是的。 如果你想声明一个泛型类型参数需要是“可传播的”(根据 ES 规范只是意味着它必须是可迭代的),我们已经有了一种在 TypeScript 中用extends

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

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

当然,在rest参数的情况下,众所周知它不仅是可迭代的,而且是一个数组。

其中,我们所说的已经提出: https :

但剩下的太长了,不能在一个座位上吃掉。 🌷

如果元组连接正在登陆,那么我们可以实现教堂编号! 万岁!

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

如果条件类型的递归有效,我们可以创建具有所需长度的元组:

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

我不认为我已经阅读了所有这个线程,但是如果在通用参数中有...T令人困惑,
为什么不尝试进一步反映值级解构语法,以便在[...T]中使用
如果类型级参数位置是类型级元组,类型级参数位置会解构类型吗? 这也需要
允许使用元组输入 rest params,这将在调用站点生成以下等效项
在打字稿中:

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

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

INB4“但那是类型导向的发射”——不。 这不会改变任何发射, first仍然有两个
发出不同的参数, second仍将有一个剩余参数。 它唯一改变的是
在调用站点,TypeScript 将检查second的参数是否匹配,按顺序,元组
[number, string]

无论如何,假设我们承认[...Type]语法,那么我们可以编写apply如下:

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`

[...Type]语法将完全表现为值级解构,允许拆分
并根据需要加入类型级元组:

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

关于如何使用此键入curry函数的示例:

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`

我认为能够说某些类型参数是类型级元组也会有所帮助
某种。 那么问题将是如何 - 考虑到 2.7(我认为?)元组可分配性需要
考虑元组长度——表达_任何类型级元组_的概念。 但也许像
[...]可以工作吗? 我没有强烈的意见,但如果这个概念是可命名的,那就太好了。

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

在这种情况下, [...ArgsT]的上述语法基本上可以是ArgsT extends [...]的简写,
使用类型级解构意味着将类型约束为类型级元组。

想法?

@jaen

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

是的,看起来像#4130。 我在 #18004 尝试了一些东西,但我的方法有点笨拙(合成节点)。

在表达任何元组时,我看到有人使用any[] & { 0: any } ,我猜它在空元组类型 fwiw 之前一直有效。 我个人并没有太多打扰,主要是解决了any[]

RxJS 到处都需要这个。 最重要的是Observable.prototype.pipe ,我们目前有很多重载,但我总是被要求添加“再增加一层”。

第二个@benlesh,我们广泛使用RXJS,并且需要它来处理管道函数。

我是ppipe的作者,它就像 RXJS 中的管道函数一样,需要这个。 我想我在这里看到了一个模式^^

我是runtypes的作者,迫切需要这个功能来表达联合和交集。 唯一(不完整)的解决方法是巨大的重载:

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

🤢

我重写了ramda的类型,这也需要它用于例如pipe 、镜头和柯里化。
我们需要代码生成器,因为柯里化使得直接维护重载变得难以管理。 我们的path类型跨越了一千多行,此时我们发现重载类型的性能也成为问题。

推断和应用其余参数的问题是否已解决?

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

如果apply(example, ['0', 1, 2, 3])的 T 类型被推断为[string, number[]] ,则对 apply 的调用将引发错误。

这意味着 T 的类型真的是

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

或者

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

确实是一头奇怪的野兽,但考虑到({0: string} & Array<number>)[0]将如何
当前解析为string [1] 似乎可以在不进行太多更改的情况下进行编码
到类型系统。

[1] 这是一个错误,它真的应该是string | number吗?

很抱歉打扰这个问题的 36 位参与者(提示:使用这个),但是我们如何监控这是否仍在考虑中,如果这在路线图上,等等?

令人遗憾的是,2 年半之后还没有人被分配到它,这似乎是一个非常重要的功能:(

PS:我读了几十条评论,试过 Cmd+F 等,没有找到这个信息。

@brunolemos在最新的设计会议中提到了可变参数类型
https://github.com/Microsoft/TypeScript/issues/23045
为了制作这个功能,他们首先需要迭代地创建更多的原始和概念,当有足够的基础时,我相信他们会将它添加到任何里程碑

我不能做到这一点

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

这是#14174的问题,但作为一个关系

@kgtkr供参考,请参阅@fightingcat使编译器忽略递归的技巧

谢谢

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

嗯,我有个问题。 我有这样的代码来处理混合:

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

我使用它如下:

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

它按预期工作 - 具有正确的类型,状态处理等,但是有没有办法以更短的方式重写而不等待可变参数类型? 因为我现在觉得这个有点傻。

使用 3.0 现在可以将 rest args 声明为元组

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

但是是否可以反向获取给定函数的元组类型的参数?

Arguments<foo>这样的东西会很好。

@whitecolor这个怎么样?

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

使用 TS 3.0 我们现在可以做到这一点

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

但是我们有一个小问题,我们必须声明为单个参数返回元组事件的函数,并以某种方式处理 void,我们必须声明返回类型,否则它被推断为数组:)

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%0Afunction%20foo1(a%3A%20string)%3A%20%5Bstring%5D%20%7B%0D%0A%20%20return%20%5Ba%5D%3B%0D%0A% 7D%0D%0A%0D%0Afunction%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% 20return%20%5Ba1%2C%20a2%5D%3B%0D%0A%7D%0D%0A%0D%0Afunction%20foo21(a1%3A%20string%2C%20a2%3A%20boolean)%3A%20%5Bstring %5D%20%7B%0D%0A%20%20return%20%5Ba1%5D%3B%0D%0A%7D%0D%0A%0D%0A%0D%0Afunction%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%0Afunction%20compose%3CX%20extends%20any%5B%5D%2C%20Y%20extends%20any%5B%5D%2C%20Z%20extends%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%20return%20function%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%20compose(%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
使用条件类型:

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

当然,但是这对于那些as any来说有点hacky :) 我认为如果类型系统支持这个没有hacks会很酷

好吧,函数体中的类型可以是多种类型,实际上没有什么可以做的——你可以做一些类似(f as (...args: any[]) => Y)事情,但我认为这会无缘无故地降低清晰度

如果可变参数类型实现,我将能够概括我为自己的项目编写的一些 TypeScript 代码,这让我可以完全定义 REST API 的形状,并在相应的 Node 服务器和 JavaScript 的类型上强制执行该形状它的客户端库。

概括这将允许我简化我自己的代码,以及对任意 API 做同样的事情,并进一步可能为其他语言客户端生成 Swagger 定义......可能对其他人有用! 虽然只是大声做梦哈哈

@kgtkr :看起来很棒! :)

管道类型对我来说使 TS 游乐场崩溃(尽管其他人工作正常),我想它需要最新的 TS?

TS 还显示了一些递归深度错误——看起来@isiahmeadows 为此打开了#26980。

@tycho01

管道类型对我来说使 TS 游乐场崩溃(尽管其他人工作正常),我想它需要最新的 TS?

它也挂在我身上,我不得不侵入 devtools 以迫使它抛出错误以摆脱它。

TS 还显示了一些递归深度错误——看起来@isiahmeadows 为此打开了#26980。

这是为了一些相关但不同的事情:解除条件类型的约束有两个原因:

  • 为了更轻松地完成更复杂的工作,例如列表迭代。 它还为开放诸如类型级整数数学之类的东西奠定了框架,而不会使编译器崩溃或最终陷入一些临时混乱。
  • 为了更正确地解决是什么使 TS 的类型系统图灵完备的问题,因此它可以潜在地使用利用它的工具来具体化或通过强制执行可证明的终止而将其移除。

如果删除索引类型不足以使类型系统不是图灵完备的,请随时在那里发表评论,以便我可以相应地更新它。 (当然,我不建议删除它。我只是建议在内部更好地处理它,以警告人们潜在的无限循环。)

再想一想,这种 Pipe 类型感觉就像是一种非常复杂的(vs...: Params<T[0]>) => ReturnType<Last<T>> 。 除此之外的任何迭代(除了中间参数检查)对于依赖于输入的返回类型可能会变得更有用。

@ tycho01它试图型喜欢的东西这样,这里的类型基本上是这样的:

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

必须单独迭代参数以正确键入它,因为后续参数的参数取决于先前参数的返回值,并且您还必须考虑第一个参数和结束返回值( @kgtkr没有) . 我在 #26980 中的“改进”版本进行了优化以使用累加器,所以我对参数的迭代次数减少了很多,但它也使它更正确。


如果你在 #26980 中查看我的,它应该做的事情会更清楚(更少的数字追逐),这也是我提交该功能请求的原因之一。

@tycho01 @kgtkr顺便说一句,我用更正的PipeFunc片段更新了那个错误,为了方便起见,复制到这里:

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

顺便说一句,这个不会在操场上崩溃。 它实际上会进行类型检查,而且速度非常快。

我还没有在潜在的_.flow_.flowRight类型上测试它,但这应该作为一个起点。

@tycho01
必需的
打字稿@下一个
3.0.1/3.0.2 不起作用

感谢这个线程,我做了这个

人们,请停止发布与讨论此问题几乎不相关的信息。 有很多人关注这个讨论,因为我们想要可变参数类型。 在过去的几天里,我收到了 10 多封与我关注此问题的原因无关的电子邮件。
我希望还有其他人同意我的看法。 到目前为止,我只是希望它会停止,因为我不想为垃圾邮件做出贡献。 但说真的,够了。
PS 很抱歉这个通知,对于像我一样厌烦他们的人

@Yuudaari我会指出输入 Lodash 的_.flow 、Ramda 的_.compose等是导致此错误的驱动因素之一,成功输入是解决此问题的一部分。 事实上,这是原始问题描述中列出的原因之一。

真的,在这一点上,我认为今天可变参数存在的 99% 的问题都与人体工程学有关,而不是与功能有关。 我们可以完美地输入Function.prototype.bindPromise.all混合索引类型、条件类型和递归(你可以重复Append迭代列表Function.prototype.bindPromise.all将是一个简单的迭代 + Append ),只是这样做非常尴尬和样板。

不想在这里增加噪音,只是解释从这里开始的内容是技术上的主题,因为它涉及错误存在的一些原因,即使它们不是您个人关心的原因。

我认为在这里等待公告的人们错过了大新闻——事实证明,现在可能的Concat<T, U>功能与[...T, ...U]完全一样。

Pipe子线程是关于演示我们在这里要求的功能。 今天就到了这个话题的重点。

我认为这意味着我们关闭这个话题不会更糟,所以也许现在是一个很好的时间来问 - 人们仍然希望从这个提案中得到什么?

[它]只是非常尴尬和样板

大多数使用它的类型将使用递归本身,因此编写它们的人肯定会熟悉它们,而最终用户可能只会使用具有预定义类型的库并编写其前端,而不必知道 TS 迭代存在。

到那时,也许这个提议可能主要提高性能?

首先,是否使用映射对象来欺骗类型系统进行递归甚至是有意的? 这对我来说似乎很hacky。 如果我使用这样的功能(我是,但这无关紧要),它以后不会受到破坏吗?

其次,使用这些变通办法并不......友好。 它的可读性不是很好(特别是对于那些没有编写它的人),因此它看起来很难维护。

为什么我要退回到以预期的、可读的和可维护的方式添加相同功能的提案,仅仅因为有一种解决方法?

我不认为这个解决方法的存在会导致这个提议被认为是语法糖,但即使是,我为什么想要语法糖来解决这个烂摊子?

@Yuudaari

编辑:添加上下文链接。

首先,是否使用映射对象来欺骗类型系统进行递归甚至是有意的? 这对我来说似乎很hacky。 如果我使用这样的功能(我是,但这无关紧要),它以后不会受到破坏吗?

看看我最近提交的错误:#26980。 您并不是唯一一个质疑这种模式的人。 这里有点离题,但请随意插话。

请注意,如何确定递归是否终止需要一些数学运算(这是它最初如此细微差别的主要原因之一)。

其次,使用这些变通办法并不......友好。 它的可读性不是很好(特别是对于那些没有编写它的人),因此它看起来很难维护。

为什么我要退回到以预期的、可读的和可维护的方式添加相同功能的提案,仅仅因为有一种解决方法?

我不认为这个解决方法的存在会导致这个提议被认为是语法糖,但即使是,我为什么不想要语法糖来解决这个烂摊子?

在有效Array.prototype.map的常见情况下,确实存在一种迭代元组的简化方法,但这对我的需求来说基本上没用(我需要一个累加器)。

我个人喜欢这些语法糖:

  1. 通过[...First, ...Second]连接两个列表。
  2. 通过[...Values, Item]附加值。
  3. 通过T extends [...any[], infer Last]提取最后一个元素。
  4. 通过T extends [A, B, ...infer Tail]提取尾部。

结合#26980,我可以把上面的类型变成这样:

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;

但仅此而已。 我认为任何其他语法糖都没有多大用处,因为这里的大部分内容都只处理元组,并且对象已经在很大程度上拥有类似操作所需的一切。

首先,是否使用映射对象来欺骗类型系统进行递归甚至是有意的? 这对我来说似乎很hacky。 如果我使用这样的功能(我是,但这无关紧要),它以后不会受到破坏吗?

我认为官方的说法类似于“不要这样做”。 @ahejlsberg

这很聪明,但它肯定会使事情远远超出其预期用途。 虽然它可能适用于小例子,但它的扩展性会很糟糕。 解析那些深度递归类型会消耗大量时间和资源,并且将来可能会与我们在检查器中使用的递归调控器发生冲突。

不要这样做!

☹️

@jcalz那么#26980 存在的更多理由是什么?

当我在今年的休息时间开始使用 TS 时,我的倾向是_just that_! ( ...T ) 希望它成为可变类型变量元组的语法。 好吧,希望这能进来:)

刚刚发现[...T, ...U]的新用途:正确键入 HTML 构建器。 举个具体的例子, <video>的孩子必须是以下的:

  • 如果元素具有src属性:

    • 零个或多个<track>元素

  • 如果元素没有src属性:

    • 零个或多个<source>元素

  • 根据父级内容模型的零个或多个元素,除了不允许audiovideo后代元素。

这基本上等同于这种类型,但今天没有办法在 TypeScript 中表达这一点:

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

3.5 年:/

用例:

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

我只看到提案的开放问题部分中简要提到的重载,但绝对是我遇到的并且非常适合看到解决方案或提案的内容,例如:

  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

目前这根本不起作用,只是将promisify键入为(ctx: ICtx, filter: FilterQuery<TSchema>) => Promise<TSchema[]> ,这会丢失这些签名中的信息。

AFAICT 唯一真正的解决方案基本上是创建一个承诺函数并手动为该变体指定所有可能的类型 - 从包装的变体中实际提供受尊重的签名的唯一方法是不指定重载而只指定实现签名,但是如果您以这种方式指定签名,则无法让调用者根据他们传递的参数知道他们应该期望哪种返回类型。

由于 rest 参数只能是最后一个参数(即(cb, ...args)有效但不是(...args, cb) ,因此即使签名在内部是联合类型,您实际上也不能正确地传播事物 - 例如,如果cb始终是将 promisify 类型化为function promisify<T, V extends any[]>(fn: (cb: (err: Error | null, res?: T) => void, ...args: V)): (...args: V) => T的第一个参数,并且您至少可以获得具有相同返回响应的签名的联合类型,那将是相当简单的,但是因为这是最后一个参数 afaict 这里没有太多可以做的

@Qix- 您的场景已由 #24897 启用。 已在 TS 3.0 中实现。

@ahejlsberg Oof ! 太棒了,谢谢你♥️

等待了很长时间......但是今天可以编写可变参数类型。 TS 已经足够成熟,可以编写有效的复杂类型。 所以我花时间为Ramda编写了compose 和 pipe 的类型。

现在随ts-toolbelt一起提供。

然而,这个提议是一个很好的语法糖,使常见的元组操作更容易。

你已经在medium.com上了吗? 网址?

Medium上有原始文章,但奖金不包括在内,它在 repo 中。 它还解释了我如何创建所有的小工具来编写、管道和咖喱 :smile:

@pirix-gh 但这不是本提案中的可变参数泛型

declare function m<...T>(): T

m<number, string>() // [number, string]

@goodmind是的,它不是,它被更多地模仿。 所以你可以像这样模拟...

declare function m<T extends any[], U extends any[]>(): Concat<T, U>

m<[number, string], [object, any]>() // [number, string, object, any]

是相同的:

declare function m<...T, ...U>(): [...T, ...U]

m<number, string, object, any>() // [number, string, object, any]

与此同时,在等待这个提议的时候:hourglass_flowing_sand:

@pirix-gh 你能帮忙包装函数吗

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

我试图使用 compose 方法但失败了,请您建议一种正确的方法吗?

发生这种情况是因为当您提取依赖于泛型的fn的参数/返回时,TS 会将它们推断为它们最接近的类型(在这种情况下T将是any )。 所以目前没有办法做到这一点。 我们最大的希望是等待这个提案与https://github.com/Microsoft/TypeScript/pull/30215结合

或者,也许我们可以找到一种方法来保留/移动泛型,我们可以这样做:

declare function ideal<...T>(a: T[0], b: T[1], c: T[2]): T

ideal('a', 1, {}) // T = ['a', 1, {}]

这样,我们就可以从它的片段中重建fn 。 今天缺少的部分是像@goodmind指出的通用部分。

@pirix-gh 如果我没记错的话,你可以这样做来实现你在那里的目标:

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不完全是,因为我提出的建议适用于无限数量的参数。 但也许我们也可以这样做,使用您提出的建议(我在提案中没有看到):

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 示例中的ABC类型参数未使用。

-declare function MyFunction<A, B, C, ...Args>(...[a, b, c]: Args): Args
+declare function MyFunction<...Args>(...[a, b, c]: Args): Args

即使实现了可变参数类型,前两个示例也可能会产生编译错误,如果您只需要三个参数,可变参数类型有什么意义。

你的例子应该说明为什么需要可变参数,如果它们可以用现有的 TS 代码完成,那么它根本无济于事。

@goodmind是的,它不是,它被更多地模仿。 所以你可以像这样模拟...

declare function m<T extends any[], U extends any[]>(): Concat<T, U>

m<[number, string], [object, any]>() // [number, string, object, any]

是相同的:

declare function m<...T, ...U>(): [...T, ...U]

m<number, string, object, any>() // [number, string, object, any]

与此同时,在等待这个提议的同时⏳

你从哪里得到的Concat<>

编辑:没关系找到源代码。

@pirix-gh 所以我试图用你的建议来做这件事,但无法弄清楚。

~问题是我试图扩展一个类的ctor的参数,它的工作原理是我有一个类型数组,但我不能为ctor参数传播它们。~

Class Test {
  constructor(x: number, y: string) {}
}
let ExtendedClass = extendCtor<[number, string], [number]>(Test);

let instance = new ExtendedClass(1, '22', 2);

更新:没关系,这也可以通过在 ctor 函数中使用扩展来实现。

这是解决方案的链接

唯一的问题是 TS 几乎每次都崩溃:|
这就是 TypeScript 所说的Type instantiation is excessively deep and possibly infinite.ts(2589)

更新 2:
我通过将新类型放在开头来实现它,但能够合并这些类型仍然很好。

// ...
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');

与:

let MyClass1 = extendClass<typeof Test, string>(Test);

let instance = new MyClass1('check', '22', 2);

链接到最终解决方案。

如果我理解正确Object.assign可以声明如下内容以完全支持可变参数。

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

是否有任何理由在 TypeScript 的lib.d.ts中以不同的方式声明它?

它不使用递归条件类型,因为它们不受支持(请参阅 #26980 以进行讨论,或此评论告诉我们不要这样做)。 如果愿意使用当前的交集返回类型,则有 #28323。

@jcalz我创建了一个繁重的测试,显示Minus类型在起作用。 它在不到 4 秒的时间内执行了Minus 216000 次。 这表明 TS 可以很好地处理递归类型。 但这是最近的事情。

为什么? 这要归功于 Anders :tada: (https://github.com/microsoft/TypeScript/pull/30769)。 他允许我从条件类型切换到索引条件(如开关)。 事实上,它提高了 ts-toolbelt 的性能 x6。 非常非常感谢他。

所以从技术上讲,我们可以使用 ts-toolbelt 安全地重写@kimamula的类型。 复杂度遵循 O(n):

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'},
]>

该库还使用Iteration使递归安全,这将防止 TypeScript 的任何溢出。 换句话说,如果I超过40那么它就会溢出并且Pos<I>等于number 。 从而安全地停止递归。

我写的一个类似的递归类型 ( Curry )随 Ramda 一起提供,看起来它做得很好。

顺便说一句,我在项目页面上感谢您(@jcalz)提供的所有好建议。

我不确定 #5453 是否是进行此讨论的最佳场所...我们应该在 #26980 中讨论这个问题还是有更规范的位置? 在任何情况下,我很想有一个官方和支持的方式来做到这一点,这将不可能在打字稿的后续版本崩溃。 一些包含在他们的基线测试中的东西,这样如果它坏了,他们就会修复它。 即使性能被测试为良好,我也会警惕在任何生产环境中这样做,而没有像@ahejlsberg 这样的人的官方消息。

一些包含在他们的基线测试中的东西,这样如果它坏了,他们就会修复它。

我认为我们内部使用了一些非常接近的东西

@weswigham原谅我过于密集,但你能告诉我突出显示的类型是如何递归的吗? 我担心的是形式

type Foo<T> = { a: Foo<Bar<T>>, b: Baz }[Qux<T> extends Quux ? "a" : "b" ]

或我见过的任何变体。 如果我遗漏了一些东西并且这已经被批准了,请有人告诉我(并教我如何使用它!)

哦,公平 - 在这方面是不同的,是的。 我只是说“立即索引对象以选择类型”模式并意识到我们有_那个_。

我有个问题。

这里提出的内容有多少仍然相关? 这个问题是 4 年前打开的,我觉得从那时起很多东西都发生了变化。

从我这里的评论来看,
https://github.com/microsoft/TypeScript/issues/33778#issuecomment -537877613

我说,

TL;DR,元组类型,rest args,映射数组类型,非rest arg的元组推断,递归类型别名=不需要变量类型参数支持

但是我很想知道是否有人拥有现有工具根本无法启用的用例

在我们获得官方祝福的Concat<T extends any[], U extends any[]>版本之前,这仍然是相关的。 我不认为即将到来的递归类型引用功能会告诉我们这一点,但我很高兴(权威地)被告知否则。

我们不是已经有Concat<>实现了吗?

还是这里的关键词是“官方祝福”?

因为我的主张是你基本上可以做你现在想做的一切(或几乎所有事情?),即使它不是“官方祝福”。

但我想“官方祝福”应该总是首选......好点。 我太习惯 (ab) 使用那些递归类型别名

我通常更喜欢真实、优雅的语法,这样每次我做这样的事情时,我都不必一直向我的(通常是初级)队友解释现状滥用所必需的令人困惑的指定类型的情况。 这种混淆损害了我在组织中传播 TypeScript 的能力,或者至少是它的这些用途。

大👍!

这个功能非常重要。

@AnyhowStep

因为我的主张是你基本上可以做你现在想做的一切(或几乎所有事情?)

我没有看到任何证据表明今天在 TS 中可以轻松实现同时输入多个单个参数的扩展参数。

@matthew-dean 不完全正确。 这是您可能在某种程度上实现的示例

据我了解,TS 试图输入尽可能多的 vanilla JS 程序。 这是一个谜题:

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

我希望那里的类型不会比[number, ...T, number, ...T, number]更复杂。 如果我们必须写 20 行一些奇怪的代码来滥用发生检查中的错误以在最后一行中有正确的返回类型,则此问题没有解决。

@polkovnikov-ph 该类型目前被推断为: [number, ...any[]] ,这是没有帮助的。

另外我想指出,我们不必像 C++ 那样遵守Greenspun 的第十条规则长达 15 年,因为 C++ 已经为我们经历了所有的Head<>Cons<> ,并且设计了一些非常方便和干净的可变参数模板语法。 我们可以节省(数百年)开发人员的时间,并从中提取最好的部分。

例如,可变参数类型在 C++ 中具有不同的种类,因此您不能在需要类型的地方使用可变参数类型变量,这与 TS 中extends any[]的类型不同。 这允许 C++ 通过在包含在省略号运算符中的某些表达式中提及可变参数类型变量来映射/压缩元组。 这几乎是映射对象类型的元组替代方案。

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]

请注意,示例中使用建议的省略号语法而不是extends any[]不仅是出于审美原因,而且是因为

type A<T> = any[]
type B<T extends any[]> = [...A<T>]
type C = B<[1, 2]>

已经是一个有效的 TS 程序。 C最终成为any[]而不是映射的可变参数类型会生成的[any[], any[]]

@DanielRosenwasser我很抱歉给你这样的 ping,但我想重新提出这个问题以增加它得到一些爱的机会。 可变参数将非常有用,尽管我意识到实现它们是一项艰巨的任务!

顺便说一句,仅对元组类型进行类型级别的扩展操作对我的团队来说将是一个巨大的帮助,即使缺少可变类型意味着它们不能与类型参数一起使用。 在我们的问题域中,“具有某种结构的数组”非常常见。 如果此操作有效,它将为我们大大简化事情:

type SharedValues = [S1, S2, S3];
type TupleOfSpecificKind = [V1, ...SharedValues, V2];

@sethfowler如果你有一些你想表达的东西的例子,这对我们总是有帮助的。 否则你可能对https://github.com/microsoft/TypeScript/issues/26113感兴趣

@DanielRosenwasser当然,我可以让事情更具体一点。 我将不涉及细节,但在较高的层次上,您可以将我们的项目视为生成发送到远程服务器的图形操作流和其他类似事件。 出于效率原因,我们需要以可直接转换为序列化形式的格式来表示内存中的这些操作。 这些事件的类型最终看起来像这样:

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

我们希望能够更像这样表达这些类型:

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

我们有大量的这些命令,因此仅具有类型级别的传播将导致代码的可维护性大大提高。 拥有可变参数类型以便我们可以编写像Repeated<>RepeatedUpToTimes<>这样的类型级函数(在这个例子中它们将计算为递归定义的元组类型联合)将进一步简化事情。

支持元组类型的类型安全连接(如 OP 中所述)等内容也将非常有用。 为了使用上述类型,我们目前必须在单个元组文字表达式中构造整个元组。 我们现在不能分部分构建它并将它们连接在一起。 换句话说,下面的操作今天不起作用,但我们真的希望它们起作用。

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

希望这些例子有帮助!

您可以使用Concat<>轻松编写Concat<>类型,并创建Concat3<>类型。

然后,

type OpCircle = Concat3<Colors, Vertex3D, [number] /* radius */>;

从上面,您可以编写一个具有 2、3、4、5、6 等重载的 concat 函数。 参数的数量。

甚至可以编写一个 Concat<> impl,它接受一个元组元组并连接元组。 一个可变参数 Concat<> 类型。


这不是今天做不到的事情。 它可以完成,即使它需要您编写递归类型和辅助函数。

每次当我在 vscode 中使用这些递归类型时,TS 想杀死 CPU 或者它只是挂起! 这是主要问题,我觉得 TS 无缘无故变得太重了。

也许编写类型的人在优化它方面做得还不够?

我不想垃圾邮件或拖入不会为该线程增加太多价值的旧对话,但我记得在某些时候听说递归条件类型的计算在 javascript 中很昂贵(我在此线程中找到了它)

也就是说(我知道这听起来很疯狂,)也许是时候用另一种语言重写 TS 以便能够提升类型系统,因为 TS 已经发展得如此之快,以至于要求更好的语言是合理的。

您可以使用Concat<>轻松编写Concat<>类型,并创建Concat3<>类型。

您能否为您描述的Concat<>类型提供一个实现? 编写Cons<>很容易,但Concat<>并不那么容易(对我来说),我很想看看你在想什么。

关于Concat3<>Concat4<>等,希望从长远来看我们不需要编写几十个这样的变体,因为我们将拥有可变参数类型。 🙂 如果今天可以很好地实施它们,那将是一个合理的权宜之计。

对于两个元组的常规连接,
https://github.com/AnyhowStep/ts-trampoline-test (使用蹦床连接非常大的元组,大多数人不需要)

Concat3就是 Concat, C>

VarArgConcat 将是,
VarArgConcat<TuplesT extends readonly (readonly unknown[])[], ResultT extends readonly unknown[] = []>

虽然元组不为空,但VargArgConcat<PopFront<TuplesT>, Concat<ResultT, TuplesT[0]>>

如果 TuplesT 为空,则返回 ResultT

当然,朴素的递归将导致具有合适长度的元组的最大深度错误。 因此,要么使用 ts-toolbelt 中的递归技术,要么使用带有复制粘贴到所需深度的蹦床


我链接到的那个 repo 使用Reverse<>来实现Concat<> 。 我从我正在处理的另一个项目中复制粘贴了代码。

我同意这将是一个非常有用的功能。

假设我们有一个类型T

type T = {
  tags: ["a", "b", "c"];
};

我们想要创建一个新类型,将附加标签"d"添加到T["tags"]元组中。 用户最初可能会尝试创建此实用程序 ( WithTag<NewTag, ApplyTo> ),如下所示:

type WithTag<
  Tag extends string,
  Target extends {tags: string[]}
> = Target & {
  tags: [Tag, ...Target["tags"]];
};

尝试此操作当前会引发错误A rest element type must be an array type 。 用户可能认为将string[]换成Array<string>会有所作为,但事实并非如此。 也不使用条件 + never

type WithTag<
  Tag extends string,
  Target extends {tags: string[]}
> = Target & {
- tags: [Tag, ...Target["tags"]];
+ tags: Target["tags"] extends string[] ? [Tag, ...Target["tags"]] : never;
};

游乐场链接: https://www.typescriptlang.org/play?#code/C4TwDgpgBA6glsAFgFQIYHMA8AoKU3pQQAewEAdgCYDOU1wATnOegDS76oPoTBGkUaUAN7AM1AFx1GzdAG0AugF9sAPigBeTt15QAZCI5j0kqHIKsoAOhtodwOQCJj1RwoUBubEq -ZQkKABJTUM8FyknVEdLRwAjaKhHAGM3Lx9sP3BoZBD4JAJMR0oEwNVfAHoAKkrcSqgAUWJIJLJKKAADZHaoYAB7KFjoXoAzHsRoYd6AGynegHdZHqyrWqhV4VWe8QjHKJj4mJSY4s9VlShK8qA

相关问题:

不幸的是,此策略不适用于其余参数。 这些只是变成了数组:

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

在这里,咖喱: ...([string, boolean[]] | [boolean[]]) => number
我认为如果有一个带有元组 rest 参数的函数的特殊情况,元组的最后一个元素是一个数组,那么这可以得到支持。
在这种情况下,函数调用将允许正确类型的额外参数与数组匹配。
然而,这似乎太复杂了,不值得。

这有两个问题:

  1. 代码不正确。 curried()不接受数组。 用[true, false]填充c rest 参数可以通过curried('foo', ...[true, false])来完成,但这将在 TypeScript 中失败并使用此建议。 我们可能无法为某些情况提供打字解决方案,但不鼓励提供错误的人!
  2. 无意中,你结合了 optional 和 rest 参数,并在你的提案中发现了一个错误。 curried()不能在没有b情况下调用,但有c 。 这样做会导致不当行为。 TypeScript 知道curried()(...items: [string, boolean[]] | [boolean[]])但这不是真的。 因为 JavaScript 是不需要输入的,所以将[true, false]传递给c (假设我们解决了上述问题)和curried([true, false])不会将bundefined (或其默认值)和c[true, false] ,但会将btruec[false]

我建议进行以下修复:

  1. 对于第二个(更简单的)问题,解决方案很简单:当有 rest 参数时,不要为最后一个可选参数(即[number, string, boolean[]] | [number, boolean[]]在我们的例子中)推断联合元组。 相反,推断[number, string, boolean[]] | [number] - 即,完整签名的一种情况,包括所有可选项和其余部分,一种用于除最后一个其余部分。
  2. 第一个问题更棘手:你已经说过你认为它太复杂而不值得。 鉴于rest参数的流行,我认为这是值得的,但由于第一个问题(胜利!😄),这是必要的。 我认为如果我们公开 tuple-with-last-rest-array 的接口会很好(我考虑语法[t1, t2, t3, ...arr] ),但我们不需要。 我们可以将其作为内部使用(哈哈,您仍然需要处理如何在 IDE 中显示类型😈)。

但毕竟是抱怨和挑衅,伟大的提议! 谢谢👍(只是为了安慰你,这是我在 GitHub 上第一次用三个表情符号回应的问题 - 👍、🎉 和 ❤️)。

这在当前必须使用any https://github.com/angular/angular/issues/37264的 Angular Injector 中非常有用

在此示例中,A、B、C 可以表示为单个...A可变参数泛型类型。 但我不知道这将如何映射到可变泛型的每个元素都包含在另一种类型( Type )中的东西。 也许有帮手类型? 或者语法应该允许类似...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>];
}

(上下文: Provider将按照该顺序将deps实例注入到该工厂函数中。严格的类型将确保开发人员知道将注入什么以及以什么顺序注入。)

完成后,请记住更新 String.prototype.replace 的第二个参数,以便它最终在 Typescript 中正确键入!

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_function_as_a_parameter

@Griffork您确实意识到需要解析正则表达式才能确定它有多少个捕获组,对吗?

这在当前必须使用any angular/angular#37264的 Angular Injector 中非常有用

在此示例中,A、B、C 可以表示为单个...A可变参数泛型类型。 但我不知道这将如何映射到可变泛型的每个元素都包含在另一种类型( Type )中的东西。 也许有帮手类型? 或者语法应该允许类似...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>];
}

(上下文: Provider将按照该顺序将deps实例注入到该工厂函数中。严格的类型将确保开发人员知道将注入什么以及以什么顺序注入。)

@AlexAegis

我觉得它会被输入如下:

export declare interface TypedFactoryProvider<T, ...P> {
  provide: Type<T | T[]> | InjectionToken<T | T[]>;
  multi?: boolean;
  useFactory: (...providers: ...P) => T;
  deps: [...Type<P>];
}

此问题现已由 #39094 修复,预定用于 TS 4.0。

如果这是 4.0,我们现在有理由将其命名为 4.0 😃
这真的是一个重大的新功能🎉

这很棒! 对于文字字符串类型,只有“左”是相同的

@sandersn我正在考虑如何在RxJS类的东西中使用这种语法,其中pipe方法参数相互依赖,

pipe(map<T, V>(...), map<V, U>(...), filter(...), ...) 。 你会如何以一种他们现在不这样做的方式输入它? (几十行不同的可变长度打字)

@gioragutt使用@ahejlsberg提交的 PR 我认为这会起作用,但我可能是错的 😄

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不太有效。

但是,通常的解决方法有效

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这似乎对我不起作用。 😢
操场示例

我有一些更接近工作的东西,但它不会推断出类型。
游乐场示例

我不得不改变
R extends readonly [unknown] ? 1 : 0

R extends readonly [infer _, ...infer __] ? 1 : 0

不知道为什么

@tylorr @treybrisbane可能相关: https :

此外,在任何一种情况下,我都强烈建议在评论所在的拉取请求中分享。

可变元组类型是该语言的一个很棒的补充,感谢您的努力!

看起来,像curry这样的构造也可能会受益(刚刚用staging playground测试过):

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

通用函数不适用于上述咖喱。

我不相信这个 PR 解决了这个特定问题 tbh。

有了公关,这有效

function foo<T extends any[]>(a: [...T]) {
  console.log(a)
}

foo<[number, string]>([12, '13']);

但就我所见,这个问题希望看到一个实现:

function bar<...T>(...b: ...T) {
  console.log(b)
}

bar<number, string>(12, '13');

那里有很多尖括号,看起来有点多余。

@AlexAegis我不确定我在“休息类型参数”中看到了很多价值。 你已经可以这样做了:

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

不要以为我们真的想要一个全新的概念(即剩余类型参数),只是为了在推理无法解决的极少数情况下避免使用方括号。

@ahejlsberg我明白了。 我问是因为一些库(提到的 RxJS)使用变通方法来提供此功能。 但它是有限的。

bar<T1>(t1: T1);
bar<T1, T2>(t1: T1, t2:T2);
bar<T1, T2, T3>(t1: T1, t2:T2, t3: T3, ...t: unknown) { ... }

所以现在他们要么坚持,要么让用户输入括号,这是一个突破性的变化,而不是那么直观。

我使用这个例子的原因是因为在这里我定义了那个元组的类型很简单。 一个方括号在这里,一个那里

foo<[number, string]>([12, '13']);

如果你从外面看,元组引用那个 rest 参数并不那么明显

foo<[number, string]>(12, '13'); 

但是是的,正如您所说,如果我们让推理弄清楚,那么这些微不足道的情况不需要用户进行任何修改。 但是我们不知道他们是否明确设置了它们,这取决于他们,所以它仍然算作一个重大更改。 但这是 lib 的关注点,而不是这个变化的关注点。

也就是说,我只是觉得奇怪,如果有从外部一个接一个定义的其余参数,这些参数是内部由...区分的单个数组,则不能以相同的方式通用:一个接一个在外面,在里面的单个数组,由...区分。

轻微的语法差异并不值得为一个单独的支持成本
种类。 当 TS 计划时,使用种类将是一个正确的设计决策
支持休息参数,但我想现在它可能会导致更多
语言开发人员和用户都感到困惑。 我们需要一个解决方案
这个问题,而安德斯出色地避免了这个问题
通过坚持[...T]而不是T来实现复杂性。 脱帽致敬!

(我们现在可以看看一个将交集类型统一为
条件类型中的推断变量返回最右边的交集类型
参数,或者数组的联合不是联合数组? 我们仍然
在类型系统中有主要表现。)

2020 年 6 月 19 日,星期五,10:41 Győri Sándor [email protected]写道:

@ahejlsberg https://github.com/ahejlsberg我明白了。 我问是因为
一些库(提到的 RxJS)使用变通方法来提供这个
功能。 但它是有限的。

酒吧(t1: T1);酒吧(t1: T1, t2:T2);bar(t1: T1, t2:T2, t3: T3, ...t: 未知) { ... }

所以现在他们要么坚持下去,要么让用户输入括号,
这不是那么直观。

我使用这个例子的原因是因为这里很简单
我定义了那个元组的类型。 一个方括号在这里,一个那里

foo<[数字, 字符串]>([12, '13']);

在这里,元组指的是那个 rest 参数并不是那么明显,如果
你从外面看

foo<[数字, 字符串]>(12, '13');


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646490130
或取消订阅
https://github.com/notifications/unsubscribe-auth/AAWYQIMTTB6JEPSQFUMTMDTRXMJD5ANCNFSM4BTBQ7DQ
.

我当然远不及他的能力,但我恭敬地不同意@ahejlsberg
根据我的经验,打字稿的大部分复杂性来自这样一个事实,即许多(有趣且有用的)功能都作为它们自己的概念进行了特殊处理。

不过,这种复杂性本质上并不是特征数量的函数!
相反,该语言可以围绕更大、更全面的概念进行设计,然后可以从中轻松推导出这些特殊情况,或在 std(类型)库中实现。

最普遍的这样的概念当然是完全实现依赖类型,然后可以从中派生出其他所有类型,但没有必要走那么远:
正如 C++ 以及在较小程度上 Rust 所展示的那样,一些大规模、一致的概念为您免费提供了大量功能。
这类似于 OCaml 和 Haskell(我假设 F#?)在值级别上所做的,只是在类型级别上。

类型级编程没有什么可害怕的,只要它被设计到语言中而不是附加到提供特定功能。
C++ 14/17 中的工具非常直观,除了它们的语法,这纯粹是由于历史包袱。

可以在原始设计中添加总体概念。 设计后
已经犯了错误,不能在不冒巨大风险的情况下增加一致性
背面不兼容。 我同意对语言设计的怀疑
一个整体(TS离学术界制定的标准还很远,没有人可以
不同意这一点)。 有很多错误和不一致之处
数以百万计的生产代码库的基础。 仅仅是事实
开发人员能够为该语言提出有用的补充
在我看来,如果不意外修复这些错误,那就太棒了
并值得尊重。 TS 在这里与 C++ 具有相同的设计复杂性,但它的
富有表现力的类型系统使情况变得更糟。

在周五,2020年6月19日,12:47贝内特Piater [email protected]写道:

我当然远不及他的能力,但我恭敬地不同意
@ahejlsberg https://github.com/ahejlsberg
根据我的经验,打字稿的大部分复杂性来自事实上,许多(有趣且有用的)功能是特殊情况下作为他们自己的概念。

这种复杂性本质上不是特征数量的函数
尽管!
相反,该语言可以围绕更大、更全面的设计
可以从中轻松推导出这些特殊情况的概念,或
在 std(类型)库中实现。

最普遍的这样的概念当然是完全实施
依赖类型,然后可以从中派生出其他所有类型,但是
没有必要走那么远:
正如 C++ 以及在较小程度上,Rust 所展示的,一些大规模的,
一致的概念免费为您提供大量功能。
这类似于 OCaml 和 Haskell(我假设是 F#?)
价值层面,就在类型层面。

类型级编程没什么好害怕的,只要它是
设计成语言而不是附加到提供特定的
特征。
C++ 14/17 中的工具非常直观,除了它们的语法,
这纯粹是由于历史包袱。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646543896
或取消订阅
https://github.com/notifications/unsubscribe-auth/AAWYQIMWYLGGCWPTDBZJR4TRXMX4RANCNFSM4BTBQ7DQ
.

@polkovnikov-ph 我很高兴我们就手头的问题达成一致:)

至于解决方案,我认为仍然值得考虑逐步转向更精心设计的类型系统。 主要版本是毕竟的事情,并且是替代的簇* *是C ++ 20在结束了-在添加上的2层的先前尝试的不能被除去顶部更很好的设计特征的极佳的尝试,在已经无法确定性解析的语法。

所有这些都与本主题无关,正在此处讨论。 所以我会尽量坦率地说:

学术界花了几十年的时间才找到正确的子类型方法:mlsub 类型系统仅在 6 年前创建,就在 TypeScript 首次发布之后。 它可能是具有总体概念的类、接口、联合和交集类型的基础。

但也要记住有条件类型。 我不知道有任何论文给他们提供了正式的语义,或者描述了一个带有进度/保存证明的条件类型的最小类型系统。 我相信这可能与科学家仍然羞于打印他们失败的尝试有关。 如果你的提议假设那些主要的不兼容版本将在 2040 年代制作,当学术界对条件类型感到满意时,我可以同意。

否则,“精心设计的类型系统”将不得不从语言中删除条件类型,而且我认为没有人能胜任将 60% 的绝对类型转换为使用任何替代选择来替换它们的任务。 (然后再做几次,因为这不是唯一的问题。)

恐怕唯一可行的解​​决方案是创建一种在某种程度上类似于 TS 的单独编程语言,并以某种方式(不仅是通过更愉快地编写代码)吸引开发人员使用它。 Ryan 之前非常直言不讳地推荐这种方法来改进 TS。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

nitzantomer picture nitzantomer  ·  135评论

rwyborn picture rwyborn  ·  210评论

Gaelan picture Gaelan  ·  231评论

disshishkov picture disshishkov  ·  224评论

Taytay picture Taytay  ·  174评论