Skip to content
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

New syntax for advanced typing array of arguments #33778

Closed
5 tasks done
sviat9440 opened this issue Oct 3, 2019 · 11 comments
Closed
5 tasks done

New syntax for advanced typing array of arguments #33778

sviat9440 opened this issue Oct 3, 2019 · 11 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@sviat9440
Copy link
Contributor

sviat9440 commented Oct 3, 2019

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.
@sviat9440
Copy link
Contributor Author

Well, or a more simple example:

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

image

@AnyhowStep
Copy link
Contributor

Rest arguments and tuple types?

@sviat9440
Copy link
Contributor Author

@AnyhowStep +

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 3, 2019

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

@sviat9440
Copy link
Contributor Author

sviat9440 commented Oct 3, 2019

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 3, 2019

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

@sviat9440
Copy link
Contributor Author

sviat9440 commented Oct 3, 2019

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

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Oct 3, 2019

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

@AnyhowStep
Copy link
Contributor

Sorry for the double post but,

#29594 (comment)

#29594

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Oct 17, 2019
@AnyhowStep
Copy link
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)

@sviat9440
Copy link
Contributor Author

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
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants