Skip to content

New syntax for advanced typing array of arguments #33778

Description

Search Terms

new syntax, advanced types, generic, array of arguments

Suggestion

Hi, everybody!
I think it would be cool to create a solution to this problem.

If you want to control the typing of the result of a function based on an unlimited number of incoming arguments, you will need a huge number of overloads.

Use Cases

For example, you have a class that contains a property configuration:

class PropertyConstructor<T> {
  type: T;
}

class PropertyConfiguration<T, S extends keyof any> {
   constructor(
      public propertyKey: S,
      public propertyConstructor: new () => PropertyConstructor<T>,
   ) {}
}

Next, create several inheritors of constructors:

class StringPropertyConstructor extends PropertyConstructor<string> {
}

class NumberPropertyConstructor extends PropertyConstructor<number> {
}

Next, create function, that will build class with property configs:

type Join<T, S extends keyof any> = {[P in S]: T};

function CreateClass<T1, S1 extends keyof any>(p1: PropertyConfiguration<T1, S1>): new () => Join<T1, S1>;
function CreateClass<T1, S1 extends keyof any, T2, S2 extends keyof any>(
  p1: PropertyConfiguration<T1, S1>,
  p2: PropertyConfiguration<T2, S2>,
): new () => Join<T1, S1> & Join<T2, S2>;
// ... And so on. It's very uncomfortable and ugly.
function CreateClass(...properties: Array<PropertyConfiguration<any, keyof any>>): new () => any {
  class NewClass {}
  // Here was the logic of executing property constructors
  return NewClass;
}

And create some instances of PropertyConfiguration:

const property1 = new PropertyConfiguration('property1', StringPropertyConstructor);
const property2 = new PropertyConfiguration('property2', NumberPropertyConstructor);

Funnaly, Let's try to apply it:

class A extends CreateClass(property1, property2) {
}

const a = new A();
a.property1 // string;
a.property2 // number;

It's works! But, very uncomfortable and ugly. For example if you want to create class with ten properties, you need to implement corresponded overloads.

Examples

As I see the solution to this problem:

type Join<T, S extends keyof any> = {[P in S]: T};

type Union<T extends Array<any>> = ... 
/*
built-in type for compare array types.
Example:
Union<[string, number]> => string & number
Union<[Join<string, 'property1'>, Join<number, 'property2'>]> => Join<string, 'property1'> & Join<number, 'property2'>
*/

function CreateClass<N extends number, T{N}, S{N} extends keyof any>(
  ...properties: [PropertyConfiguration<T{N}, S{N}>]
): new () => Union<[Join<T{N}, S{N}]> {
  class NewClass {}
  // Here was the logic of executing property constructors
  return NewClass;
}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Activity

  1. sviat9440 commented on Oct 3, 2019

    @sviat9440
    ContributorAuthor

    Well, or a more simple example:

    You can look at the typification of methods Promise.all or Promise.race...

    image

  2. AnyhowStep commented on Oct 3, 2019

    @AnyhowStep
    Contributor

    Rest arguments and tuple types?

  3. sviat9440 commented on Oct 3, 2019

    @sviat9440
    ContributorAuthor
  4. AnyhowStep commented on Oct 3, 2019

    @AnyhowStep
    Contributor

    I'm on mobile, so, I'm not too sure if this would work but your Promise.all thing can probably be written with far fewer overloads,

    //We want T to distribute
    type UnwrapPromiseLike<T> = T extends PromiseLike<infer U> ? U : T;
    //snip
    
    //for empty tuple
    all (arr : []) : Promise<[]>;
    //for non-empty tuple
    all<ArrT extends any[]> (arr : ArrT & {"0":any}) : Promise<{ [i in keyof ArrT] : UnwrapPromiseLike<ArrT[i]> }>;
    //for non-tuple array
    all<ArrT extends any[]> (arr : ArrT) : Promise<{ [i in keyof ArrT] : UnwrapPromiseLike<ArrT[i]> }>;

    The idea is that we use {"0":any} to make TS infer a non-empty tuple of arbitrary length. Then, we use a mapped type to return a tuple with the await'd types.

    We place the "regular array" overload after the tuple overload as a fallback. It does the same thing as the tuple overload but you get back a regular array instead.

    The empty tuple overload is for the empty tuple case.


    Again, I'm typing all this on mobile so I'm not 100% if the above construction will work but it should be possible to get it working with some tweaking if I've messed up.


    However, if you have rest arguments, your life should be much easier.

    //We want T to distribute
    type UnwrapPromiseLike<T> = T extends PromiseLike<infer U> ? U : T;
    //snip
    
    //should handle empty tuple, non-empty tuple, and regular arrays
    all<ArrT extends any[]> (...arr : ArrT) : Promise<{ [i in keyof ArrT] : UnwrapPromiseLike<ArrT[i]> }>;
  5. sviat9440 commented on Oct 3, 2019

    @sviat9440
    ContributorAuthor

    AnyhowStep Promise.all is a method of Typescript npm lib. It is not my method.
    File: lib.es2015.promise.d.ts.
    I mentioned this to have more than one example of use.

    But I will try to apply your solution in my case. Thank you!)

  6. AnyhowStep commented on Oct 3, 2019

    @AnyhowStep
    Contributor

    I know. I'm just saying it can be written to handle tuples of arbitrary length.

    We don't really need new special syntax to handle variable argument generic types because we have tuple types and array/tuple mapped types now.

    TS also somewhat unofficially supports recursive types right now, which can be used for pretty insane type level operations.

    And it will get more official support for recursive types with 3.7.

    I'm not entirely sure how thorough the new official support will be, though.


    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

    #5453

  7. sviat9440 commented on Oct 3, 2019

    @sviat9440
    ContributorAuthor

    AnyhowStep Is it possible to implement something like Union type, from the first example?

  8. AnyhowStep commented on Oct 3, 2019

    @AnyhowStep
    Contributor

    What you're looking for is UnionToIntersection<U>.

    https://github.com/AnyhowStep/type-mapping/blob/master/src/type-util/union-to-intersection.ts


    type Foo = [string, number];
    //string & number
    type Bar = UnionToIntersection<Foo[number]>; 
  9. AnyhowStep commented on Oct 3, 2019

    @AnyhowStep
    Contributor

    Sorry for the double post but,

    #29594 (comment)

    #29594

  10. AnyhowStep commented on Nov 3, 2019

    @AnyhowStep
    Contributor

    Copy-pasting my comment on #33707 over here.

    This is better,

    type Awaited<T> =
        T extends undefined ?
        T :
        T extends PromiseLike<infer U> ?
        U :
        T
    ;
    declare function all<T extends readonly any[]>(
        values: T
    ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>;
    
    declare const numArr: number[];
    
    //const newAll0: Promise<never[]>
    const newAll0 = all([]);
    //const newAll1: Promise<(string | number | boolean)[]>
    const newAll1 = all([1, "2", true]);
    //const newAll2: Promise<number[]>
    const newAll2 = all(numArr);
    
    //const curAll0: Promise<never[]>
    const curAll0 = Promise.all([]);
    //const curAll1: Promise<(string | number | boolean)[]>
    const curAll1 = Promise.all([1, "2", true]);
    //const curAll2: Promise<number[]>
    const curAll2 = Promise.all(numArr);
    
    //for empty tuple
    declare function betterAll (arr : []) : Promise<[]>;
    //for non-empty tuple
    declare function betterAll<ArrT extends readonly [any, ...any[]]>(
        arr: ArrT
    ): Promise<{ -readonly [i in keyof ArrT]: Awaited<ArrT[i]> }>;
    //for non-tuple array
    declare function betterAll<ArrT extends readonly any[]>(
        arr: ArrT
    ): Promise<{ -readonly [i in keyof ArrT]: Awaited<ArrT[i]> }>;
    
    //const betterAll0: Promise<[]>
    const betterAll0 = betterAll([]);
    //const betterAll1: Promise<[number, string, boolean]>
    const betterAll1 = betterAll([1, "2", true]);
    //const betterAll2: Promise<number[]>
    const betterAll2 = betterAll(numArr);

    Playground

    See all (this PR) vs Promise.all (current impl) vs betterAll (mine)

  11. sviat9440 commented on Nov 6, 2019

    @sviat9440
    ContributorAuthor

    Thank you for solution. It's really helps me!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions