New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Variadic Kinds -- Give specific types to variadic functions #5453

Open
sandersn opened this Issue Oct 29, 2015 · 197 comments

Comments

Projects
None yet
@sandersn
Member

sandersn commented Oct 29, 2015

Variadic Kinds

Give Specific Types to Variadic Functions

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:

  1. #5331 -- Tuples as types for rest ...arguments
  2. #4130 -- Compiler incorrectly reports parameter/call target signature mismatch when using the spread operator
  3. #4988 -- Tuples should be clonable with Array.prototype.slice()
  4. #1773 -- Variadic generics?
  5. #3870 -- Rest types in generics for intersection types.
  6. #212 -- bind, call and apply are untyped (requires #3694's this-function types).
  7. #1024 -- Typed ...rest parameters with generics

I'll be updating this proposal on my fork of the Typescript-Handbook: sandersn/TypeScript-Handbook@76f5a75
I have an in-progress implementation at sandersn/TypeScript@f3c327a 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.

Preview example with 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 = []

Syntax

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

Semantics

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 off,let a:...Tis instantiated aslet a:[number,number,string]. However, because the type ofais 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 froma` 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].

Limits on type inference

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

  1. ...T = [string,string,string], ...U = [string]
  2. ...T = [string,string], ...U = [string,string]
  3. ...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.

Semantics on classes and interfaces

The semantics are the same on classes and interfaces.

TODO: There are probably some class-specific wrinkles in the semantics.

Assignability between tuples and parameter lists

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

Tuple types generated for optional and rest parameters

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.

Extensions to the other parts of typescript

  1. Typescript does not allow users to write an empty tuple type.
    However, this proposal requires variadic kinds to be bindable to a empty tuple.
    So Typescript will need to support empty tuples, even if only internally.

Examples

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.

Return a concatenated type

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]

Concatenated type as parameter

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"

Variadic functions as arguments

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

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

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

TODO: Could f return ...U instead of U?

Decorators

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

Open questions

  1. Does the tuple-to-parameter-list assignability story hold up? It's especially shaky around optional and rest parameters.
  2. Will the inferred type be a union of tuples as with the optional-parameter case? Because 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.
  3. Should rest parameters be special-cased to retain their nice calling syntax, even when they are generated from a tuple type? (In this proposal, functions typed by a tuple kind have to pass arrays to their rest parameters, they can't have extra parameters.)
@ivogabe

This comment has been minimized.

Contributor

ivogabe commented Oct 31, 2015

+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?

@sandersn

This comment has been minimized.

Member

sandersn commented Nov 2, 2015

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.

@sandersn

This comment has been minimized.

Member

sandersn commented Nov 2, 2015

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.

@sandersn

This comment has been minimized.

Member

sandersn commented Nov 4, 2015

@ahejlsberg, you had some ideas how tuple kinds would work, I think.

@kitsonk

This comment has been minimized.

Contributor

kitsonk commented Nov 5, 2015

So 👍 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?

@sandersn

This comment has been minimized.

Member

sandersn commented Nov 5, 2015

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.

@sccolbert

This comment has been minimized.

sccolbert commented Nov 5, 2015

Big 👍 on this!

@Igorbek

This comment has been minimized.

Contributor

Igorbek commented Nov 6, 2015

+1 awesome! It would allow to express such things much more expressive and lightweight.

@JsonFreeman

This comment has been minimized.

Contributor

JsonFreeman commented Nov 6, 2015

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?
@sandersn

This comment has been minimized.

Member

sandersn commented Nov 6, 2015

@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:

  1. Union the types: f(['hello', 1], [1, false]): [string | number, number | boolean]
  2. Disallow inference of repeated tuple kind parameters, particularly if type argument inference proves complicated. Something like this:
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?

@JsonFreeman

This comment has been minimized.

Contributor

JsonFreeman commented Nov 6, 2015

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.

@sandersn

This comment has been minimized.

Member

sandersn commented Nov 6, 2015

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.

@JsonFreeman

This comment has been minimized.

Contributor

JsonFreeman commented Nov 7, 2015

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?

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Dec 17, 2015

If I understand correctly, this would actually solve the mixin story in TypeScript. Am I correct in this understanding?

@sandersn

This comment has been minimized.

Member

sandersn commented Dec 17, 2015

Maybe. Can you give an example? I'm not fluent with mixin patterns.

@aleksey-bykov

This comment has been minimized.

aleksey-bykov commented Dec 18, 2015

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?

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Dec 18, 2015

@aleksey-bykov +1. I don't see a reason why that shouldn't be the case.

@aleksey-bykov

This comment has been minimized.

aleksey-bykov commented Dec 18, 2015

Developers with Haskell background would appreciate that.

@sandersn

This comment has been minimized.

Member

sandersn commented Dec 19, 2015

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.

@aleksey-bykov

This comment has been minimized.

aleksey-bykov commented Dec 19, 2015

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?

@maciejw

This comment has been minimized.

maciejw commented Aug 21, 2018

sure, but this is kind of hacky with those as any :) I think it would be cool if type system supported this without hacks

@Yuudaari

This comment has been minimized.

Yuudaari commented Aug 21, 2018

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

@osdiab

This comment has been minimized.

osdiab commented Sep 4, 2018

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

@kgtkr

This comment has been minimized.

kgtkr commented Sep 8, 2018

I succeeded in typing Pipe
https://github.com/kgtkr/typepark/blob/master/src/pipe.ts

@tycho01

This comment has been minimized.

Contributor

tycho01 commented Sep 8, 2018

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

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 8, 2018

@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:

  • To make for an easier time doing more complex work like list iteration. It'd also lay the framework for opening up things like type-level integer math without crashing the compiler or ending up in some ad-hoc mess.
  • To more properly address the issue of what makes TS's type system Turing-complete, so it can be potentially either reified with tools leveraging it or removed by enforcing provable termination down the road.

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

@tycho01

This comment has been minimized.

Contributor

tycho01 commented Sep 8, 2018

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.

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 8, 2018

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

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 8, 2018

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

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 9, 2018

I haven't tested it on a potential _.flow or _.flowRight type yet, but that should work as a starting point.

@kgtkr

This comment has been minimized.

kgtkr commented Sep 9, 2018

@tycho01
required
typescript@next
3.0.1/3.0.2 does not work

@KSXGitHub

This comment has been minimized.

Contributor

KSXGitHub commented Sep 9, 2018

Thanks to this thread, I made this

@Yuudaari

This comment has been minimized.

Yuudaari commented Sep 9, 2018

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

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 9, 2018

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

@tycho01

This comment has been minimized.

Contributor

tycho01 commented Sep 9, 2018

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?

@Yuudaari

This comment has been minimized.

Yuudaari commented Sep 9, 2018

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?

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 9, 2018

@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:

  1. Concatenating two lists via [...First, ...Second].
  2. Appending values via [...Values, Item].
  3. Extracting the last element via T extends [...any[], infer Last].
  4. Extracting the tail via 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.

@jcalz

This comment has been minimized.

Contributor

jcalz commented Sep 13, 2018

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!

☹️

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Sep 14, 2018

@jcalz So all the more reason for #26980 to exist?

@ozra

This comment has been minimized.

ozra commented Nov 22, 2018

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

@isiahmeadows

This comment has been minimized.

Contributor

isiahmeadows commented Nov 27, 2018

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:

  • If the element has a src attribute:
    • Zero or more <track> elements
  • If the element does not have a src attribute:
    • Zero or more <source> elements
  • Zero or more elements as per the parent's content model, except no audio 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]},
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment