This proposal lets Typescript give types to higher-order functions that take a variable number of parameters.
Functions like this include concat
, apply
, curry
, compose
and almost any decorator that wraps a function.
In Javascript, these higher-order functions are expected to accept variadic functionsas arguments.
With the ES2015 and ES2017 standards, this use will become even more common as programmers start using spread arguments and rest parameters for both arrays and objects.
This proposal addresses these use cases with a single, very general typing strategy based on higher-order kinds.
This proposal would completely or partially address several issues, including:
I'll be updating this proposal on my fork of the Typescript-Handbook: sandersn/TypeScript-Handbook@76f5a75868de3fb1ad4dbed5db437a8ab61a2698
I have an in-progress implementation at sandersn/TypeScript@f3c327aef22f6251532309ba046874133c32f4c7 which currently has the simple parts of the proposal implemented.
It supercedes part 2 of my previous proposal, #5296.
Edit: Added a section on assignability. I'm no longer sure that it strictly supercedes #5296.
curry
curry
for functions with two arguments is simple to write in Javascript and Typescript:
function curry(f, a) {
return b => f(a, b);
}
and in Typescript with type annotations:
function curry<T, U, V>(f: (t: T, u: U) => V, a:T): (b:U) => V {
return b => f(a, b);
}
However, a variadic version is easy to write in Javascript but cannot be given a type in TypeScript:
function curry(f, ...a) {
return ...b => f(...a, ...b);
}
Here's an example of using variadic kinds from this proposal to type curry
:
function curry<...T,...U,V>(f: (...ts: [...T, ...U]) => V, ...as:...T): (...bs:...U) => V {
return ...b => f(...a, ...b);
}
The syntax for variadic tuple types that I use here matches the spread and rest syntax used for values in Javascript.
This is easier to learn but might make it harder to distinguish type annotations from value expressions.
Similarly, the syntax for concatenating looks like tuple construction, even though it's really concatenation of two tuple types.
Now let's look at an example call to 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')();
In the first call,
V = [number, number, string, string]
...T = [number, number]
...U = [string, string]
In the second call,
V = [number, number, string, string]
...T = [number, number, string, string]
...U = []
The syntax of a variadic kind variable is ...T
where _T_ is an identifier that is by convention a single upper-case letter, or T
followed by a PascalCase
identifier.
Variadic kind variables can be used in a number of syntactic contexts:
Variadic kinds can be bound in the usual location for type parameter binding, including functions and classes:
function f<...T,...U>() {}
}
class C<...T> {
}
And they can be referenced in any type annotation location:
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];
}
Variadic kind variables, like type variables, are quite opaque.
They do have one operation, unlike type variables.
They can be concatenated with other kinds or with actual tuples.
The syntax used for this is identical to the tuple-spreading syntax, but in type annotation location:
let t1: [...T,...U] = [...ts,...uProducer<...U>()];
let t2: [...T,string,string,...U,number] = [...ts,'foo','bar',...uProducer<...U>(),12];
Tuple types are instances of variadic kinds, so they continue to appear wherever type annotations were previously allowed:
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);
A variadic kind variable represents a tuple type of any length.
Since it represents a set of types, we use the term 'kind' to refer to it, following its use in type theory.
Because the set of types it represents is tuples of any length, we qualify 'kind' with 'variadic'.
Therefore, declaring a variable of variadic tuple kind allows it to take on any _single_ tuple type.
Like type variables, kind variables can only be declared as parameters to functions, classes, etc, which then allows them to be used inside the body:
function f<...T>(): ...T {
let a: ...T;
}
Calling a function with arguments typed as a variadic kind will assign a specific tuple type to the kind:
f([1,2,"foo"]);
Assigns the tuple type ...T=[number,number,string]
...T.
So in this application of
f,
let a:...Tis instantiated as
let a:[number,number,string].
However, because the type of
ais 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` is allowed.
For example, new elements can be added to the tuple:
function cons<H,...Tail>(head: H, tail: ...Tail): [H,...Tail] {
return [head, ...tail];
}
let l: [number, string, string, boolean];
l = cons(1, cons("foo", ["baz", false]));
Like type variables, variadic kind variables can usually be inferred.
The calls to cons
could have been annotated:
l = cons<number,[string,string,boolean]>(1, cons<string,[string,boolean]>("foo", ["baz", false]));
For example, cons
must infer two variables, a type _H_ and a kind _...Tail_.
In the innermost call, cons("foo", ["baz", false])
, H=string
and ...Tail=[string,boolean]
.
In the outermost call, H=number
and ...Tail=[string, string, boolean]
.
The types assigned to _...Tail_ are obtained by typing list literals as tuples -- variables of a tuple type can also be used:
let tail: [number, boolean] = ["baz", false];
let l = cons(1, cons("foo", tail));
Additionally, variadic kind variables can be inferred when concatenated with types:
function car<H,...Tail>(l: [H, ...Tail]): H {
let [head, ...tail] = l;
return head;
}
car([1, "foo", false]);
Here, the type of l
is inferred as [number, string, boolean]
.
Then H=number
and ...Tail=[string, boolean]
.
Concatenated kinds cannot be inferred because the checker cannot guess where the boundary between two kinds should be:
function twoKinds<...T,...U>(total: [...T,string,...U]) {
}
twoKinds("an", "ambiguous", "call", "to", "twoKinds")
The checker cannot decide whether to assign
...T = [string,string,string], ...U = [string]
...T = [string,string], ...U = [string,string]
...T = [string], ...U = [string,string,string]
Some unambiguous calls are a casualty of this restriction:
twoKinds(1, "unambiguous", 12); // but still needs an annotation!
The solution is to add type annotations:
twoKinds<[string,string],[string,string]>("an", "ambiguous", "call", "to", "twoKinds");
twoKinds<[number],[number]>(1, "unambiguous", 12);
Uncheckable dependencies between type arguments and the function body can arise, as in 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);
This function can be typed, but there is a dependency between n
and the kind variables: n === ...T.length
must be true for the type to be correct.
I'm not sure whether this is code that should actually be allowed.
The semantics are the same on classes and interfaces.
TODO: There are probably some class-specific wrinkles in the semantics.
Tuple kinds can be used to give a type to rest arguments of functions inside their scope:
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']);
In this example, the parameter list of f: (a: number, b:string) => string
must be assignable to the tuple type instantiated for the kind ...T
.
The tuple type that is inferred is [number, string]
, which means that (a: number, b: string) => string
must be assignable to (...args: [number, string]) => string
.
As a side effect, function calls will be able to take advantage of this assignability by spreading tuples into rest parameters, even if the function doesn't have a tuple kind:
function g(a: number, ...b: [number, string]) {
return a + b[0];
}
g(a, ...[12, 'foo']);
Since tuples can't represent optional parameters directly, when a function is assigned to a function parameter that is typed by a tuple kind, the generated tuple type is a union of tuple types.
Look at the type of h
after it has been curried:
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
Here ...T=([number] | [number, string])
, so curried: ...([number] | [number, string]) => number
which can be called as you would expect. Unfortunately, this strategy does not work for rest parameters. These just get turned into arrays:
function i(a: number, b?: string, ...c: boolean[]): number {
}
let curried = curry(i, 12);
curried('foo', [true, false]);
curried([true, false]);
Here, curried: ...([string, boolean[]] | [boolean[]]) => number
.
I think this could be supported if there were a special case for functions with a tuple rest parameter, where the last element of the tuple is an array.
In that case the function call would allow extra arguments of the correct type to match the array.
However, that seems too complex to be worthwhile.
Most of these examples are possible as fixed-argument functions in current Typescript, but with this proposal they can be written as variadic.
Some, like cons
and concat
, can be written for homogeneous arrays in current Typescript but can now be written for heteregoneous tuples using tuple kinds.
This follows typical Javascript practise more closely.
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: Could f
return ...U
instead of 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);
}
}
bind
, call
and apply
are methods defined on Function, their type arguments need to be bound at function-creation time rather than the bind
call site (for example). But this means that functions with overloads can't take or return types specific to their arguments -- they have to be a union of the overload types. Additionally, Function doesn't have a constructor that specifies type arguments directly, so there's really no way provide the correct types to bind
et al. TODO: Add an example here. Note that this problem isn't necessarily unique to variadic functions. +1, this is really useful for functional programming in TypeScript! How would this work with optional or rest arguments? More concrete, can the compose
function be used on functions with rest arguments or optional arguments?
Good point. I think you could assign the smallest allowed tuple type to an optional-param function since tuple are just objects, which allow additional members. But that's not ideal. I'll see if I can figure out the compose
example and then I'll update the proposal.
Actually union types would probably work better. Something like
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
This still breaks rest parameters, though.
@ahejlsberg, you had some ideas how tuple kinds would work, I think.
So :+1: on this. For information this is related to (and would fulfill) #3870. We have tried to implement a compose type API in TypeScript but are having to work around some of the limitations noted in this proposal. This would certainly solve some of those problems!
It seems though that sometimes you may want to "merge" such tuple types instead of persisting them, especially with something like compose. For example:
function compose<T, ...U>(base: T, ...mixins: ...U): T&U {
/* mixin magic */
}
Also, in a lot of your examples, you have been using primitives. How would you see something more complex working, especially if there are conflicts?
Unfortunately this proposal as-is does not address #3870 or the type composition, since the only composition operator for tuple kinds is [T,...U]
. You could also write this as T + ...U
(which is more indicative of what happens to the types), but #3870 and your type composition library need T & ...U
. I think that might be possible, but I need to understand @JsonFreeman's and @jbondc's ideas from #3870 first. I'll expand the proposal if I can figure out how it should work.
Note: I decided to go with the syntax [...T, ...U]
because it looks like the equivalent value spreading syntax, but T + ...U
is more indicative of what's happening with the types. If we end up with both, then +
and &
might be the operators to use.
Big :+1: on this!
+1 awesome! It would allow to express such things much more expressive and lightweight.
My point in #3870 seems to be an issue here. Specifically, I worry about inferring type arguments for variadic type parameters.
Type argument inference is a rather complicated process, and it has changed in subtle ways over time. When arguments are matched against parameters in order to infer type type arguments, there are no guarantees about the order in which candidates are inferred, nor how many candidates are inferred (for a given type parameter). This has generally not been a problem because the result surfaced to the user does not (in most cases) expose these details. But if you make a tuple type out of the inference results, it certainly does expose both the order and the count of the inferences. These details were not intended to be observable.
How serious is this? I think it depends on how exactly the inference works. What is the result of the following:
function f<...T>(x: ...T, y: ...T): ...T { }
f(['hello', 0, true], [[], 'hello', { }]); // what is the type returned by f?
@jbondc, -
seems like a good idea. I'll keep it in mind but not explore it here, because I think we should introduce new type operators one at a time. Both &
and +
create new types, but &
creates an intersection type whereas +
creates a new tuple type (which is why I prefer the syntax [T,...U]
instead of T + ...U
, because [T,U]
already does this for types).
@JsonFreeman l think it's OK to do one of two things with repeated kind parameters:
f(['hello', 1], [1, false]): [string | number, number | boolean]
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
I think real libraries (like the reactive extensions @Igorbek linked to) will usually only have one tuple kind parameter so even though neither (1) nor (2) are particularly usable, it shouldn't impact real-world code much.
In the examples above, curry
is the hardest to infer -- you have to skip f: (...args:[...T,...U]) => V
, infer ...ts:...T
, then go back and set ...U
to what's left after consuming ...T
from f
's parameters.
I've started prototyping this (sandersn/TypeScript@1d5725d), but haven't got that far yet. Any idea if that will work?
I would err on the side of disallowing anything where the semantics is not clear (like repeated inferences to the same spreaded type parameter). That allays my concern above as well.
I can't think of a good mechanism for typing curry. As you point out, you have to skip the parameter list of the first function to consume the ...T
argument and then see what's left over. There would have to be some policy to postpone inferences to a spreaded type parameter if it's not final in its list. It could get messy.
That said, I think this is worth a try. There is high demand for the feature.
I think you would have to skip multiple tuple kinds that occur in the same context (eg top-level like (...T,string,...U) => V
or concatenated like [...T,...U,...T]
). Then you can make multiple passes on the skipped kinds, eliminating already-inferred kinds and re-skipping kinds that are still ambiguous. If at any point no single kind is available for inference, stop and return an error.
So, yeah. Complicated.
You may be able to draw inspiration from a similar problem. It is actually somewhat similar to the problem of inferring to a union or intersection. When inferring to a union type that includes a type parameter that is a member of the inference context, as in function f<T>(x: T | string[])
, you don't know whether to infer to T. The intended manifestation of the union type may have been string[]
. So typescript first infers to all other constituents, and then if no inferences were made, infers to T.
In the case of intersection, it's even harder because you may have to split the type of the argument across the different intersection constituents. Typescript doesn't make inferences to intersection types at all.
What if you only allowed spreading tuple if it is the last type in its sequence? So [string, ...T]
would be allowed, but [...T, string]
would not be?
If I understand correctly, this would actually solve the mixin story in TypeScript. Am I correct in this understanding?
Maybe. Can you give an example? I'm not fluent with mixin patterns.
The syntax of a variadic kind variable is ...T where T is an identifier that is by convention a single upper-case letter, or T followed by a PascalCase identifier.
Can we leave the case of a type parameter identifier up to the developer?
@aleksey-bykov +1. I don't see a reason why that shouldn't be the case.
Developers with Haskell background would appreciate that.
Sorry, that sentence can be parsed ambiguously. I meant 'or' to parse tightly: "by convention (a single upper-case letter || T followed by a PascalCase identifier)". I'm not proposing constraining the case of the identifiers, just pointing out the convention.
For what it's worth, though, _I_ have a Haskell background and I don't like breaking conventions of the language I'm writing in.
Sorry for derailing. My last curious question (if you don't mind me asking) what is the "convention" of TypeScript that might get broken and who is concerned?
@sandersn
This should type check, assuming T & ...U
means T & U & V & ...
(which is the intuitive behavior).
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)
}
Or in a definition file:
interface Object {
assign<T, U, ...V>(host: T, arg: U, ...args: ...V): T & U & ...V
}
@aleksey-bykov the convention I'm talking about is the case of type parameter identifiers. Who is concerned? People who have to read new Typescript code they've never seen before -- conventions help new readers understand new code faster.
@sandersn What @aleksey-bykov got the impression of was that the following would be _syntactically_ invalid:
function assign<a, b, ...cs>(x: a, y: b, ...zs: ...cs): a & b & ...cs;
@isiahmeadows &
and |
operations over kinds are not covered in this proposal, although I should add them to open questions/future work if I haven't. Right now the only proposed operator is concatenation: [THead, ...TTail]
.
One difference is that concatenation still produces a tuple type while &
and |
produce intersection and union types respectively.
@sandersn My assign
example in TypeScript would be trivial to change with that.
Although:
@isiahmeadows An intersection is not in general a concatenation of dictionaries. That's only true for an intersection of object types, but not for example an intersection of unions. Unions are also not the same as just taking the properties objects have in common. The two are better characterized by the set of values inhabiting them.
@sandersn I'm a little confused about type argument inference with variadic kinds. What should be inferred here?
function foo<...T>(...rest: ...T): ...T { }
foo('str', 0, [0]);
Is the result [string, number, number[]]
? That would mean you have to rely on type argument inference adding candidates in a left-to-right order, which is not a trivial assumption. It would also be the first time the type system surfaces the list of inference candidates to the user.
I know that is an experimental / early proposal, but we could discuss ...T
syntax for rest parameters. From my perspective, it doesn't really work.
So the proposed syntax is:
declare function f<...T>(...a: ...T);
let's compare with existing syntax of rest parameters:
declare function f(...a: number[]);
so the type of a
parameter that catches rest arguments is number[]
, so we can clearly understand that is an array. By analogy, I can infer that ...T
from the proposal represents an array as well. But that's not very obvious.
Next, let's say we could define more restrictive rest parameters:
declare function f(...a: [number, string]);
// same as
declare function f(c: number, d: string); // or very close to
So now, we still see that type of a
is a tuple (which is an array).
My proposal is to use more consistent way to treat notion of ...T
to represent as a "some abstract ordered list of types". And use it in the same way how we use spread operator:
var a: [number, string] = [1, "1"];
var b = [true, ...a]; // this must be [boolean, number, string], but it doesn't work :)
So ...a
in case of variable, is just 1, "1"
.
My syntax for defining rest parameters by ...T
notion:
declare function f<...T>(...a: [...T]);
declare function g<H, ...T>(head: H, ...tail: [...T]): [H, ...T];
For me it makes much more sense.
@Igorbek I've been running on the assumption declare function f<...T>(...a: ...T);
already worked like that. But I don't see declare function f(...a: [number, string]);
getting much use.
To be more clear.
Originally proposed syntax for rest parameters:
function func<...T>(...a: ...T)
If I can do this
function g<...T>(...a: ...T): [number, ...T] { ... }
then I will be to able do this:
function f<...T>(...a: ...T): [...T] { return a; }
So the type of a
is [...T]
(we return so), but we defined it as ...T
in the signature.
We could say that ...T
and [...T]
are same, but it doesn't work in case of variables.
For variables:
var a = [1, 2];
[a] === [[1,2]];
[...a] === [1, 2];
f(...a) === f(1, 2)
...a === 1, 2 // virtually
If we apply same to standard rest parameters
function f(...a: number[]): number[] { return a; }
the type of a
is number[]
(by return type), same as it was defined in the signature.
@isiahmeadows yes, function f(...a: [number, string])
doesn't work. I just developed thoughts about how we can treat rest parameters.
So, going further. To explicitly define type parameters, the following syntax was proposed:
function f<...T, ...U>()
f<[number, string], [boolean, number]>();
Turns to:
f<...[number, string], ...[boolean, number]>();
So this might work too:
function g<T1, T2, T3>()
g<A, B, C>();
// same as
g<...[A, B, C]>();
g<...[A], ...[B, C]>();
g<...[A], B, C, ...[]>();
@JsonFreeman that is how my prototype works, yes. But I'm not familiar enough with the type inference algorithm to understand why it works. In other words, the question is not whether left-to-right inference is a _trivial_ assumption but a correct one. For the identity case the answer is yes but I don't know if you can construct cases where the answer is no.
Also can you work through an example of an exposed set of type inference candidates? Like I said, I don't understand the working of the inference algorithm very well, so an example would help me to see what you mean.
And even better:
function<...T>(...a: T): T;
// same as
function<...T>(...a: [...T]): T;
I suggest to prefix [] to the type identifier to signify the rest of type params.
function fn<R, []T>(...a:[]T): R;
It's 1 character shorter than ...T
and (in my opinion) makes less visual noise.
@aleksey-bykov I'm actually of the opposite opinion on that. It doesn't fit with the existing rest parameter syntax, so I believe it's also less clear from a glance.
[...T]
/T
as a rest array parameter type seems much better for me. Once again, compare with array and their sprad operator:
| arrays | types (from proposal) | types (my update) |
| --- | --- | --- |
| var x = [1,2]
| no | T = [T1, T2]
|
| [0, ...x] === [0,1,2]
| [T0, ...T] === [T0, T1, T2]
| [T0, ...T] === [T0, T1, T2]
|
| f(x) === f([1, 2])
| no | 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>
|
From proposal
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
}
From my update
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]
}
The update looks nicer now IMO. Lists to represent types sounds very nice, but even typed Lisps don't go that far (homoiconic types, anyone? :smile:).
I get the allure of purity, but I'm also looking at the pragmatic aspect as well. Lists would also be relatively easy to implement on their own, but it doesn't fit in with the rest of the language. It's almost like the numerous attempts to implement monads in Java (the language) or lambdas in C - they always turn out incredibly ugly and hackish.
@sandersn I can try to explain what I mean by exposing the list of candidates. Type argument inference generates a list of candidates for each type parameter. Then it checks if any candidate is a supertype of all the others, and if so, that candidate is the winner. So in the following example:
function foo<T>(a: T, b: T): T {}
foo(["hi", 0], ["", ""]);
The arguments will be typed, and then inferred to each parameter. Two candidates will be generated, namely (string | number)[]
and string[]
. But the first one will win because it is a supertype of the second. And as a result, the user never observes that string[]
was ever in the picture. There is one inference for T
, and all other candidates are invisible. This means that there are two things invisible to the user, namely the order of the candidates and the multiplicities of the candidates.
Here is an issue with the multiplicities if you rely on the candidate list as your list of elements in the tuple denoted by ...T
:
function foo<...T>(...rest: ...T): ...T
foo(0, 1);
I think you would want to infer [number, number]
for T given the intent of your proposal as I understand it. But because of the contains check in line https://github.com/Microsoft/TypeScript/blob/master/src/compiler/checker.ts#L6256, the number
candidate will only be added once, and T
will be inferred as [number]
. This is the mulitiplicity issue I was talking about.
As for the order, it is left to right. But there are multiple passes, and arguments will be reprocessed if they contain function expressions that will be contextually typed. If there are n arguments containing contextually typed function expressions, then there are n + 1 passes over the arguments. An example is Array.prototype.reduce, where the initialValue parameter is effectively typed and inferred before the callback, despite the fact that it's on the right. So something like the following might be an issue for the proposal:
function foo<...T>(...rest: ...T): ...T
foo(x => x, 0);
Intuitively, T should be [(x: any) => any, number]
, but if you rely on the order the candidate are added, it will be [number, (x: any) => any]
. This is because type argument inference is left to right generally, but functions subject to contextual typing are deferred to the end.
Both the multiplicity and the order issues I've explained are instances of surfacing the candidate list. @ahejlsberg will surely be a good person to ask about this as well, and indeed he can help explain, confirm or disprove anything I've said.
@JsonFreeman why you think it'd be an issue?
It can be implemented by virtually introduce extra generic types for each rest factual argument and infer against function with fixed parameters length.
For example,
function foo<...T>(...rest: T) { ... }
foo(x => x, 0);
// to infer, the following function is used
function foo2<T0, T1>(rest0: T0, rest1: T1) { ... }
foo2(x => x, 0);
// inferred as
foo2<(x: any) => any, number>
// T0 = (x: any) => any
// T1 = number
// T = [T0, T1] = [(x: any) => any, number]
BTW, can we infer x => x
to be of type { <T>(x: T): T; }
?
@Igorbek I think your suggestion about manufacturing type parameters (at least as intuition, regardless of how it is implemented) is the correct way to do it. You could infer a sequence of types for T
, where each element in the sequence has an index and a list of candidates (this is an alternate way of implementing what you mentioned).
However, my point was, I do not think this is what would naturally happen if you just repurposed the inference candidate list as the inferred tuple. It would require explicit mechanics to make the right thing happen.
For your point about { <T>(x: T): T; }
, that doesn't generalize well to typing things like x => foo(x)
where foo is some function. You'd need to know the type of x
to do overload resolution for foo
.
A small step out from the battle with type-checker inference rules.
I have a comment/suggestion about the syntax. I think there are two consistent but mutually exclusive options:
If we choose this form:
type F<...Args> = (...args:...Args) => ...Args
then we should use it like
var a: F // a: () => []
var b: F<number> // b: (arg: number) => [number]
var c: F<number, string> // c: (arg1: number, arg2: string) => [number, string]
...
Thus it will be true rest formal types. They should be used only at the last position of the formal type parameter section.
(...args:[string, number]) => boolean IS EQUIVALENT TO (s: string, n: number) => boolean
In this case we always have fixed number of slots in formal type parameter section.
function f<T>(...args: T): T {
return args;
}
we infer that T should be a tuple type if either condition is met:
Thus, we do not need to use an ellipsis in the formal type parameter section (we can infer it even _syntactically_ without any type checker)
in this case, we can write also
function f<T>(...args: [...T]): [...T] {
return args;
}
but it is redundant.
Personally, I would like to see the later one implemented in TypeScript. @JsonFreeman, @sandersn?
@Artazor I think it boils down to expressivity, and I don't think the two approaches are necessarily equivalent. The second one includes the ability to spread a rest type parameter inside a tuple type, whereas the first does not seem to.
I think for generic type references, it is just a matter of deciding where and syntactically how to use a rest type parameter. This would need to be decided for all type constructors that take a type sequence (tuples, signatures, generic type references).
For generic signatures, it's more complicated because of type argument inference. What if you had the following:
function callback(s: string, n: number): void { }
declare function foo<...T>(cb: (...cbArgs: T) => void, ...args: T): [...T];
foo(callback, "hello", 0, 1);
What does foo return? My point is just that people expect generics rules to be the same for generic types and generic signatures, but if you make generic types more expressive, type argument inference needs a way to handle it. This may just be a matter of formally identifying the cases that are hard for type argument inference, and requiring the user to pass explicit type arguments in these cases.
In terms of my opinion, I think your option 1 is better. I personally do not see the use of using tuple types as rest parameters. I think a rest parameter should only be allowed to be an array type, or a rest type parameter, because it is supposed to have variable length. I also like the concept of a rest type parameter being an abstract sequence of types, not associated with something that already exists in the type system.
My philosophy on tuples is that they represent a subset of array values where the length is known. Those array values are real runtime entities. I don't like the idea of using them as a sort of type system device to represent an abstract sequence of types (for instance the sequence of parameters in a signature). But whether you are allowed to spread a rest type parameter in a tuple is a different story.
I like the tuple proposal because it's more powerful and solves more use cases, it's also very intuitive that I can spread a tuple as a rest parameter because tuples are just arrays and when calling a function with a rest parameter I can spread the array. The type system would then match my understanding of the code better.
@JsonFreeman in your case foo would return [string, number, number]
as that would be inferred from ...args
, the inferred cb type would be (string, number, number) => void
and the passed callback would just ignore the last argument which is very common in both TS and JS.
I don't like the idea of using them a sort of type system device to represent an abstract sequence of types
That's exactly what they are, JS doesn't know about tuples, only TS. For TS a tuple is a sequence of types.
I like tuple-based approach too. Especially if we could have compatible functions signatures like that:
// 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
The latter we cannot express with current syntax but could if we had #6229 adopted.
So for me it seems that a proper way is to use tuples and unify tuples to express more. Without more expressive tuples, it'd be hard to have something like [...T, ...T]
because T as a tuple have an open length.
@JsonFreeman for your example, @Pajn showed exactly as my understanding of that - there's no any visible problems in inferring these types.
@JsonFreeman I'd better use that syntax
declare function foo<T>(cb: (...cbArgs: T) => void, ...args: T): T;
declare function foo<T>(cb: (...cbArgs: T) => void, ...args: T): [...T]; // same
Hm, probably it may introduce some ambiguity:
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]
I could get behind the idea of spreading a rest type parameter in a tuple. But I'm not sure I want a rest type parameter to be implicitly interpreted as a tuple. @Pajn's example would still work if rest type parameters are allowed to be spread in all type sequence positions (tuples, parameter lists, type arguments).
@Igorbek You're right about the ambiguity in your first example. Your third example is problematic too though. Given a sequence like number, string
, there are 2 possible instantiations of the signature. Namely (arg1: number, arg2: string) => [number, string]
as well as (arg1: [number, string]) => [number, string]
(adopting the implicit tuple interpretation for the sake of the example).
The other odd thing about the implicit tuple interpretation, is this: say you have a rest type parameter T being instantiated to number, string
. Now say you pass those as type arguments, Foo<T>
. Is that to be interpreted as Foo<[number, string]>
whereas Foo<...T>
is Foo<number, string>
? There is an argument for this, as it would be extending the spread operator to the type system. But I'd still rather the tuple version be represented as Foo<[...T]>
Call me crazy, but I sense some fundamental flaws with the idea of using
tuples. What happens if you try to spread a tuple type across too many
parameters? Like this?
declare function foo<T>(...args: [...T]): void
foo<[number]>(1, 2)
Also, what happens if the type parameters are of the wrong type or used in
unusual, potentially erroneous places?
// 1. unusual place
declare foo<T>(x: T, ...ys: [...T]): void
// 2. bad type
declare foo<T>(...xs: [...T]): void
foo<number>(2)
The first example is directly relevant for Function#apply (and could be an
error), and the second is a non-obvious mistake that will fail to compile,
and non-trivial to detect with Intellisense.
On Sun, Feb 28, 2016, 03:04 Jason Freeman [email protected] wrote:
The other odd thing about the implicit tuple interpretation, is this: say
you have a rest type parameter T being instantiated to number, string.
Now say you pass those as type arguments, Foo. Is that to be
interpreted as Foo<[number, string]> whereas Foo<...T> is Foostring>? β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-189817561
.
@JsonFreeman
Your third example is problematic too though. Given a sequence like
number, string
, there are 2 possible instantiations of the signature. Namely(arg1: number, arg2: string) => [number, string]
as well as(arg1: [number, string]) => [number, string]
(adopting the implicit tuple interpretation for the sake of the example).
From my third example that is clear it can only interpret as (...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]>;
The other odd thing about the implicit tuple interpretation, is this: say you have a rest type parameter
T
being instantiated tonumber, string
.
T
cannot instantiated to number, string
as it is a true tuple. It must be [number, string]
.
Now say you pass those as type arguments,
Foo<T>
. Is that to be interpreted asFoo<[number, string]>
whereasFoo<...T>
isFoo<number, string>
?
True. However, having <...T>
seems redundant for this particular use cases we're discussing (catch positioned types for rest arguments). Nevertheless, let's say we have it.
There is an argument for this, as it would be extending the spread operator to the type system. But I'd still rather the tuple version be represented as
Foo<[...T]>
There're two cases when we might use that syntax:
// 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]
So for usage it's nothing more than type operator like |
, &
or []
. But in the declaration that syntax might be interpret as T extends any[]
or whatever base type for all tuples is, to indicate that must be a tuple type.
@isiahmeadows
What happens if you try to spread a tuple type across too many
parameters? Like this?
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
Also, what happens if the type parameters are of the wrong type or used in
unusual, potentially erroneous places?
// 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
I still do not see the benefit of representing these things as tuples. Furthermore, I think they should be declared as <...T>
and not <T>
. As I said before, I don't see tuple types as an appropriate device to use for arbitrary length type sequences in the type system. I am still not convinced that this is required for the expressivity that people want.
I'd agree it could be more expressive, but having 'spread' operator in type parameters position will limit us to catch rest arguments once only, same as we can't have rest parameters twice. So given <...T>
and <A, B, C>
, T
will catch them as [A, B, C]
. And we wouldn't able to express <...T, ...U>
as it would be ambiguous - [A, B, C], []
or [A, B], [C]
or ... etc.
Let's say I wanted to express a function with the following behavior:
declare function foo(a: A, b: B): R;
declare function boo(c: C, d: D, e: E): U;
let combined: (a: A, b: B, c: C, d: D, e: E) => [R, U] = combine(foo, boo);
// so the signature could be:
declare function combine<R, U, ???>(
f1: (...args: [...T1]) => R,
f2: (...args: [...T2]) => U):
(...args: [...T1, ...T2]) => [R, U];
// if ??? is '...T1, ...T2'
combine<R, U, A, B, C, D, E> // what will be T1 and T2 ?
combine<R, U, ...[A, B, C], ...[D, E]> // ok ? so we will preserve spread to specific positions. so then
combine<...[R, U], A, ...[B, C, D], E> // will be restricted.
// however, ES6 allows to do it with function arguments
f(1, 2, 3);
f(...[1, 2], 3);
f(...[1], ...[2, 3]);
// if ??? is 'T1 extends TupleBase, T2 extends TupleBase'
// or just '[...T1], [...T2]' as a shortcut for such constraints
combine<R, U, [A, B, C], [D, E]> // pretty explicit, and doesn't occupy spread operator for type arguments
Ok, I see now how you are thinking of it. It sounds like what you're proposing is actually a different feature from what I thought. Instead of adding a new construct for capturing a sequence of type parameters, you just want tuple types to be spreadable because they already represent a sequence of types. That way it is possible to pass multiple tuples of various lengths in a more transparent way.
In javascript, it's more like function foo([...rest]) { }
instead of function foo(...rest) { }
.
That makes more sense to me now, thanks for explaining. I think that is a reasonable approach.
@JsonFreeman Exactly!
@JsonFreeman Question: why should [1, 2]
satisfy [number]
? That seems very odd to me. That actually working would be very surprising. It's not at all type safe.
Not that I have anything against tuples being used for variadic types, though (I'm neutral, too be honest).
@isiahmeadows in what way is [1, 2]
not substituable for [number]
? It's definitely a subtype. It's the same how { x: 1, y: 2 }
is a valid { x: number }
Okay. I'll concede partially, but do take into account Function.prototype.apply, which accepts a tuple of arguments.
interface Function<T, U, V> {
(this: T...args: [...U]): V;
apply(object: T, args: U): V;
}
If the caller throws a TypeError on too many arguments, then passing too many will result in a runtime error, not a compile error like it should.
Isn't it pretty rare for any JS function to throw TypeError when passed too many arguments? What are some examples?
@isiahmeadows as an abstract example, I understood that the error you are worried about is:
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.
Is that correct?
@sandersn, I think that TypeError on too many arguments is something that violates the spirit of the JS, as we usually pass function with less formal arguments than actual ones that will be passed into this function. We simply do not use them. For example Array.prototype.forEach
What about function currying? That's probably much more common, with Ramda
and lodash/fp.
On Mon, Feb 29, 2016, 13:45 Anatoly Ressin [email protected] wrote:
@sandersn https://github.com/sandersn, I think that TypeError on too
many arguments is something that violates the spirit of the JS, as we
usually pass function with less formal arguments than actual ones that will
be passed into this function. We simply do not use them. For example
Array.prototype.forEachβ
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190327066
.
@isiahmeadows I'd say that currying based on the arguments.length
is very unstable and runtime error-prone. Real currying is extra-argument-proof:
var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // still 7
When I pass my function with fixed signature as callback to somewhere I think about it in the following way: 'my function expects _at least_ those arguments'
What about things like 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))
That's very common in functional programming. And omitting the last
argument is fairly common.
On Mon, Feb 29, 2016, 13:52 Anatoly Ressin [email protected] wrote:
@isiahmeadows https://github.com/isiahmeadows I'd say that currying
based on the aruments.length is very unstable and runtime error-prone.
Real currying is extra-argument-proof:var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // still 7β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190330620
.
If you want to pass that as a callback to, say, map
(working over a list
of lists), you'll probably want to curry it.
On Mon, Feb 29, 2016, 13:59 Isiah Meadows [email protected] wrote:
What about things like
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))
That's very common in functional programming. And omitting the last
argument is fairly common.On Mon, Feb 29, 2016, 13:52 Anatoly Ressin [email protected]
wrote:@isiahmeadows https://github.com/isiahmeadows I'd say that currying
based on the aruments.length is very unstable and runtime error-prone.
Real currying is extra-argument-proof:var plus = x => y => x + y
console.log(plus(3)(4)) // 7
console.log(plus(3,10)(4,20)) // still 7β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190330620
.
I think it's mostly about that:
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]
That's why I've proposed #6229
The question of whether [1, 2]
satisfies [number]
is a valid one to ask and debate. But what has it got to do with the spreadable tuples feature?
It's whether a variadic application of tuples should ignore extra arguments
or not. This overloaded function should elaborate more of my concern.
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)
On Mon, Feb 29, 2016, 18:47 Jason Freeman [email protected] wrote:
The question of whether [1, 2] satisfies [number] is a valid one to ask
and debate. But what has it got to do with the spreadable tuples feature?β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190453352
.
And this is why, for practical reasons, I would prefer rest parameter-like
variadic types.
On Mon, Feb 29, 2016, 19:00 Isiah Meadows [email protected] wrote:
It's whether a variadic application of tuples should ignore extra
arguments or not. This overloaded function should elaborate more of my
concern.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)
On Mon, Feb 29, 2016, 18:47 Jason Freeman [email protected]
wrote:The question of whether [1, 2] satisfies [number] is a valid one to ask
and debate. But what has it got to do with the spreadable tuples feature?β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190453352
.
@JsonFreeman that's because spread operator for types and arrays/tuples. If spread type operator is allowed in form of "given types A
, B
and T = [A]
, so then [...T, B]
will construct [A, B]
" (which is implicitly proposed) then it would not be aligned with array/tuple spread operator. Given var a: [A]
and var b: B
, the type of expression [...a, b]
cannot be proven as to be of type [A, B]
. According to current rules of tuples, it could be proven as to be of type [A, A|B]
.
Does it makes sense for you? Or I can create comparison table to highlight that mismatching.
@Igorbek I understand what you are saying. It ultimately stems from the fact that the compiler has perfect knowledge of the types it's dealing with, but not perfect knowledge of the values. In particular, in your example, the value a
has unknown length, whereas the type [A]
has known length. This was one of the reasons I was initially uncomfortable about using tuple types for this purpose. But I am not sure it's a serious problem.
@isiahmeadows I see what you are asking about, but why is the issue any clearer with rest type parameters? If you have more arguments than type arguments, the same question can be asked.
The type safe solution would be more consistent with the rest of the
language if it mimicked the argument syntax.
My point is if you're effectively spreading a rest parameter, you get
exactly the argument types and no more. Curried functions have a return
type dependent on the argument type. So if you apply one too many arguments
to partially apply a curried function, you'll get a completely different
type. Handling rest types like tuples would lead to runtime errors instead,
which are never good.
On Tue, Mar 1, 2016, 06:07 Jason Freeman [email protected] wrote:
@Igorbek https://github.com/Igorbek I understand what you are saying.
It ultimately stems from the fact that the compiler has perfect knowledge
of the types it's dealing with, but not perfect knowledge of the values. In
particular, in your example, the value a has unknown length, whereas the
type [A] has known length. This was one of the reasons I was initially
uncomfortable about using tuple types for this purpose. But I am not sure
it's a serious problem.@isiahmeadows https://github.com/isiahmeadows I see what you are asking
about, but why is the issue any clearer with rest type parameters?β
Reply to this email directly or view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-190667281
.
@isiahmeadows can you give example code for the currying problem?
I still think that even if you used rest type parameters (which I am all for), you'd have to explicitly decide not to allow excess arguments, but I agree with @isiahmeadows that you probably should.
@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
I'll add my variation too. Let's see the example of variadic curry from the proposal:
function curry<...T,...U,V>(f: (...ts: [...T, ...U]) => V, ...as:...T): (...bs:...U) => V {
return ...b => f(...as, ...b);
}
So then, I started to use it:
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 Whether they are represented as rest type parameters or as tuple types, it sounds like you object to the ability to spread them in a tuple position.
@Igorbek I think your example is similar in that the problem is not how variadic type sequences are represented. It's the ability to spread them in tuples that leads to problems.
@JsonFreeman It's more that I object to this behavior:
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)
Does that clarify?
@isiahmeadows it should work actually
@JsonFreeman
I feel it shouldn't. That's my biggest objection. I feel it's potentially dangerous if it is.
Question: what should be the inferred return type of ret
?
declare function foo(a: A, b: B, c: C, d: D): D
let ret = foo.bind(...[new A(), new B(), new D()])
This is actually pretty important.
That last example definitely seems like it shouldn't work. Essentially you need a mechanism to align sequences of types if function.bind is really going to work properly. You would need something akin to unification, where the types of the arguments to bind are matched against the original function's arguments, and then the remained is in the return type.
That said, it doesn't seem like anything in what's been proposed or discussed can handle that (regardless of whether extra arguments of tuples are allowed), though it's possible I missed something.
I think the biggest problem is that some sort of tuple pattern matching, where each parameter's type is matched against the spread types, needs to be done with the type parameters (a la LiveScript/CoffeeScript's arguments) to fix that problem. It's probably impossible otherwise. And as for how complicated it is, good luck implementing it. :smile:
@JsonFreeman
Or to be more precise, it'll require non-strict (in the sense of eager vs lazy) type checking to work. I also think that's probably a more useful extension than just variadic types, anyways, since it pretty much opens the door for a lot of other more useful things, like self-recursive types.
// 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>[]
Thankfully, non-strict type checking should be a purely non-breaking change in that only code that previously failed to check now works.
That's probably the biggest thing blocking Function.prototype.bind
's proper typing, other than the fact it'll require a very complex type signature.
That's an interesting connection. I'm not convinced they are related though. The recursive types issue is a consequence of the caching policy for generics, and the representation of type aliases in the compiler. All the information is there, it's just the compiler's design that gets in the way.
For the tuple pattern matching, you can't always know how many arguments are being matched against the tuple. If you spread an array into the arguments of bind
, you don't know how many are left in the resulting callback.
@JsonFreeman that being said, do you think that as a step of adoption argument spread operator proposal #6229 needs to be considered first?
@JsonFreeman
And non-strict checking of types would allow enough laziness to make it easier to fix that problem with Function.prototype.bind
. With such laziness, you could accomplish that type with the following (which will require a tuple syntax to sequence them, unless multiple rest parameters are okay in a type declaration):
interface Function {
bind<R, T, ...X, ...Y>(
this: (this: T, ...args: [...X, ...Y]) => R,
thisObject: T,
...args: [...X]
): (this: any, ...rest: [...Y]) => R
}
Why would this require non-strict type checking to infer? You have to deduce the rest type step by step to check against the function. Given the following, here's how it would have to check:
// 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
This is how non-strict type checking works. It infers types as needed, instead of the instant it sees it. You can (and should) combine the two passes, so that wrong types fail. Example:
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
In this case, the expected parameter should be the first one inferred in that round, doing a depth-first iteration. In this case, the first one inferred in that search was a string, and symbols aren't assignable to strings, so this fails with that.
And because of this and trying to type Function.prototype.apply
, my opinion on using tuples for applying rest types have changed.
interface Function {
apply<T, R, ...X>(
this: (this: T, ...args: [...X]) => R,
thisArg: T,
args: [...X]
): R
}
Few other notes:
ts
interface Foo extends Function<void, ...string[]> {}
Function.prototype.bind
and friends should check against all overloads for the function. If there's multiple that work, it should return a union of all of them.Those type parameters in your example are not really the type parameters of the bind signature though. They belong to the Function type. But yes, the idea is that if you could use two rest params, or spread two rest type params in a tuple, you would be able to write this.
In order for the signature of bind to be flexible enough, the boundary between ...X
and ...Y
needs to be decided on a per call basis. It would need to be inferred. It would be a problem if a signature were to use ...X
in isolation though. In this case, the boundary will not have been decided. For example:
interface SomeType<T, R, ...X, ...Y> {
someMethod(someArgs): [...X]; // No way of knowing how long X is
}
And overloads are quite a problem for the Function type. I don't think you want to take the union type elementwise on each argument, because that would allow mixing and matching of parameters between overloads. Is that what you meant?
@JsonFreeman
_TL;DR: Skip to the horizontal line break. I have a new, more practical idea._
Function
itself.Although I'm also thinking it might be a little more feasible to define these kinds of situations on just the method in question. It'll also be much easier and faster to detect those kinds of potential problems you alluded to.
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
}
There is a potential other problem that will be much harder to work out (and why I think it should be constrained to 2, not _n_ divisions):
declare function foo<...T>[...T = ...A, ...B, ...C](
a: [...A, ...C],
b: [...A, ...B],
c: [...B, ...C]
): any
// This should obviously check, but it's non-trivial to figure that out.
let x = foo<
boolean, number, // ...A
string, symbol, // ...B
Object, any[] // ...C
>(
[true, 1, {}, []],
[true, 1, "hi", Symbol()],
["hi", Symbol(), {}, []]
)
_Sorry if I'm getting too deep into CS theory here..._
Yes, I think that is the right idea. It is not pretty, but I can't think of any other way to properly type bind
, knowing the type arguments of Function
. The ultimate thing is that a boundary must be inferred. And I agree that it should be limited to 2 buckets so that you have to infer 1 boundary, instead of some arbitrary number of boundaries, which can blow up combinatorially.
There are likely more issues that we have not thought of.
@JsonFreeman Another issue are things like curry
. I've yet to come up with something that can properly type that. And it'll take a while before I can. I'd have to do some serious Haskell-like type hacking to come up with such a process.
Thinking about how think kind of proposal could work with some Bluebird functions.
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>;
}
Do we have a solution for the all<...T>(promises: [...PromiseLike<T>]): Promise<...T>;
above?
@DerFlatulator
See my big comment in PromiseConstructor. I also corrected your Promise interface to be a little closer to my proposal.
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>;
}
If [...Foo<T>]
expands to [Foo<T1>, Foo<T2>, /*... Foo<TN>*/]
, then is [...Foo<T,U>]
a syntax error or a combinatorial expansion?
@DerFlatulator
T
or U
is a rest parameter, it expands normally. Assuming T
is a rest parameter, then it would be [Foo<T1, U>, Foo<T2, U>, /*... Foo<TN, U>*/]
.Note that I strongly oppose more than 2 rest parameters for practical reasons, and that the rest parameters, if they need split up, should only be split on a per-method basis. Something like this:
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
}
_(If someone can come up with a better syntax, I'm all ears. I don't like it, but I can't come up with anything that doesn't visually conflict.)_
@isiahmeadows
With 2., in what order would the expansion be?
[
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>
]
Or conversely:
[
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>
]
Won't this ambiguity would cause confusion? Perhaps limiting to one dimension would be wise.
Just an alternative suggestion for split syntax:
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
I'd expect the second. And I'd doubt it would cause too much confusion, since as long as it's consistently that, people would quickly get used to it. It's also an unusual edge case that would only really be run into in practice by people who know what they're doing, or people who should question the need in the first place.
I'm also looking at it as you're expanding the first, then the second for each part of the first. Like this pseudocode:
for (let TT of T) {
for (let UU of U) {
expand(TT, UU);
}
}
Iterating on some of the ideas above...
interface Function<TReturn, TThis, ...TArgs> {
bind<
[...TBound, ...TUnbound] = [...TArgs],
TNewThis
>(
thisObject: TNewThis,
...args: [...TBound]
): Function<TReturn, TNewThis, ...TUnbound>
}
Here, [...TBound, ...TUnbound] = [...TArgs]
is valid because the length of ...TBound
is known from the length of args
. It also permits changing TThis
's type.
One issue with this approach is that you can only bind this
once, for example:
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>
The inferred type of y
is incorrect, it should be Function<number, IBar>
. I'm not sure if this is a concern or not, but solving it would require introducing logic into the <T>
syntax.
Option 1
interface Function<TReturn, TThis, ...TArgs> {
bind<
[...TBound, ...TUnbound] = [...TArgs],
TNewThis = TThis is undefined ? TNewThis : TThis
>(
thisObject: TNewThis,
...args: [...TBound]
): Function<TReturn, TNewThis, ...TUnbound>;
}
Option 2
interface Function<TReturn, TThis, ...TArgs> {
bind<
[...TBound, ...TUnbound] = [...TArgs],
TThis is undefined,
TNewThis
>(
thisObject: TNewThis,
...args: [...TBound]
): Function<TReturn, TNewThis, ...TUnbound>;
bind<
[...TBound, ...TUnbound] = [...TArgs],
TThis is defined
>(
thisObject: any,
...args: [...TBound]
): Function<TReturn, TThis, ...TUnbound>;
}
However that would likely be outside the scope of this proposal.
I don't think we should allow such kind of expansions by using type spread operator. I think about spread operator as a "brackets remover" which is absolutely aligned with array spread operator and object/properties spread operator (stage 2 proposal). Just compare:
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 };
You are suggesting to extend it for constructing new set of types:
<...T> = <A, B, C>
...U<T> = <U<A>, U<B>, U<C>>
It's entirely different operation. If you will, it could be modeled by higher order constructs such as:
<...(from R in T select U<R>)> // linq-like
<...(T[R] -> U<R>)> // ugly
@Igorbek How about using an operator to determine what will be expanded?
interface PromiseConstructor {
all<
...T,
[...TThen] = ...(PromiseLike<@T> | @T)
>(
promises: [...TThen]
): Promise<[...T], ...T>;
}
Where ...Foo<@T, U>
expands to [Foo<T1,U>, /*...*/, Foo<TN,U>]
.
...(PromiseLike<@T> | @T)
expands to
[PromiseLike<T1>|T1, /*...*/, PromiseLike<TN>|TN]
Some syntax alternatives:
...Foo<&T,U>
(T) Foo<T,U>
(...T => Foo<T,U>)
for (T of ...T) Foo<T,U>
I agree with @Igorbek here. At least at this stage, mapping sequences of types does not seem like a priority, given that we are still trying to solve the more basic problem of variadic type parameters.
I don't have much problem with prohibiting it (at least initially), since behavior for that is pretty uninutitive, and two different people might expect two very different things, even. I do agree with @Igorbek for at least now, since TypeScript first needs to have a higher-order type model (that's map
ping a type in a sense). And higher order types are not exactly something you can just bolt on.
So definitely :+1: for prohibiting that, probably for quite a while. Even though it's a nice to have, it's complicated as hell to implement, and would be a complete hack to do, since TypeScript doesn't use a functional, type-safe type system.
Coming in a bit late, but I agree with @Igorbek as well. Reiterating my comment made in #1336 and borrowing ideas from C++ parameter packing having clear 'pack' and 'unpack' operators.
Packing types into a tuple seems consistent with Typescript's usage of the spread operator:
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];
Which makes <...T_values> = [T1, T2, T3, etc...]
a lot easier to reason about.
While C++ uses spread operator for packing and ellipsis for unpacking, using spread for both is more consistent with 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 what would be the point of all this without mapping?
Also as raised in #1336, how about a variadic Array.flatten
?
@jameskeane That first half was the initial idea, but it doesn't cover the case of a middle rest parameter (which some APIs have):
function foo<...T>(a: Foo, b: Bar, ...rest: [...T, Baz]): Foo;
It also doesn't cover Function.prototype.apply
vs Function.prototype.call
very well.
As for #1336, it might be similarly implemented via this:
angular.module('app').controller(['$scope', function($scope: ng.IScope) { /*etc...*/ }]);
interface IModule {
controller(injectable: [...string[], () => any]);
}
I have caught up, and realized I was naively assuming tuple types were strict length; which imo is the most intuitive. So assuming we get strict length tuple types (#6229), what are the issues?
@isiahmeadows In your above example of the middle rest parameter case, is it not solved by having strict length tuples? I'm reading ...rest: [...T, Baz]
the same as spread unpacking arr = [...other, 123]
. This is the same issue you raised with curry
, right?
As for apply
and call
, are they not covered by intersecting types? (Not that I really see the value in having the types on the Function
interface anyways).
// 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
The current variadic proposal assumes #6229 actually ends up accepted (i.e. tuples are strict by default).
As for func.apply
, func.bind
, func.call
, and _.curry
, the only problem is with func.bind
, _.curry
, and friends, or more generally anything using partial application. You also need to be able to choose which rest parameter to separate, and it can only really be done on a per-method basis.
call
and apply
are pretty straightforward:
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
would be more difficult. The split parameters would have to be matched as-needed, unlike eagerly, as is the case now, until the first split half is fully unpacked. This should be implemented as syntax, so the compiler can tell it apart and correctly discern the type without evaluating anything.
// 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
would be extremely difficult, as it has to know that f(1, 2, 3)
=== f(1, 2)(3)
=== f(1)(2, 3)
=== f(1)(2)(3)
. Not only does there have to be the ability to split a rest parameter into two like in bind
, there has to be the ability to perform very primitive pattern matching on a per-method basis.
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>;
I don't believe the additions for curry
would make it Turing-complete, but it would be close. I think the primary thing preventing it would be the ability to match specializations of a particular type (which C++, Scala, and Haskell, three languages with Turing-complete type systems, all have).
@sandersn I couldn't see an example above, but can I ask about constraints on variadic parameters?
Consider the following example:
interface HasKey<T> {
Key(): T;
}
class Row<...T extends HasKey<X>, X> {
// ...
}
_Incidentally, see https://github.com/Microsoft/TypeScript/issues/7848 for discussion on potentially dropping the requirement that X
needs to be listed_
Now there is potentially some ambiguity here as to whether the constraint is:
(...T) extends HasKey<X>
or...(T extends HasKey<X>)
In this example I'm assuming 2.
Will these sorts of constraints (1 and/or 2) be possible?
@myitcv 2 would probably be the best route to go, but it would make sense to just reuse existing logic for checking the constraint.
Well...I just realized something: how would arrays containing variadic types? Or more specifically, what type is arg
below?
function processItems<...T>(...args: [...T]): void {
for (const arg of args) { // Here
process(arg);
}
}
I guess you are asking what is the element type of args
. For tuples, I believe this is usually the union type of the elements. I can't think of a better way to type that.
@sandersn can you comment on what is the status of this feature? I feel that there has been a lot of discussion, but it doesn't sound like there is a definite plan for the feature, is that correct?
@JsonFreeman I was asking specifically what arg
was. In my opinion, it should be any
for my original example, and Item<T>
with below (with the F-bounded T):
function processItems<...T extends Item<T>>(...args: [...T]): void {
for (const arg of args) { // Here
process(arg);
}
}
This is so the types can be resolved locally. You don't know the types beforehand, and this will speed up compilation tremendously, since you don't have to compute the types within the function for each call to it. Note that if you just need the type of a single argument, typeof arg
will suffice, and would likely be shorter.
Oh sorry, for the original example, I meant that the type should be T
. Actually, for your second example, I also think it should be T.
I meant Item<any>
in the second...Sorry.
When I said it should be T, I was assuming T is a type, but I guess the whole point of this feature is that T is not a type (I think). So yeah, I guess it should be any
and Item<any>
in your examples.
But more broadly, I am curious how actively the team is considering this feature for after 2.0. I don't have a strong opinion, just wondering.
The reason I don't think it should necessarily be T
is because you don't know what T
is. Unless, of course, you mean having the variadic T
represent either a single type of the variadic type list, or when spread, the list itself, i.e. T
is the subtype of all arguments passed to the argument ...T
, and [...T]
is assignable to T[]
.
Or, to clarify what I mean in all the unclear jargon, here's what I mean in terms of code:
// To put it into code
function foo<...T>(list: [...T]): void {
// This is allowed
let xs: T[] = list
// This is allowed
let list2: [...T] = list
// This is not allowed
let list1: [...T] = xs
// This is allowed
let item: ?T = null
// This is not allowed, since it's not immediately initialized
let other: T
for (let arg of args) {
// This is allowed
let alias: T = arg
// This is allowed
let other: ?T = arg
// This is allowed, since `item` is defined upwards as `?T`
item = arg
// This is allowed, since you're doing an unsafe cast from `?T` to `T`.
alias = item as T
}
}
That would probably make more sense, though, and it would be much more flexible.
It's still on our nice-to-have list, but it's primarily of interest to library authors and has a decent workaround — _n_ overloads — so I'm not actively working on it. If I had to guess, I'd say 2.1 is possible but not likely.
If/when we commit to properly support object rest/spread (#2103) then variadic kinds might be close enough to spread types to justify doing them all at once. (Spread types are a variant of object types that look like { ...T, x: number, ...U, y: string, ...V }
.)
Just want to mention that the n overloads
workaround doesn't work for classes or interfaces, which is my particular interest in this feature.
@sandersn Would a pull request for "_n_ overloads" be invited for bind
, apply
and call
in functions, using this
typing? I think that would be an acceptable temporary compromise for many, and could catch quite a few bugs in the process for some projects.
@isiahmeadows
The reason I don't think it should necessarily be T is because you don't know what T is.
It seemed to me there was agreement that T is a tuple type of the variadic types. In your original example, the type of arg
would be the same as the tuple element type (as @JsonFreeman noted, "the union type of the elements"): For now imagine typescript supports using a tuple as a rest type (#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]
Separate from this proposal, I think there should be a way to represent the 'element type' of a tuple. That could be another use for spread, i.e as above ...T :: number|string|boolean
; that spreading the tuple type results in it's element type.
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;
}
}
With this in mind, your other examples:
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
}
}
Not loosing sight of my original goal, I wanted a strongly typed 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 Now that other requested features are beginning to depend on this, could the priority maybe be bumped up? bind
, call
etc. typing depends on this, and ES bind syntax if/when it comes out depends on that, so now there's more riding on this than quirky library authors nagging you all the time. :)
Not that this is particularly constructive to add, but I would be sooo happy if these two features made it into 2.1. I know of at least one library (RxJS), where not only would the codebase itself be improved by these features, consuming code would also be significantly less awkward and prone to bugs (every third person getting started with Angular 2 gets bitten by missing imports for operators that are patched into the observable prototype). It really would be a breakthrough feature for people looking to write maintainable functional code.
Could this be used to provide a complete type definition for _.extend
, whose return type is the intersection of all its parameters?
declare module underscore {
function extend<A, B, C, D, ...>(a: A, b: B, c: C, d: D, ...): A&B&C&D&...;
}
Not as it stands. It needs an extension to the proposal that gives detail on a new operator for variadic kinds — probably called &. @kitsonk proposed this operator earlier, in this comment.
Right now this feature is below a couple of other more immediately important things so I haven't looked at this proposal in a while.
While not delivering full variadic kinds, #10727 is part of the solution (and is likely to address the challenges we (@dojo) have).
Good to hear! Although it's still not actually variadic kinds. :( For example, this week when I tried to type Object.assign
, I got this far:
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 };
}
Note that the "six dots" syntax is object spread of a tuple kind variable, which we haven't really discussed above.
@sandersn
With Object.assign
in particular, it could be typed this way, and technically capture a subset (albeit a little too inferrably weak), since it mutates its target (you'd have to have a reference point for this):
assign<T>(target: T, ...sources: Partial<T>[]): T;
The glitch with that is that it mutates its target, changing its structural type in place.
@isiahmeadows then inference would fix T
to be the type of target
without accounting types of sources
. You can try it now with non-variadic version:
declare function _assign<T>(target: T, source: Partial<T>): T;
_assign({}, { a: 10 }); // T is {}
As already mentioned, assign
uses _a spread type_ #10727 and can be defined this way:
// 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]};
_Note: I'm still insisting on tuple-based syntax [...T]
which has much more sense to me._
@sandersn BTW, is any update on when variadic kinds are going to be landed? Is any chance to see it in 2.2?
And, regarding the syntax, do you still accept feedback on syntax or you're all agreed with that?
The syntax and low level semantics have no clear consensus yet.
On Tue, Dec 13, 2016, 13:26 Igor Oleinikov notifications@github.com wrote:
@sandersn https://github.com/sandersn BTW, is any update on when
variadic kinds are going to be landed? Is any chance to see it in 2.2?
And, regarding the syntax, do you still accept feedback on syntax or
you're all agreed with that?β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-266819647,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AERrBIa5fE8PSk-33w3ToFqHD9MCFoRWks5rHuM5gaJpZM4GYYfH
.
Any idea on what the status of this issue is?
so what are the options youβre thinking about? is this on the agenda with the team? itβs the only weak part of the type system i repeatedly run into. i have two use cases. a simple and a complex, but more general one.
simple would be to add a Tuple extends any[]
supertype that can only be subtyped by tuple types. As spreads need to be any[]
subtypes, this would work:
declare interface Plugin<A: Tuple, P> {
(...args: A): P | Promise<P>
}
const p: Plugin<[string, { verbose: boolean }], int> =
(dest, { verbose = false }) => 4
at the moment, ...args: T[]
is only allowed at the end of signatures.
the complex use case would need ...args: Tuple
to be legal anywhere inside a signature (which is no problem, as tuples are fixed-length):
/**
* 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)
Yeah I only just started with TypeScript yesterday and this already has made it impossible to automatically type my functions (I can still do it manually of course) because of a single decorator I'm using to wrap my functions in (which is the first thing I tried starting with!):
function portable(func) {
return function(...args) {
if (this === undefined) {
return func(...args)
} else {
return func(this, ...args)
}
}
}
Effectively all the decorator does is makes a function be allowed to be called as a method as well so that they can be attached as a method and work identically, as a basic example here's a bad example which patches the Array
prototype with a basic version of 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])
Now hopefully it's obvious that the type of flatMap
(not _flatMap
) is:
function flatMap<T, R>(this: T[], iteratee: (item: T) => R[]): R[]
function flatMap<T, R>(this: undefined, array: T[], iteratee: (item: T) => R[]): R[]
However I have no way to add types
to portable because I can't extract the parameters type from _flatMap
to then use within the type definition of the decorated function, I imagine with this proposal I could write something like:
// 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
}
I just wanted to share this as it shows my initial experiences with TypeScript and perhaps shows another case of why variadic generics are important.
@sandersn:
it has a decent workaround - n overloads
While technically not incorrect, I feel this doesn't fully reflect the reality here. Yeah, technically the lack of this could not prevent overloads from typing any function mentioned in this thread; and yet, this minor inconvenience has meant none such overload-based solutions have made it into lib.d.ts
so far.
And in fact, many in this thread have felt desperate enough on tackling their respective functions to propose even more syntax not originally part of this proposal, including your ......
, as well as ...*X
, [...T = ...A, ...B, ...C]
, [...PromiseLike<T>]
, <[...X, ...Y] = [...A]>
, and <PromiseLike<T*>>
.
I think this goes to show that we're all trying to tackle problems here, we share a general sense that we need more powerful syntax like this, and are hoping whatever road we pick here will help lead us to resolve those.
Side note: for Ramda's R.path
we had generated a typing of thousand-ish lines of overloads, which still missed tuple support (permutations would've exploded way harder still), and just caused compilation on real projects to not terminate anymore. Recently discovered iteration as a seemingly viable alternative there (#12290).
By the way, afaik you had yet to comment on the proposal laid out by @Artazor and @Igorbek. What were your thoughts on that?
I'd like to argue with a basic implementation like that here (plus #6606) we can do just about anything. I'll offer a couple solutions here to illustrate this, but I'm open to further questions.
Let me first go over some places where a ...
operator could be implemented:
v ...
for | definition (capture) | use (spread)
-|-|-
function | type Fn = (...args: any[]) => {}
| type Returns = typeof fn(...MyTuple);
(#6606)
array | type-level tuple destructuring. can technically be emulated using index access + spread (see right) + recursion. | type Arr = [Head, ...Tail];
object | type-level object destructuring. not necessary, just use Omit
, see #12215. | type Obj = { a: a, ...restObj };
(not necessary, same as Overwrite
, see #12215)
generics | define type Foo<...T>
to do Foo<1, 2, 3>
(captures [1, 2, 3
] into T
). fun, but don't know what use-case requires this. | define type Bar<A,B,C>
to do Bar<...[1,2,3]>
(A
= 1
etc.). ditto, don't know of use-cases that need this.
unions (bonus) | ? | type Union = "a" | "b"; type MyTuple = ...Union; // ["a", "b"]
(order unreliable but enables iteration of unions/objects through tuples. anyway, way out of scope here.)
So there's just two type-level ...
instances that are immediately relevant; specifically, the two used here:
declare function f<U, T>(head: U, ...tail: T): [U, ...T];
In the context of #6606, another becomes relevant: the ability to unpack a tuple type for function application, e.g. typeof f(...MyTuple)
. I think these suffice to solve the tougher problems I've heard mention of here. To try to offer some solutions here:
@jameskeane:
I think there should be a way to represent the 'element type' of a tuple
If you'd like to get the union of their elements, see my TupleToUnion
.
Promise.all
// helpers: `mapTuple` needs #5453 to define, #6606 to use
type TupleHasIndex<Arr extends any[], I extends number> = ({[K in keyof Arr]: '1' } & Array<'0'>)[I];
type Inc = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // longer version in gist
declare function mapTuple<F extends (v: T) => any, Tpl extends T[], T>(f: F, tpl: Tpl): MapFn<F, Tpl, T>;
type MapFn<
F extends (v: T) => any,
Tpl extends T[],
T,
// if empty tuple allowed:
// I extends number = 0,
// Acc = []
// otherwise:
I extends number = 1,
Acc = [F(Tpl[0])]
> = { 1: MapFn<F, Tpl, T, Inc[I], [...Acc, F(Tpl[I])]>; 0: Acc; }[TupleHasIndex<Tpl, Int>];
declare module Promise {
function all<Promises extends Promise<any>[]>(promises: Promises): typeof mapTuple(<T>(prom: Promise<T>) => T, Promises);
}
@danvk:
_.extend
@sandersn:
Object.assign
These are both just variadic versions of Ramda's mergeAll
. No six dots needed!
@isiahmeadows:
Any idea on what the status of this issue is?
The syntax and low level semantics have no clear consensus yet.
If I understand correctly you mainly worried whether the approach offered by a few of the others would take into account tackling tougher typings such as the curry
and bind
you'd mentioned. Here's my take on that particular one, following their proposal.
The strategy is a bit similar, cheating around the fact it's tough to say, extract type requirements for params i~j from a function type into a tuple type, by deferring type-check of arguments to the function application.
// 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])`
}
Yeah, I used a bunch of helper types -- just trying to make do with what we have (+ imagine what we could do with just a little more). I'm not so much against ......
, ...*X
, [...T = ...A, ...B, ...C]
, [...PromiseLike<T>]
, <[...X, ...Y] = [...A]>
, or <PromiseLike<T*>>
. But IMO, even just ...
helps address a real issue right now, and I'd love to see it addressed.
Edit: I solved the argument constraint for bind
.
Just a probably stupid question. This looks very promising to be able to correctly type curry functions.
But for some real-world projects, one could not want to spend much time to type heavily functional programming oriented piece of code.
So, I am wondering, since --strict
is enabled by default in _tsconfig.json_, if there could be a way to disable type-checking for a part of the code (because of laziness or lack of time).
But, as I said, it is probably a stupid question... ^_^
@yahiko00 kind of off topic, but use exclude
section in tsconfig
or different tsconfig
s on different project levels.
I would also like to make another suggestion, could we have it so the &
and |
work with a single tuple argument with this syntax:
<...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's above suggestion would be very useful for something that I'm doing too.
I want to add a disclaimer that this proposal is not actively being worked on. But I found exactly the "prior art" kind of paper I wanted to read when I first started looking at this problem: http://www.ccs.neu.edu/racket/pubs/esop09-sthf.pdf
I'll leave that here for future reference.
I opened a few PRs experimenting toward this:
const c = 'a' + 'b';
Can solve the problem? Infer type of c
is 'ab'
not string
A related question on StackOverflow: Explicit last function parameter in TypeScript
@sandersn You proposal would cover this case, as far as I can see, is this correct?
More than 2 years since the initial proposal, should we still remain hopeful?
Hello!
I'm trying to type a generator that takes a variable number of arrays and mixes and matches elements of them to create a new array.
I want to use this generator in a for...of
loop, but can't get proper typing of the values.
Code (there may be mistakes since I haven't run it yet, but this is what I'm trying to do):
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;
}
}
}
}
Example usage:
for (let [target, child] of
CombineEveryArgumentWithEveryArgument(targetsarray, childrenarray)) {
I can't figure out any way to get typing for target and child without creating an intermediary variable.
Something like this would be good?
function * generator<...T[]>(...args: T[]): [...T]
@Griffork the correct practice, until this proposal will be implemented, is to create many overloads for the functions
for example, see the promise.all types
https://github.com/Microsoft/TypeScript/blob/master/lib/lib.es2015.promise.d.ts#L41-L113
I find this syntax very confusing:
function apply<...T,U>(ap: (...args:...T) => U, args: ...T): U {
this feels much more natural to me:
function apply<T, U>(ap: (...args: T) => U, args: T): U {
At runtime a rest parameter is an array, and we can do this currently in TS:
function apply<T, U>(ap: (...args: T[]) => U, args: T[]): U {
So it seems logical to just remove the restriction of args
being an array of T
and instead enabling the TS compiler to infer a tuple type for T
, e.g.
function apply(ap: (...args: [number, number]) => number, args: [number, number]): number {
I saw that there were some concerns raised about tuples and I don't fully understand them all, but I just wanted to weigh in that in the current proposal it is hard to understand when a developer would need to use ...
in a type position and tuples are a lot more intuitive.
...
still makes sense to me for concatenating two tuple types though, like [...T, ...U]
.
@felixfbecker
The proposal for
function apply<...T,U>(ap: (...args:T) => U, ...args: T): U {
Would be that T
is a dynamically created tuple type, so if you pass a string
and an int
into the function, then T
is [string, int]
.
This is particularly interesting if you want to dynamically express a pattern like this:
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.
At the moment if you want to write a function that takes a variable number of arguments and type them generically, you have to write an genericised overload for every number of arguments that function could possibly be given. The ...T
proposal essentially allows us to get the compiler to automatcially generate the function definitions for us.
Your proposal:
function apply<T, U>(ap: (...args: T) => U, args: T): U {
Forces all parameters to be treated as the same type, and they can not have more specific type checking. E.g. in my example above, all returned values would be of type any
.
I also find the extra ...
is very hard to read.
Like @felixfbecker idea, I don't see the need to do:
function apply<...T, U>(ap: (...args: ...T) => U, args: ...T): U {...}
The first thing come to mind when reading apply<...T,
is that it is a spread operator, but it actually does not do spreading at all.
@Griffork , in your example T
would still be [string, int]
.
That's what @felixfbecker means by "instead enabling the TS compiler to infer a tuple type for T", at least the way I understand it.
Forces all parameters to be treated as the same type, and they can not have more specific type checking. E.g. in my example above, all returned values would be of type any.
@Griffork No, in my mind it would infer a tuple type for the args
array, giving each parameter its own type by its position in the tuple. ...args: T[]
would force them all to be the same type T
, but ...args: T
(which is currently a compile error) would infer a tuple type for T
.
The first thing come to mind when reading apply<...T, is that it is a spread operator, but it actually does not do spreading at all.
@unional agree, that's exactly where the confusion stems from.
@unional
I read it as the spread operator too, I read it as "spread this type every time it's used".
To me, reading this
function apply<T, U>(ap: (...args: T) => U, args: T): U {
I'd expect T
to be a something array (e.g. string[]
).
And reading this:
function apply<T, U>(ap: (...args: T[]) => U, args: T[]): U {
I'd expect all args to be assignable to the type T
(which is one type, such as string
).
The point towards the above proposal was to avoid implicitly making generics capable of representing an arbitrary amount of types.
@felixfbecker
Edit:
Oh ok. Still don't think that's intuitive.
I'd expect T to be a something array (e.g. string[]).
A tuple is "something array", it's just an array with a fixed length and specific types for each element, e.g. [string, number]
(vs (string | number)[]
, which is unbound and does not declare what element has what type).
Well then what do you type if you actually want that behavior?
Not sure what behaviour exactly you are referring to, but I assume "force all parameters to be of the same type", which would be accomplished by ...args: T[]
.
I read it as the spread operator too, I read it as "spread this type every time it's used".
That's why I think it is confusing.
When you do spread, you just do it, you won't declare something that is "spreadable":
const a = { x: 1, y: 2 }
const b = { ...a }
// likewise
function appendString<T>(...args: T): [...T, string] {
args.push('abc')
return args
}
Yeah. If you want to declare that a generic type argument needs to be "spreadable" (which per ES spec just means it must be iterable), we already have a way to express that in TypeScript with extends
:
function foo<T extends Iterable<any>>(spreadable: T): [...T, string] {
return [...spreadable, 'abc']
}
const bar = foo([1, true])
// bar is [number, boolean, string]
of course, in the case of a rest parameter it is known that it is not just Iterable, but an Array.
Which, what we are saying has already been proposed: https://github.com/Microsoft/TypeScript/issues/5453#issuecomment-189703556
But the rest is too long to consume in one seating. π·
If tuple concatenation is landing, then we can implement church numbers! Hooray!
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'];
And if recursion of conditional type is working, we can create tuple with desired 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'];
I don't presume I've read all of this thread, but if having ...T
in generic parameters is confusing,
why not try to mirror the value-level destructuring syntax further, so that using [...T]
in a
type-level argument position destructures the type if it's a type-level tuple? This would also require
allowing typing rest params with tuples, which would make the following equivalent at the call site
in TypeScript:
const first = (a: number, b: string) => β¦;
const second = (...ab: [number, string]) => β¦;
first(12, "hello"); // ok
second(12, "hello"); // also ok
INB4 "but that's type-directed emit" β no. This doesn't change emit whatsoever, the first
still has two
distinct arguments emitted, the second
will still have one rest argument. The only thing it changes is
that at the call site, TypeScript will check that the parameters of second
match, in order, the tuple
[number, string]
.
Anyway, assuming we admit the [...Type]
syntax, then we could write apply
as follows:
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`
The [...Type]
syntax would behave entirely as value-level destructuring, allowing for splitting
and joining type-level tuples, as needed:
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];
An example on how to type the curry
function using this:
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`
I assume it would also be helpful to be able to say, that some type argument is a type-level tuple
of some kind. The issue then would be how β considering since 2.7 (I think?) tuple assignability takes
into account tuple length β to express the concept of _any type-level tuple_. But maybe something like
[...]
could work? I have no strong opinion, but it would be good if the concept was nameable.
// bikeshed me
type OnlyTuplesWelcome<ArgumentT extends [...]> = ArgumentT;
In that case the above syntax of [...ArgsT]
could basically be a shorthand for ArgsT extends [...]
,
having the usage of type-level destructuring imply a constraint on the type to be a type-level tuple.
Thoughts?
@jaen:
(...ab: [number, string]) => β¦
Yeah, that looks like #4130. I tried something at #18004, but my approach was a bit hacky (synthetic nodes).
On expressing any tuple I'd seen someone using any[] & { 0: any }
, which I guess works until empty tuple type fwiw. I haven't bothered much personally, mostly just settled for any[]
.
RxJS needs this all over the place. Most critically for Observable.prototype.pipe
, for which we currently have many overloads, but I'm always being asked to add "just one more level".
To second @benlesh we use RXJS extensively and need this for pipe functions.
I'm the author of ppipe, which, like the pipe functions in RXJS, needs this. I think I see a pattern here^^
I'm the author of runtypes, and this feature is desperately needed to express unions and intersections. The only (incomplete) workaround is gargantuan overloads:
https://github.com/pelotom/runtypes/blob/master/src/types/union.ts
π€’
I'd rewritten the types of ramda, which also need this for e.g. pipe
, lenses, and currying.
We needed codegen as currying made directly maintaining overloads unmanageable. Our path
type spanned over a thousand lines, at which point we found overload type performance had become problematic as well.
Has the problem with inferring and applying rest arguments been resolved?
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)
}
If the type of T in apply(example, ['0', 1, 2, 3])
is inferred as [string, number[]]
, the call to apply would raise an error.
That means the type of T is really
type T = [string, ...number[]]
or
type T =
{0: string} &
{[key: Exclude<number, 0>]: number} &
Methods
A strange beast indeed, but given how ({0: string} & Array<number>)[0]
will
currently resolves to string
[1] it seems possible to encode without much change
to the type system.
[1] Is this a bug, should it really be string | number
?
Sorry to bother 36 participants of this issue (tip: use this), but how can we monitor if this is still being considered, if this is on the roadmap, etc?
It's sad that no one was assigned to it yet after 2 years and a half, seems to be a pretty important feature :(
PS: I've read a couple dozen comments, tried Cmd+F etc, didn't find this info.
@brunolemos there's a reference to variadic types in the latest design meeting
https://github.com/Microsoft/TypeScript/issues/23045
In order make this feature they need to first create more primitive and concepts iteratively, and when there's enough foundations i'm sure they will add it to any milestone
I can not do this
type Last<T extends any[]> =
T extends [infer P] ? P :
((...x: T) => any) extends ((x: any, ...xs: infer XS) => any) ? Last<XS> :
It is a matter of #14174 but as a relation
@kgtkr for reference see @fightingcat's trick to get the compiler to overlook the recursion.
thanks
type Last<T extends any[]> = {
0: never,
1: Head<T>,
2: Last<Tail<T>>,
}[T extends [] ? 0 : T extends [any] ? 1 : 2];
Hmm, I have a question. I've got a code like this to handle mixins:
export const Mixed = <
OP = {}, OS = {}, // base props and state
AP = {}, AS = {}, // mixin A props and state
BP = {}, BS = {}, // mixin B props and state
// ...and other autogenerated stuff
>(
// TODO: Find a way to write that as ...args with generics:
a?: ComponentClass<AP, AS>,
b?: ComponentClass<BP, BS>,
// ...and other autogenerated stuff
) => {
type P = OP & AP & BP;
type S = OS & AS & BS;
const mixins = [a, b];
return class extends Component<P, S> {
constructor(props: P) {
super(props);
mixins.map(mix => {
if (mix) {
mix.prototype.constructor.call(this);
// some state magic...
}
});
}
};
};
I use it like following:
class SomeComponent extends Mixed(MixinRedux, MixinRouter, MixinForm) {
// do some stuff with mixed state
}
It works as expected - with proper typing, state handling and so on, but is there any way to rewrite in a more short manner without waiting for a variadic kinds? Because I feel a little dumb about this one right now.
With 3.0 it is now possible to do declare rest args as tupple
declare function foo(...args: [number, string, boolean]): void;
But is it possible to get reversely to get tuple type of arguments of the given function?
Something like Arguments<foo>
would be nice.
@whitecolor how about this?
type Arguments<F extends (...x: any[]) => any> =
F extends (...x: infer A) => any ? A : never;
with TS 3.0 we can do this now
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);
};
}
but we have small problem we have to declare functions that return tupples event for single params and also deal with void somehow and we have to declare return type otherwise it is inferred as array :)
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
Use conditional types:
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;
}
sure, but this is kind of hacky with those as any
:) I think it would be cool if type system supported this without hacks
Well, the types in the function body can be multiple things, there's not really much that can be done about it β you could do something like (f as (...args: any[]) => Y)
instead but I think that reduces clarity for no real reason
If variadic types comes to fruition, I would be able to generalize some TypeScript code that I wrote for my own project, that let me fully define the shape of my REST API, and enforce that shape on the types of the corresponding Node server and JavaScript client library for it.
Generalizing that would allow me simplify my own code, as well as do the same for arbitrary APIs, and go further and probably generate Swagger definitions for other language clients too... could be useful for others! Just dreaming aloud though haha
I succeeded in typing Pipe
https://github.com/kgtkr/typepark/blob/master/src/pipe.ts
@kgtkr: That looks great! :)
The Pipe type crashes TS playground for me (though others work fine), I guess it needs the newest TS?
TS also shows some recursion depth errors -- looks like @isiahmeadows opened #26980 for that.
@tycho01
The Pipe type crashes TS playground for me (though others work fine), I guess it needs the newest TS?
It hangs for me, too, and I had to hack into the devtools to force it to throw an error to break out of it.
TS also shows some recursion depth errors -- looks like @isiahmeadows opened #26980 for that.
That's for something related, but different: lifting a constraint with conditional types for two reasons:
If removing indexed types isn't sufficient to make the type system not Turing-complete as-is, please feel free to drop a comment there so I can update it accordingly. (I don't propose removing it, of course. I just propose handling it better internally to warn people of potential infinite loops.)
On second thought, this Pipe type feels like a really convoluted way of doing (vs...: Params<T[0]>) => ReturnType<Last<T>>
. Any iteration beyond that (aside from intermediate param checks) probably becomes more useful with input-dependent return types.
@tycho01 It's attempting to type things like this, where the type is basically this:
f1, f2, ..., fm, fn -> composed
(a -> b, b -> c, ..., x -> y, y -> z) -> (a -> z)
You have to iterate the parameters individually to type it correctly, since subsequent parameters' parameters are dependent on previous parameters' return values, and you also have to account for the first parameter and end return value (which @kgtkr's doesn't). My "improved" version in #26980 made an optimization to use an accumulator instead, so I was iterating the arguments a lot less, but it also made it more correct.
If you look at mine in #26980, it's a bit clearer what it's supposed to be doing (less number chasing), and that's kind of part of the point of why I filed that feature request.
@tycho01 @kgtkr BTW, I updated that bug with a corrected PipeFunc
snippet, copied here for convenience:
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>);
This one doesn't crash in the playground, BTW. It actually type-checks and it does so pretty quickly.
I haven't tested it on a potential _.flow
or _.flowRight
type yet, but that should work as a starting point.
@tycho01
required
typescript@next
3.0.1/3.0.2 does not work
Thanks to this thread, I made this
People, please stop posting information barely relevant to discussion of this issue. There are a lot of people following this discussion, because we want variadic kinds. I have received like 10+ emails in the last couple days that are irrelevant to the reasons I'm following this issue.
I expect there are others in agreement with me. Up to this point I've just hoped it would stop, because I didn't want to contribute to the spam. But seriously, enough is enough.
P. S. Sorry for this notification, to anyone who is as sick of them as I am
@Yuudaari I will point out that typing Lodash's _.flow
, Ramda's _.compose
, etc. is one of the driving things for this bug, and a successful typing is part of solving this issue. In fact, that's one of the reasons listed in the original issue description.
Really, at this point, I'm of the opinion 99% of the problems that exist today with variadics are with ergonomics, not functionality. We can type Function.prototype.bind
and Promise.all
perfectly with a mixture of indexed types, conditional types, and recursion (you can do a repeated Append
iterating the list for Function.prototype.bind
, and Promise.all
would be a simple iteration + Append
), just it's very awkward and boilerplatey to do so.
Not trying to add to the noise here, just explaining that the stuff starting here is technically on-topic as it concerns some of the reasons the bug exists, even if they aren't ones you're personally concerned about.
I think people waiting for announcements here missed the big news -- it turned out the now possible Concat<T, U>
functions exactly as [...T, ...U]
.
The Pipe
sub-thread is about demonstrating the functionality we asked for here. It's about reaching the point of this thread, today.
I think that means we'd be no worse off closing this thread, so perhaps this is a good moment to ask -- what do people still want from this proposal?
[it's] just it's very awkward and boilerplatey
Most types using this will be using recursion themselves, so those writing them will definitely be familiar with them, while end-users will likely just use libraries with predefined types and write their front-end, without having to know TS iteration exists.
At that point perhaps this proposal may mostly improve performance?
Firstly, is using map objects to trick the type system into doing recursion even intended? It seems pretty hacky to me. If I use functionality like that (I am, but that's irrelevant), is it not going to be subject to breaking later?
Secondly, using these workarounds just isn't... friendly. It's not very readable (especially for those that didn't write it), and as a result it looks miserable to maintain.
Why would I want to fall back on a proposal that adds the same features, in an intended, readable, and maintainable way, just because there's a workaround for them?
I don't think the existence of this workaround causes this proposal to be considered syntactic sugar, but even if it was, why would I not want syntactic sugar for this mess?
@Yuudaari
Edit: Add link for context.
Firstly, is using map objects to trick the type system into doing recursion even intended? It seems pretty hacky to me. If I use functionality like that (I am, but that's irrelevant), is it not going to be subject to breaking later?
Take a look at the bug I recently filed: #26980. You're not alone in questioning the pattern. It's a bit off-topic here, but feel free to chime in there.
Do note there is a little bit of math involved in how to figure out whether something recursive terminates (one of the main reasons why it's so nuanced in the first place).
Secondly, using these workarounds just isn't... friendly. It's not very readable (especially for those that didn't write it), and as a result it looks miserable to maintain.
Why would I want to fall back on a proposal that adds the same features, in an intended, readable, and maintainable way, just because there's a workaround for them?
I don't think the existence of this workaround causes this proposal to be considered syntactic sugar, but even if it was, why would I not want syntactic sugar for this mess?
There does exist a simplified way to iterate tuples in the common case of what's effectively Array.prototype.map
, but that was basically useless for my needs (I needed an accumulator).
I'd personally like syntactic sugar for these:
[...First, ...Second]
.[...Values, Item]
.T extends [...any[], infer Last]
.T extends [A, B, ...infer Tail]
.Combine that with #26980, and I could turn the above types into this:
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;
But that's about it. I don't see much use for any other syntactic sugar, since most everything here just deals with tuples, and objects already largely have all you would ever need for similar operations.
Firstly, is using map objects to trick the type system into doing recursion even intended? It seems pretty hacky to me. If I use functionality like that (I am, but that's irrelevant), is it not going to be subject to breaking later?
I think the official word is something like "don't do it". @ahejlsberg said:
It's clever, but it definitely pushes things well beyond their intended use. While it may work for small examples, it will scale horribly. Resolving those deeply recursive types consumes a lot of time and resources and might in the future run afoul of the recursion governors we have in the checker.
Don't do it!
βΉοΈ
@jcalz So all the more reason for #26980 to exist?
When I started using TS just the break of this year, my inclination was to write _just that_! (...T
) in the hope that it would be the syntax for variadic typevar tuples. Well, hope this gets in :)
Just found a new use for [...T, ...U]
: correctly typing HTML builders. For a concrete example, <video>
's children is required to be the following:
src
attribute:<track>
elementssrc
attribute:<source>
elementsaudio
or video
descendant element is permitted.This basically equates to this type, but there is no way to express this in TypeScript today:
type VideoChildren<ParentModel extends string[]> = [
...Array<"track">, // Not possible
...{[I in keyof ParentModel]: P[I] extends "audio" | "video" ? never : P[I]},
]
3.5 years :/
Use case:
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);
}
I only see overloads mentioned briefly in the open questions section of the proposal, but definitely something that I have run into and would be excellent to see a solution or a proposal, e.g.:
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
currently this doesn't work at all and just types the promisify
as (ctx: ICtx, filter: FilterQuery<TSchema>) => Promise<TSchema[]>
which is losing information from these signatures.
AFAICT the only real solution to this is basically to create a promise function and manually specify all the possible types to that variant - the only way to actually provide an honored signature from the wrapped variant is to not specify overloads and just specify the implementation signature, but there's no way to let the caller know which of the return types they should expect based on the arguments they pass if you specify the signature in that way.
this is exacerbated by the fact that rest parameters can only be the last parameter (i.e. (cb, ...args)
is valid but not (...args, cb)
, so even if the signature is internally a union type, you can't actually spread things correctly - e.g. it would be fairly straightforward if cb
were always the first argument to type promisify as function promisify<T, V extends any[]>(fn: (cb: (err: Error | null, res?: T) => void, ...args: V)): (...args: V) => T
and you could at least get union types for signatures with the same return response, but because it's the final parameter afaict there isn't much that can be done here
@Qix- Your scenario is enabled by #24897. Was implemented in TS 3.0.
@ahejlsberg Oof! Awesome, thank you β₯οΈ
It's been a long time waiting... But it's possible to write variadic kinds today. TS is mature enough to write complex types that work. So I took the time to write types for curry, concat, compose and pipe for Ramda.
And are now shipped with the ts-toolbelt.
However, this proposal is a nice syntactic sugar to make common tuple manipulations much easier.
do you have it on medium.com already? URL?
There's the original article on Medium but the bonus is not included in it, it's in the repo. It also explains how I created all the little tools to get to compose, pipe & curry :smile:
@pirix-gh but this isn't variadic generics as in this proposal
declare function m<...T>(): T
m<number, string>() // [number, string]
@goodmind Yes, it isn't, it is more emulated. So you could emulate the ...
like this:
declare function m<T extends any[], U extends any[]>(): Concat<T, U>
m<[number, string], [object, any]>() // [number, string, object, any]
Is the same as:
declare function m<...T, ...U>(): [...T, ...U]
m<number, string, object, any>() // [number, string, object, any]
In the meantime, while waiting for this proposal :hourglass_flowing_sand:
@pirix-gh can you please help with wrapping function like
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) // {}
I was trying to use compose approach but failed, may you suggest on a proper way please?
This happens because when you extract the parameters/return of fn
which are dependent on generics, TS infers them to their closest type (in this case T
will be any
). So the're is no way to to this at the moment. Our best hope is to wait for this proposal to combine with https://github.com/Microsoft/TypeScript/pull/30215. So, you'll have to write type overloads.
Or, maybe we could find a way to preserve/move generics around in such a way that we could do:
declare function ideal<...T>(a: T[0], b: T[1], c: T[2]): T
ideal('a', 1, {}) // T = ['a', 1, {}]
This way, we'd reconstruct fn
from pieces of it. The missing part today is the generic part like @goodmind pointed out.
@pirix-gh If I'm not mistaken, you can just do this to achieve what you have up there:
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 Not exactly, because what I proposed works for an unlimited amount of arguments. But maybe we could also do this, with what you've proposed (I haven't seen it in the proposal):
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 The A
, B
and C
type arguments in your example are unused.
-declare function MyFunction<A, B, C, ...Args>(...[a, b, c]: Args): Args
+declare function MyFunction<...Args>(...[a, b, c]: Args): Args
Even if variadic types were implemented the previous two examples would probably generate a compile error, what's the point of variadic types if you only want three arguments.
Your examples should show why variadic is needed, if they can be done with existing TS code then it doesn't help the cause at all.
@goodmind Yes, it isn't, it is more emulated. So you could emulate the
...
like this:declare function m<T extends any[], U extends any[]>(): Concat<T, U> m<[number, string], [object, any]>() // [number, string, object, any]
Is the same as:
declare function m<...T, ...U>(): [...T, ...U] m<number, string, object, any>() // [number, string, object, any]
In the meantime, while waiting for this proposal β³
Where did you get the Concat<>
?
Edit: Never mind found the source code.
@pirix-gh so I tried to do this with your suggestions but couldn't figure it out.
~The problem is I'm trying to extend the parameters of the ctor of a class and it works to the point that I have an array of types but I can't spread them for the ctor params.~
Class Test {
constructor(x: number, y: string) {}
}
let ExtendedClass = extendCtor<[number, string], [number]>(Test);
let instance = new ExtendedClass(1, '22', 2);
Update: Never mind that also worked by using a spread in the ctor function.
Here is the link of the solution
The only problem is TS crashes almost every time :|
and this is what TypeScript says Type instantiation is excessively deep and possibly infinite.ts(2589)
Update 2:
I achieved it by putting the new type in the beginning, still it would nice to be able to merge these types.
// ...
type CtorArgs<T, X> = T extends (new (...args: infer U) => any) ? [...U, X] : never;
// To be used as CtorArgs<typeof Test, string>
// ...
let instance = new MyClass1('22', 2, 'check');
as opposed to:
let MyClass1 = extendClass<typeof Test, string>(Test);
let instance = new MyClass1('check', '22', 2);
Link to the final solution.
If I understand correctly Object.assign
can be declared something like the following to fully support variadic arguments.
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>
}
Is there any reason it is declared in a different way in TypeScript's lib.d.ts
?
It's not using recursive conditional types because those are not supported (see #26980 for discussion of that, or this comment telling us not to do that). If one is willing to use the current intersection return types there is #28323.
@jcalz I created a heavy test that shows the Minus
type in action. It performs the Minus
216000 times in less than 4 seconds. This shows that TS can handle recursive types very well. But this is quite recent.
Why? This is thanks to Anders :tada: (https://github.com/microsoft/TypeScript/pull/30769). He allowed me to switch from conditional types to indexed conditions (like a switch). And as a matter of fact, it improved performance by x6 for the ts-toolbelt. So many, many thanks to him.
So technically, we could re-write @kimamula's type safely with the ts-toolbelt. The complexity follows 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'},
]>
The lib also makes recursion safe with Iteration
that will prevent any overflow from TypeScript. In other words, if I
goes over 40
then it overflows and Pos<I>
equals number
. Thus stopping the recursion safely.
A similar recursive type that I wrote (Curry
) is shipped with Ramda, and it seems like it's doing well.
By the way, I thanked you (@jcalz) on the page of the project for all your good advice.
I'm not sure if #5453 is the best place to have this discussion... should we talk about this in #26980 or is there a more canonical location? In any case I would love to have an official and supported way to do this which won't possibly implode upon subsequent releases of TypeScript. Something that's included in their baseline tests so that if it breaks they will fix it. Even if the performance is tested to be good I'd be wary of doing this in any production environment without some official word from someone like @ahejlsberg.
Something that's included in their baseline tests so that if it breaks they will fix it.
@weswigham forgive me for being dense, but can you show me how the highlighted type is recursive? The thing I'm worried about is of the form
type Foo<T> = { a: Foo<Bar<T>>, b: Baz }[Qux<T> extends Quux ? "a" : "b" ]
or any of the variants I've seen. If I'm missing something and this has been given some sort of green light, please someone let me know (and teach me how to use it!)
Oh, fair - it is different in that respect, yeah. I just say the "immediately indexed object to select types" pattern and realized we had _that_.
I have a question.
How much of the stuff proposed here is still relevant? This issue was opened 4 years ago and I feel like a bunch of stuff has changed since then.
From my comment here,
https://github.com/microsoft/TypeScript/issues/33778#issuecomment-537877613
I said,
TL;DR, tuple types, rest args, mapped array types, tuple-inference for non-rest arg, recursive type aliases = no real need for variable type argument support
But I'm curious to see if anyone has a use case that simply can't be enabled by the existing tools
Until we get an officially blessed version of Concat<T extends any[], U extends any[]>
then this is still relevant. I don't think the upcoming recursive type reference feature gives this to us, but I'd be happy to be (authoritatively) told otherwise.
Don't we have Concat<>
implementations already?
Or is the key phrase here "officially blessed"?
Because my assertion is that you can basically do everything (or almost everything?) you could want at the moment, even if it isn't quite "officially blessed".
But I guess "officially blessed" should always be preferred... Good point. I'm too used to (ab)using those recursive type aliases
I would generally prefer a real, elegant syntax so that every time I do something like this I don't have to keep explaining to my (often junior) teammates what's going on for confusingly specified types that the status-quo abuse necessitates. That confusion harms my ability to evangelize TypeScript, or at least these uses of it, in my org.
Big π on this!
This feature is SO important.
@AnyhowStep
Because my assertion is that you can basically do everything (or almost everything?) you could want at the moment
I see no evidence that typing a spread param with multiple single params alongside it is easily achievable today in TS.
@matthew-dean not entirely true. Here's an example that you may achieve to some extent.
As I understand TS seeks to type as much of vanilla JS programs as possible. Here is a puzzle:
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);
I'd expect the type there to be no more complex than [number, ...T, number, ...T, number]
. If we have to write 20 lines of some weird code that abuses a bug in occurs check to have a proper return type in the last line, this issue is not resolved.
@polkovnikov-ph The type is currently inferred as: [number,Β ...any[]]
, which is unhelpful.
Also I'd like to notice that we don't have to honor Greenspun's tenth rule for 15 years like C++ did, because C++ already went through all the Head<>
s and Cons<>
for us, and devised some very handy and clean variadic template syntax. We can save (hundreds of years of) developer time and just take the best parts from there.
For example, variadic types have a different kind in C++, so you cannot use variadic type variables where the type is expected, unlike a type that extends any[]
in TS. This allows C++ to map/zip over tuples by mentioning variadic type variable inside of some expression enclosed in ellipsis operator. This is pretty much a tuple alternative of mapped object types.
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]
Please mention that proposed ellipsis syntax instead of extends any[]
is used in the example not merely by aesthetic reasons, but because
type A<T> = any[]
type B<T extends any[]> = [...A<T>]
type C = B<[1, 2]>
is already a valid TS program. C
ends up being any[]
instead of [any[], any[]]
that mapped variadic type would generate.
@DanielRosenwasser I apologize for pinging you like this, but I wanted to resurface this issue to increase the chance that it gets some love. Variadic kinds would be super helpful, though I realize implementing them is a big task!
As an aside, just having type-level spread operations for tuple types would be a huge help for my team, even if the lack of variadic kinds means that they can't be used with type parameters. In our problem domain "arrays with some structure" are very common. It would simplify things greatly for us if this operation would work:
type SharedValues = [S1, S2, S3];
type TupleOfSpecificKind = [V1, ...SharedValues, V2];
@sethfowler if you have some examples of what you want to express, that's always helpful for us. Otherwise you might be interested in https://github.com/microsoft/TypeScript/issues/26113
@DanielRosenwasser Sure, I can make things a bit more concrete. I'll keep the nitty-gritty out of it, but at a high level you can think of our project as generating a stream of graphics operations and other similar events that gets sent to a remote server. For efficiency reasons we need to represent those operations in memory in a format that's directly translatable into their serialized form. The types for these events end up looking something like this:
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
We'd like to be able to express these types more like this:
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>];
We have a huge number of these commands, so just having type-level spread would result in dramatically more maintainable code. Having variadic kinds so that we could write type-level functions like Repeated<>
and RepeatedUpToTimes<>
(which would evaluate to recursively-defined unions of tuple types in this example) would go even further to simplify things.
It would also be immensely useful to have support for things like type-safe concatenation of tuple types (as discussed in the OP). In order to use the types above, we currently have to construct the entire tuple in a single tuple literal expression. We can't construct it in parts and concatenate them together right now. In other words, the operations below don't work today, but we really wish they did.
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];
Hopefully these examples are helpful!
You can easily write a Concat<>
type, and make a Concat3<>
type, using Concat<>
.
Then,
type OpCircle = Concat3<Colors, Vertex3D, [number] /* radius */>;
From the above, you can write a concat function with overloads for 2,3,4,5,6,etc. number of arguments.
It's even possible to write a Concat<> impl that takes a tuple of tuples and concats the tuples. A var-arg Concat<> type.
It's not a thing that cannot be done today. It can be done, even if it requires you to write a recursive type, and helper function.
Everytime when I use those recursive types in vscode, TS wants to kill the cpu or it just hangs! That's the main problem, I feel TS is getting too heavy for no good reason.
Maybe the people writing the types are not doing enough to optimize it?
I don't wanna spam or drag in the old conversations that won't add much value to this thread but I remember at some point hearing that computation of recursive conditional types is expensive in javascript (I found it here in this thread)
That said (I know it might sound crazy,) maybe it's time to rewrite TS in another language to be able to give a boost to the type system as TS already has grown so much that it's reasonable to ask for a better one.
You can easily write a
Concat<>
type, and make aConcat3<>
type, usingConcat<>
.
Could you please provide an implementation for the Concat<>
type you're describing? It's easy to write Cons<>
, but Concat<>
is not so easy (for me) and I'd love to see what you're envisioning.
Regarding Concat3<>
, Concat4<>
, etc., the hope is that in the long term we won't need to write dozens of variants like these, because we'll have variadic kinds. π If a good implementation of them is possible today, though, that would be a reasonable stopgap measure.
For regular concatenation of two tuples,
https://github.com/AnyhowStep/ts-trampoline-test (uses trampolines to Concat very large tuples, which most people won't need)
Concat3 would just be Concat
The VarArgConcat would be,
VarArgConcat<TuplesT extends readonly (readonly unknown[])[], ResultT extends readonly unknown[] = []>
While the tuple isn't empty, VargArgConcat<PopFront<TuplesT>, Concat<ResultT, TuplesT[0]>>
If TuplesT is empty, return ResultT
Of course, naive recursion will lead to max depth errors with tuples of a decent length. So, either use the recursion technique in ts-toolbelt, or use trampolines with copy-pasting to the desired depth
That repo I linked to uses Reverse<>
to implement Concat<>
. I copy-pasted the code from another project I'm working on.
I agree that this would be a tremendously useful feature.
Let's say we have a type T
:
type T = {
tags: ["a", "b", "c"];
};
And we want to create a new type, with an additional tag "d"
, added to the T["tags"]
tuple. Users might initially try and create this utility (WithTag<NewTag, ApplyTo>
) as follows:
type WithTag<
Tag extends string,
Target extends {tags: string[]}
> = Target & {
tags: [Tag, ...Target["tags"]];
};
Attempting this currently throws the error A rest element type must be an array type
. Users might think that swapping string[]
for Array<string>
makes a difference, but it does not. Nor does using a condition + never
:
type WithTag<
Tag extends string,
Target extends {tags: string[]}
> = Target & {
- tags: [Tag, ...Target["tags"]];
+ tags: Target["tags"] extends string[] ? [Tag, ...Target["tags"]] : never;
};
Playground Link: https://www.typescriptlang.org/play?#code/C4TwDgpgBA6glsAFgFQIYHMA8AoKU3pQQAewEAdgCYDOU1wATnOegDS76oPoTBGkUaUAN7AM1AFx1GzdAG0AugF9sAPigBeTt15QAZCI5j0kqHIKsoAOhtodwOQCJj1RwoUBubEq-ZQkKABJTUM8FyknVEdLRwAjaKhHAGM3Lx9sP3BoZBD4JAJMR0oEwNVfAHoAKkrcSqgAUWJIJLJKKAADZHaoYAB7KFjoXoAzHsRoYd6AGynegHdZHqyrWqhV4VWe8QjHKJj4mJSY4s9VlShK8qA
Related Issues::
Unfortunately, this strategy does not work for rest parameters. These just get turned into arrays:
function i(a: number, b?: string, ...c: boolean[]): number { } let curried = curry(i, 12); curried('foo', [true, false]); curried([true, false]);
Here, curried:
...([string, boolean[]] | [boolean[]]) => number
.
I think this could be supported if there were a special case for functions with a tuple rest parameter, where the last element of the tuple is an array.
In that case the function call would allow extra arguments of the correct type to match the array.
However, that seems too complex to be worthwhile.
This has two problems:
curried()
does not accept an array. Filling the c
rest parameter with [true, false]
could be done by curried('foo', ...[true, false])
but this will fail in TypeScript with this suggestion. We may be able to not provide typing solution for some case, but it's discouraged to provide someone wrong!curried()
can not be called without b
but with c
. Doing so will lead to misbehavior. TypeScript knows curried()
is (...items: [string, boolean[]] | [boolean[]])
but that's just not true. Because JavaScript is typing-less, passing [true, false]
to c
(assuming we solved the above problem) with curried([true, false])
will not set b
to undefined
(or its default value) and c
to [true, false]
, but will set b
to true
and c
to [false]
!I suggest the following fixes:
[number, string, boolean[]] | [number, boolean[]]
in our case) when there is rest parameter. Instead, infer [number, string, boolean[]] | [number]
- that is, one case for full signature, including all optionals and rest, one for each optional except the last, and one without the last and the rest.[t1, t2, t3, ...arr]
), but we not need to. We can stay with it as an internal (haha, you'll still have to deal with how to display the type in an IDE π).But after all complaints and provocativenesses, great proposal! Thank you π (just to pacify you, this is the first issue ever in GitHub I responded to with three emojis - π, π and β€οΈ).
This would be really useful in the Angular Injector which currently have to use any
https://github.com/angular/angular/issues/37264
In this example A, B, C could be represented as a single ...A
variadic generic type. But I have no idea how this would map to something where each element of the variadic generic would be enclosed in another type (Type
). maybe with a helper type? Or should the syntax allow something like ...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>];
}
(context: an implementation of Provider
would inject instances of deps
into that factory function in that order. A strict typing would ensure that the developer knows what would be injected and in what order.)
When this is done please remember to update String.prototype.replace's second parameter, so that it finally has proper typing in Typescript!
@Griffork You do realize that would require parsing the regex to figure out how many capture groups it has, right?
This would be really useful in the Angular Injector which currently have to use
any
angular/angular#37264In this example A, B, C could be represented as a single
...A
variadic generic type. But I have no idea how this would map to something where each element of the variadic generic would be enclosed in another type (Type
). maybe with a helper type? Or should the syntax allow something like...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>]; }
(context: an implementation of
Provider
would inject instances ofdeps
into that factory function in that order. A strict typing would ensure that the developer knows what would be injected and in what order.)
@AlexAegis
I feel that it would be typed something like this:
export declare interface TypedFactoryProvider<T, ...P> {
provide: Type<T | T[]> | InjectionToken<T | T[]>;
multi?: boolean;
useFactory: (...providers: ...P) => T;
deps: [...Type<P>];
}
This issue is now fixed by #39094, slated for TS 4.0.
If this is coming with 4.0, we now have a reason to name it 4.0 π
This is really a major new feature π
This is great! Only thing "left" is the same for literal string types
@sandersn I'm trying to think about how would this syntax be used in things like RxJS
, where the pipe
method parameters are sorta dependent one one another,
as in pipe(map<T, V>(...), map<V, U>(...), filter(...), ...)
. How would you type it in a way that's not what they do now? (The dozens of lines of different variadic lengths typing)
@gioragutt using the PR that @ahejlsberg submitted I think this would work but I could be wrong though π
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 Doesn't quite work, due to a circular type error.
However, the usual workaround works.
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 That doesn't seem to work for me. π’
Playground example.
I got something closer to working but it won't deduce the types.
playground example
I had to change
R extends readonly [unknown] ? 1 : 0
to
R extends readonly [infer _, ...infer __] ? 1 : 0
Not sure why
@tylorr @treybrisbane Might be related: https://github.com/microsoft/TypeScript/pull/39094#issuecomment-645730082
Also, in either case, I'd highly recommend sharing that in the pull request that comment's in.
Variadic tuple types are an awesome addition to the language, thank you for the effort!
It seems, constructs like curry
might also benefit (just tested with the 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
Generic function don't work with above curry though.
I don't believe this PR solves this particular issue tbh.
With the PR this works
function foo<T extends any[]>(a: [...T]) {
console.log(a)
}
foo<[number, string]>([12, '13']);
But this issue would like to see an implementation for this as far as I see:
function bar<...T>(...b: ...T) {
console.log(b)
}
bar<number, string>(12, '13');
There is a lot angle brackets there, looks a little redundant.
@AlexAegis I'm not sure I see a lot of value in "rest type parameters" like that. You can already do this:
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
Don't think we really want a whole new concept (i.e. rest type parameters) just so the square brackets can be avoided in the rare cases where inference can't figure it out.
@ahejlsberg I see. I was asking because some libraries (RxJS as mentioned) used workarounds to provide this functionality. But it's finite.
bar<T1>(t1: T1);
bar<T1, T2>(t1: T1, t2:T2);
bar<T1, T2, T3>(t1: T1, t2:T2, t3: T3, ...t: unknown) { ... }
So now they either stick with that, or have the users type the brackets, which is a breaking change, and not that intuitive.
The reason why I used this example is because here it's straightforward that I defined the type of that tuple. One square bracket here, one there
foo<[number, string]>([12, '13']);
Here it's not so obvious that the tuple refers to that rest parameter if you look at it from the outside
foo<[number, string]>(12, '13');
But yes as you said if we let the inference figure it out then these trivial cases are not requiring any modification from the user. But we don't know if they did set them explicitly or not, it's up to them, so it still counts as a breaking change. But that's the lib's concern and not this change's.
That said I just find it odd that if there are rest parameters, defined from the outside one by one, that are a single array on the inside differentiated by ...
, cannot be made generic the same way: one by one on the outside, single array on the inside, differentiated by ...
.
Minor syntax discrepancies are not really worth support cost for a separate
kind. Using kinds would be a correct design decision when TS was planning
support for rest parameters, but I guess now it might lead to more
confusion both for language developers and users. We needed a solution for
this issue, and Anders did his job exceptionally well avoiding that
complexity by sticking to [...T]
instead of T
. Hats off!
(Could we now take a look on a bug that unifying intersection type to
inferred variable in conditional type returns rightmost intersection type
argument, or that union of arrays is not array of union please? We still
have major showstoppers in type system.)
On Fri, Jun 19, 2020, 10:41 GyΕri SΓ‘ndor notifications@github.com wrote:
@ahejlsberg https://github.com/ahejlsberg I see. I was asking because
some libraries (RxJS as mentioned) used workarounds to provide this
functionality. But it's finite.bar
(t1: T1);bar (t1: T1, t2:T2);bar (t1: T1, t2:T2, t3: T3, ...t: unknown) { ... } So now they either stick with that, or have the users type the brackets,
which is not that intuitive.The reason why I used this example is because here it's straightforward
that I defined the type of that tuple. One square bracket here, one therefoo<[number, string]>([12, '13']);
Here it's not so obvious that the tuple refers to that rest parameter if
you look at it from the outsidefoo<[number, string]>(12, '13');
β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646490130,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAWYQIMTTB6JEPSQFUMTMDTRXMJD5ANCNFSM4BTBQ7DQ
.
I am of course nowhere close to his caliber, but I respectfully disagree with @ahejlsberg .
In my experience, much of the complexity of typescript comes from the fact that a lot of (interesting and useful to be sure) features are special-cased in as their own concepts.
This complexity is not inherently a function of the number of features though!
Instead, the language could be designed around larger, more overarching concepts from which these special cases could then be trivially deduced, or implemented in the std (type) library.
The most general such concept would of course be to fully implement dependent types, from which everything else could then be derived, but going that far is not necessary:
As C++ and, to a lesser extent, Rust have shown, a few large scale, consistent concepts give you a ton of features for free.
This is similar to what OCaml and Haskell (and I assume F#?) have done on the value level, just on the type level.
Type level programming is nothing to be scared of as long as it is designed into the language instead of tacked on to provide specific features.
The facilities in C++ 14/17 are very intuitive except for their syntax, which is purely due to historical baggage.
Overarching concepts could have been added in original design. After design
mistake was already made, consistency cannot be added without risking huge
back-incompatibility. I agree with suspicions regarding language design as
a whole (TS is quite far from standards set by academia, nobody can
disagree with that). There is a lot of bugs and inconsistencies that are
foundational to millions of production code bases. Mere fact that
developers are able to come up with useful additions to the language
without accidentally fixing those bugs is, in my humble opinion, awesome
and deserves respect. TS has same design complexities as C++ here, but its
expressive type system makes the situation worse.
On Fri, Jun 19, 2020, 12:47 Bennett Piater notifications@github.com wrote:
I am of course nowhere close to his caliber, but I respectfully disagree
with @ahejlsberg https://github.com/ahejlsberg .
In my experience, much of the complexity of typescript comes from the
fact that a lot of (interesting and useful to be sure) features are
special-cased in as their own concepts.This complexity is not inherently a function of the number of features
though!
Instead, the language could be designed around larger, more overarching
concepts from which these special cases could then be trivially deduced, or
implemented in the std (type) library.The most general such concept would of course be to fully implement
dependent types, from which everything else could then be derived, but
going that far is not necessary:
As C++ and, to a lesser extent, Rust have shown, a few large scale,
consistent concepts give you a ton of features for free.
This is similar to what OCaml and Haskell (and I assume F#?) have done on
the value level, just on the type level.Type level programming is nothing to be scared of as long as it is
designed into the language instead of tacked on to provide specific
features.
The facilities in C++ 14/17 are very intuitive except for their syntax,
which is purely due to historical baggage.β
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/microsoft/TypeScript/issues/5453#issuecomment-646543896,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAWYQIMWYLGGCWPTDBZJR4TRXMX4RANCNFSM4BTBQ7DQ
.
@polkovnikov-ph I'm glad we agree on the issue at hand :)
As for the solution, I think it would still be worth considering progressively moving towards a more carefully designed type system. Major versions are a thing after all, and the alternative is to end up in the cluster** that is C++ 20 - an admirable attempt at adding even more nicely designed features on top of 2 layers of previous attempts that cannot be removed, in a syntax that is already not deterministically parseable.
All of this is off-topic to this thread and is being discussed here. So I'll try to be frank:
It took decades for academia to figure out correct approach to subtyping: mlsub type system was created only 6 years ago, well after TypeScript was first released. It could be that foundation for classes, interfaces, union and intersection types with overarching concepts.
But also remember there are conditional types. I'm not aware of any papers giving them formal semantics, or describing a minimal type system with conditional types with progress/preservation proofs. I believe that might have something to do with scientists still being shy to print their failed attempts. If your proposal assumes those major incompatible versions will be made in 2040's, when academia gets comfortable with conditional types, I can agree.
Otherwise "carefully designed type system" would have to remove conditional types from the language, and I don't think anyone is up to the task of converting 60% of DefinitelyTyped to use whatever alternative is chosen to replace them. (And then do it several more times, because it's not the only issue.)
I'm afraid the only viable solution is to create a separate programming language that would somehow resemble TS, and somehow (not only by being more pleasurable to write code in) lure developers to use it. Ryan was quite vocal recommending this approach for TS improvement previously.
Most helpful comment
This issue is now fixed by #39094, slated for TS 4.0.