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

Variadic tuple types #39094

Merged
merged 37 commits into from Jun 23, 2020
Merged

Variadic tuple types #39094

merged 37 commits into from Jun 23, 2020

Conversation

@ahejlsberg
Copy link
Member

ahejlsberg commented Jun 16, 2020

This PR implements variadic tuple types, i.e. the ability for tuple types to have spreads of generic types that can be replaced with actual elements through type instantiation. The PR effectively implements the features discussed in #5453.

Some examples of new capabilities provided in this PR:

// Variadic tuple elements

type Foo<T extends unknown[]> = [string, ...T, number];

type T1 = Foo<[boolean]>;  // [string, boolean, number]
type T2 = Foo<[number, number]>;  // [string, number, number, number]
type T3 = Foo<[]>;  // [string, number]

// Strongly typed tuple concatenation

function concat<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]): [...T, ...U] {
    return [...t, ...u];
}

const ns = [0, 1, 2, 3];  // number[]

const t1 = concat([1, 2], ['hello']);  // [number, number, string]
const t2 = concat([true], t1);  // [boolean, number, number, string]
const t3 = concat([true], ns);  // [boolean, ...number[]]

// Inferring parts of tuple types

declare function foo<T extends string[], U>(...args: [...T, () => void]): T;

foo(() => {});  // []
foo('hello', 'world', () => {});  // ["hello", "world"]
foo('hello', 42, () => {});  // Error, number not assignable to string

// Inferring to a composite tuple type

function curry<T extends unknown[], U extends unknown[], R>(f: (...args: [...T, ...U]) => R, ...a: T) {
    return (...b: U) => f(...a, ...b);
}

const fn1 = (a: number, b: string, c: boolean, d: string[]) => 0;

const c0 = curry(fn1);  // (a: number, b: string, c: boolean, d: string[]) => number
const c1 = curry(fn1, 1);  // (b: string, c: boolean, d: string[]) => number
const c2 = curry(fn1, 1, 'abc');  // (c: boolean, d: string[]) => number
const c3 = curry(fn1, 1, 'abc', true);  // (d: string[]) => number
const c4 = curry(fn1, 1, 'abc', true, ['x', 'y']);  // () => number

Structure and instantiation

The basic structure of a non-generic tuple type remains unchanged with this PR: Zero or more required elements, followed by zero or more optional elements, optionally followed by a rest element (for example [A, B?, ...C[]]). However, it is now possible to have variadic elements anywhere in a tuple type, except following the optional rest element (for example [A, ...T, B?, ...U, ...C[]]).

A variadic elemement is a spread element of the form ...T, where T is a generic type constrained to any array or tuple type (specifically, any type that is assignable to readonly any[]). Intuitively, a variadic element ...T is a placeholder that is replaced with one or more elements through generic type instantiation. Instantiation of a tuple type with a variadic element ...T depends on the type argument provided for T as follows:

  • When the type argument for T is a union type, the union is spread over the tuple type. For example, [A, ...T, B] instantiated with X | Y | Z as the type argument for T yields a union of instantiations of [A, ...T, B] with X, Y and Z as the type argument for T respectively.
  • When the type argument is a tuple type, T is replaced with the elements of that tuple type. For example, [A, ...T, B] instantiated with [X, Y] as the type argument for T yields [A, X, Y, B].
  • When the type argument is an array type, T is replaced with a rest element. For example, [A, ...T] instantiated with X[] as the type argument for T yields [A, ...X[]].
  • When the type argument is another generic type, T is simply replaced with that type.
  • When the type argument is any, T is replaced with ...any[].
  • When the type argument is never, the entire result is never.

Instantiation of a generic tuple type includes normalization to ensure the resulting tuple type follows the basic structure described above. Specifically:

  • Optionality is removed from any optional elements that precede a required element. For example, [A?, ...T, B?] instantiated with [X, Y?] as the type argument for T yields [A, X, Y?, B?].
  • Rest elements absorb any following elements. For example, [A, ...T, B] instantiated with X[] as the type argument for T yields [A, ...(X | B)[]].
  • Tuples with only a single rest element are reduced to array types. For example, [...X[]] is reduced to simply X[].

Type relationships

Generally, a tuple type S is related to a tuple type T by pairwise relating elements of S to the elements of T. Variadic elements are processed as follows:

  • A variadic element ...U in S is related to a variadic element ...V in T if U is related to V.
  • A variadic element ...U in S is related to a rest element ...X[] in T if U is related to X[].

Some examples:

function foo1<T extends unknown[], U extends T>(x: [string, ...unknown[]], y: [string, ...T], z: [string, ...U]) {
    x = y;  // Ok
    x = z;  // Ok
    y = x;  // Error
    y = z;  // Ok
    z = x;  // Error
    z = y;  // Error
}

Tuple types with single variadic elements have the following relations:

  • [...T] is related to T.
  • T is related to readonly [...T].
  • T is related to [...T] when T is constrained to a mutable array or tuple type.

Some examples:

function foo2<T extends readonly unknown[]>(t: T, m: [...T], r: readonly [...T]) {
    t = m;  // Ok
    t = r;  // Error
    m = t;  // Error
    m = r;  // Error
    r = t;  // Ok
    r = m;  // Ok
}

Type inference

Inference between tuple types with the same structure (i.e. same number of elements and fixed, variadic, or rest kind matched to the same kind in each position), simply infers pairwise between the element types. For example, inference from [string, ...Partial<S>, number?] to [string, ...T, number?] infers Partial<S> for T.

Inference between tuple types S and T with different structure divides each tuple into a starting fixed part, a middle part, and an ending fixed part. Any one of these parts may be empty.

  • The starting fixed parts of S and T consist of those elements in S and T that are fixed (i.e. neither variadic nor rest elements) in both types matching from the start of each type.

  • If T contains at least one variadic element and S has no ending rest element, the ending fixed parts of S and T consist of those elements in S and T that are fixed in both types matching from the end of each type.

  • If T contains at least one variadic element and S has an ending rest element, the ending fixed part of T consists of those elements in T that are fixed matching from the end of the type, and the ending fixed part of S is empty.

  • If T contains no variadic elements, the ending fixed parts of S and T are empty.

  • The middle parts of S and T are those elements in S and T that remain between the starting and ending fixed parts of the types respectively.

Inference then proceeds as follows:

  • Pairwise infer between the elements in the starting parts.

  • If the middle part of S is a single rest element, infer from that rest element to every element in the middle part of T.

  • If the middle part of T is a single variadic or rest element, infer from a tuple consisting of the middle part of S to that variadic or rest element.

  • If the middle part of T is exactly two variadic elements ...A and ...B, and an implied arity exists for A, infer from a tuple consisting of the initial middle part of S to A and from a tuple consisting of the remaining middle part of S to B, where the length of the initial middle part corresponds to the implied arity for A.

  • Pairwise infer between the elements in the ending parts, or infer from the rest element in S to the elements of the ending part of T.

In the context of inference for a call of a generic function with a rest parameter R, the implied arity for R is the number of rest arguments supplied for R. In all other contexts, a type parameter has no implied arity. For an example of inference involving an implied arity, see the curry function in the introduction.

Some examples:

type First<T extends readonly unknown[]> = T[0];
type DropFirst<T extends readonly unknown[]> = T extends readonly [any, ...infer U] ? U : [...T];

type Last<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : undefined;
type DropLast<T extends readonly unknown[]> = T extends readonly [...infer U, any] ? U : [...T];

type T1 = First<[number, boolean, string]>;  // [number]
type T2 = DropFirst<[number, boolean, string]>;  // [boolean, string]
type T3 = Last<[number, boolean, string]>;  // [string]
type T4 = DropLast<[number, boolean, string]>;  // [number, boolean]

Spreads in array literals

When an array literal has a tuple type, a spread of a value of a generic array-like type produces a variadic element. For example:

function foo3<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]) {
    return [1, ...t, 2, ...u, 3] as const;  // readonly [1, ...T, 2, ...U, 3]
}

const t = foo3(['hello'], [10, true]);  // readonly [1, string, 2, number, boolean, 3]

When the contextual type of an array literal is a tuple type, a tuple type is inferred for the array literal. The type [...T], where T is an array-like type parameter, can conveniently be used to indicate a preference for inference of tuple types:

declare function ft1<T extends unknown[]>(t: T): T;
declare function ft2<T extends unknown[]>(t: T): readonly [...T];
declare function ft3<T extends unknown[]>(t: [...T]): T;
declare function ft4<T extends unknown[]>(t: [...T]): readonly [...T];

ft1(['hello', 42]);  // (string | number)[]
ft2(['hello', 42]);  // readonly (string | number)[]
ft3(['hello', 42]);  // [string, number]
ft4(['hello', 42]);  // readonly [string, number]

Indexing and destructuring

Indexing and destructuring of generic tuple types appropriately recognizes fixed elements at the start of the tuple type. Beyond the fixed elements, the type is simply a union of the remaining element types.

function f1<T extends unknown[]>(t: [string, ...T], n: number) {
    const a = t[0];  // string
    const b = t[1];  // [string, ...T][1]
    const c = t[2];  // [string, ...T][2]
    const d = t[n];  // [string, ...T][number]
}

function f2<T extends unknown[]>(t: [string, ...T, number], n: number) {
    const a = t[0];  // string
    const b = t[1];  // [string, ...T, number][1]
    const c = t[2];  // [string, ...T, number][2]
    const d = t[n];  // [string, ...T, number][number]
}

function f3<T extends unknown[]>(t: [string, ...T]) {
    let [...ax] = t;  // [string, ...T]
    let [b1, ...bx] = t;  // string, [...T]
    let [c1, c2, ...cx] = t;  // string, [string, ...T][1], T[number][]
}

function f4<T extends unknown[]>(t: [string, ...T, number]) {
    let [...ax] = t;  // [string, ...T, number]
    let [b1, ...bx] = t;  // string, [...T, number]
    let [c1, c2, ...cx] = t;  // string, [string, ...T, number][1], (number | T[number])[]
}

Rest parameters and spread arguments

Spread expressions with fixed length tuples are now appropriately flattened in argument lists. For example:

declare function fs1(a: number, b: string, c: boolean, ...d: number[]): void;

function fs2(t1: [number, string], t2: [boolean], a1: number[]) {
    fs1(1, 'abc', true, 42, 43, 44);
    fs1(...t1, true, 42, 43, 44);
    fs1(...t1, ...t2, 42, 43, 44);
    fs1(...t1, ...t2, ...a1);
    fs1(...t1);  // Error: Expected at least 3 arguments, but got 2
    fs1(...t1, 45);  // Error: Type '45' is not assignable to type 'boolean'
}

A rest parameter of a generic tuple type can be used to infer types from the middle part of argument lists. For example:

declare function fr1<T extends unknown[]>(x: number, ...args: [...T, number]): T;

function fr2<U extends unknown[]>(u: U) {
    fr1(1, 2);  // []
    fr1(1, 'hello', true, 2);  // [string, boolean]
    fr1(1, ...u, 'hi', 2);  // [...U, string]
    fr1(1);  // Error: Expected 2 arguments, but got 1
}

Application of mapped types

When a mapped type is applied to a generic tuple type, non-variadic elements are eagerly mapped but variadic elements continue to be generic. Effectively, M<[A, B?, ...T, ...C[]] is resolved as [...M<[A]>, ...M<[B?]>, ...M<T>, ...M<C[]>]. For example:

type TP1<T extends unknown[]> = Partial<[string, ...T, number]>;  // [string?, ...Partial<T>, number?]

Fixes #5453.
Fixes #26113.

ahejlsberg added 26 commits Jun 1, 2020
# Conflicts:
#	src/compiler/checker.ts
#	tests/baselines/reference/callWithSpread3.errors.txt
#	tests/baselines/reference/genericRestParameters1.errors.txt
#	tests/baselines/reference/ramdaToolsNoInfinite.types
ahejlsberg added 2 commits Jun 19, 2020
# Conflicts:
#	tests/baselines/reference/destructuringTuple.errors.txt
@filipesabella
Copy link

filipesabella commented Jun 20, 2020

I still don't quite get why the playground link @treybrisbane posted doesn't compile.

@maxrothman
Copy link

maxrothman commented Jun 20, 2020

I know this PR is still in progress, but I happened to be playing around with type-level tuple concatenation and noticed some issues with recursive and conditional types.

type Concat<Arr extends any[][]> =   // Type alias 'Concat' circularly references itself.
  Arr extends [] ? [] :
  Arr extends [infer First, ...infer Rest] ? [...First, Concat<Rest>] :  // A rest element type must be an array type.
                                                                         // Type 'Concat' is not generic.
  never

type Concat2<Arr extends any[][]> =
  Arr extends [] ? [] :
  Arr extends [infer First extends any[], ...infer Rest] ? [...First, Concat<Rest>] :  // Exported type alias 'Concat2' has or is using private name ''.
                                                                                       // Exported type alias 'Concat2' has or is using private name ''.
                                                                                       // '?' expected.
                                                                                       // A rest element type must be an array type
                                                                                       // Type 'Concat' is not generic.
  never

Neither of these compiles. I'm not 100% confident it's not my mistake, but I figured I'd raise it just in case! Playground link

@ahejlsberg
Copy link
Member Author

ahejlsberg commented Jun 20, 2020

Regarding the various attempts to type pipe functions using variadic tuple types in conjunction with mapped and/or recursive types, it's not going to work without the introduction of new concepts in type inference. Consider:

type Fn<A, B> = (a: A) => B;

type Zip<A extends unknown[], B extends unknown[]> = {
    [K in keyof A]: Fn<A[K], B[K & keyof B]>;
};

type Head<R extends unknown[]> = R[0];
type Tail<R extends unknown[]> = R extends [any, ...infer T] ? T : [];
type Lead<R extends unknown[]> = R extends [...infer L, any] ? L : [];
type Last<R extends unknown[]> = R extends [...Lead<R>, infer L] ? L : never;

type Compose<R extends unknown[]> = Zip<Lead<R>, Tail<R>>;

declare function compose1<R extends unknown[]>(...fns: Compose<R>): Fn<Head<R>, Last<R>>;

declare function compose2<A, B, C>(...fns: Compose<[A, B, C]>): Fn<A, C>;

declare function toInt(s: string): number;
declare function double(n: number): number;

compose1(toInt, double);  // Error
compose2(toInt, double);  // Ok

The compose2 example works because we first introduce a set of type parameters for which we then perform type inference. In compose1 we have only one type parameter, R, yet we really want to perform type inference for a set of three type parameters, R[0], R[1], and R[2]. Yet it is not clear how we would infer that. I suppose we'd need some new way of indicating that we want to treat a type parameter as a tuple of individual inference slots, and some way of inferring the arity of that tuple. Definitely beyond the scope of this PR.

@treybrisbane
Copy link

treybrisbane commented Jun 21, 2020

@ahejlsberg Well that's unfortunate. Thanks for the explanation. 🙂

@jcalz
Copy link
Contributor

jcalz commented Jun 21, 2020

Usually for things like this I try to see if an F-bounded constraint will work:

type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
type Init<T extends any[]> = T extends [...infer I, any] ? I : never;
type Pipe<T extends ((x: any) => any)[]> = Init<[(x: any) => any, ...{
    [K in keyof T]: T[K] extends (x: any) => infer R ? (x: R) => any : never
}]>
declare function pipe<T extends ((x: any) => any)[] & Pipe<T>>(...f: T):
    (x: (T[0] extends (x: infer A) => any ? A : never)) =>
        Last<T> extends (x: any) => infer R ? R : never;

declare function toInt(s: string): number;
declare function double(n: number): number;

pipe(toInt, double);  // okay
pipe(double, toInt); // error in weird place due to #28505

🤷‍♂️

Playground link

@maxrothman
Copy link

maxrothman commented Jun 21, 2020

@ahejlsberg is the inference issue you mentioned related to the problem with recursive types, or is that a separate issue? Here's a more minimal example (playground link)

type Replace<Old, New, A extends any[]> =
  A extends [] ? [] :
  A extends [Old, ...infer Rest] ? [New, ...Replace<Old, New, Rest>] :
  A extends [infer T, ...infer Rest] ? [T, ...Replace<Old, New, Rest>] :
  never


type Replace2<Old, New, A extends any[]> =  // Type alias 'Replace' circularly references itself.
  A extends [] ? [] :
  A extends [Old, ...infer Rest] ? [New, Replace2<Old, New, Rest>] :    // Type 'Replace' is not generic.
  A extends [infer T, ...infer Rest] ? [T, Replace2<Old, New, Rest>] :  // Type 'Replace' is not generic.
  never

type A = Replace2<1, 'a', [3, 1, 4, 1]>  // Type is [3, ['a', [4, ['a', []]]]]

I realize this particular type could probably be implemented using a mapped type; it's just a minimal example of the case I was describing.

Copy link
Member

weswigham left a comment

Some tests explicitly concatenating/slicing/dicing labeled tuple elements would be nice, too. Also, some declaration emit tests would be warranted as well (cases where we infer variadic tuples (with labels, too) and have to write out the tuple type in the node builder, ideally).

@@ -5099,9 +5099,20 @@ namespace ts {
variances?: VarianceFlags[]; // Variance of each type parameter
}

export const enum ElementFlags {
Required = 1 << 0, // T

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member

Why are Required and Optional both flags? This means we can mistakenly mark a member as both required and optional, which seems incorrect. Actually, I'm pretty sure all of these are mutually exclusive - is there a good reason this shouldn't be ElementKind instead of ElementFlags?

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 22, 2020

Author Member

It's similar to TypeFlags and others. Makes it cheaper to check for multiple values at once.

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member

But the most we use at once is two... Is it really worth giving up exhaustiveness and exclusivity checking to replace so few expressions?

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 23, 2020

Author Member

Yes, I think this is fine, particularly as the current scheme makes it easy for us to add additional element flags that are orthogonal to the kind of element.

@@ -10514,6 +10519,9 @@ namespace ts {
return indexedAccess;
}
}
if (isGenericTupleType(type.objectType)) {
return getIndexTypeOfType(type.objectType, IndexKind.Number);

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member

How is this correct? If I have, eg, [A, B, ...C, D]["slice"], I don't think it should be constrained to the element types in any way. If the index type is string-like, I do not believe this constraint is correct.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 22, 2020

Author Member

Yeah, we should only do this when the index type is number-like.

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 23, 2020

Author Member

So, currently we error on indexed access types with variadic tuple types for any string-like key because we need to do more reasoning about constrains of variadic tuple types. That means you can't actually observe this issue. I think it is fine to leave this as is and address post-beta.

@@ -10702,6 +10710,9 @@ namespace ts {
return keyofConstraintType;
}
if (t.flags & TypeFlags.IndexedAccess) {
if (isGenericTupleType((<IndexedAccessType>t).objectType)) {
return getIndexTypeOfType((<IndexedAccessType>t).objectType, IndexKind.Number);

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member

Likewise here, a stringlike index access (eg, "slice") can fetch array apparent members, which are not a component of the index.

return isTupleType(type) ? getTypeReferenceArity(type) - findLastIndex(type.target.elementFlags, f => !!(f & ElementFlags.Variable)) - 1 : 0;
}

function getElementTypeOfTupleType(type: TupleTypeReference, index: number, endSkipCount = 0, writing = false) {

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member
Suggested change
function getElementTypeOfTupleType(type: TupleTypeReference, index: number, endSkipCount = 0, writing = false) {
function getElementTypeOfSliceOfTupleType(type: TupleTypeReference, start: number, end: number, writing = false) {

might be an easier to grok signature here. What this does (get the combiend element type for a slice of a tuple) isn't obvious from the current name, and the endSkipCount parameter feels somewhat awkward, when framed in the context of slicing (why not provide the last element index instead?).

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 22, 2020

Author Member

Sure, I can rename it. The endSkipCount simply ended up being more convenient as we're generally reasoning about elements towards the middle from the start or from the end.

@@ -170,7 +170,7 @@ for (obj?.a["b"] of []);

[...obj?.["a"]] = [];
>[...obj?.["a"]] = [] : never[]
>[...obj?.["a"]] : never[]
>[...obj?.["a"]] : any[]

This comment has been minimized.

Copy link
@weswigham

weswigham Jun 22, 2020

Member

There a reason for this change?

This comment has been minimized.

Copy link
@ahejlsberg

ahejlsberg Jun 22, 2020

Author Member

It's an effect of simplifications I made to checkArrayLiteral. Given that obj is of type any, the type of [...obj?.["a"]] certainly should be any[]. Not sure how the old (and quite convoluted) logic got to never[].

Copy link
Member

DanielRosenwasser left a comment

It looks like a few team members and the community have given it a stress test. While it sounds like there's some room for improvement, from what I understand it can be addressed and discussed further after the beta. Ship it! 🚀

@ahejlsberg ahejlsberg merged commit d479206 into master Jun 23, 2020
7 of 8 checks passed
7 of 8 checks passed
build (10.x)
Details
build (12.x)
Details
build (13.x)
Details
continuous-integration/travis-ci/pr The Travis CI build is in progress
Details
license/cla All CLA requirements met.
Details
node10 Build #77451 succeeded
Details
node12 Build #77449 succeeded
Details
node13 Build #77450 succeeded
Details
cangSDARM added a commit to cangSDARM/TypeScript that referenced this pull request Jun 23, 2020
* upstream/master: (58 commits)
  Variadic tuple types (microsoft#39094)
  chore: resolve suggestions
  Expand auto-import to all package.json dependencies (microsoft#38923)
  inline local functions
  Update bigint declaration file (microsoft#38526)
  Update user baselines (microsoft#39077)
  LEGO: check in for master to temporary branch.
  Add missing index.ts files to user projects (microsoft#39163)
  Add reason for a disabled code action (microsoft#37871)
  Minor fix for assertion predicates (microsoft#38710)
  Update LKG (microsoft#39173)
  Reparse top level 'await' in modules (microsoft#39084)
  change
  chore: more change
  chore: resolve review
  chore: save space
  fix: lint error
  test: add test for it
  chore: make isJsxAttr required
  chore: revert change in checker
  ...

# Conflicts:
#	src/compiler/binder.ts
#	src/compiler/checker.ts
#	src/compiler/parser.ts
#	src/compiler/types.ts
@DanielRosenwasser DanielRosenwasser deleted the variadicTuples branch Jun 23, 2020
Jack-Works added a commit to Jack-Works/TypeScript that referenced this pull request Jun 24, 2020
* Initial implementation of variadic tuple types

* Accept new baselines

* Handle variadic elements in tuple type inference

* Special case inference between tuples with matching structure

* Restore check that rest element is last element

* Handle variadic tuples in relationship checking

* Accept new baselines

* Infer readonly constraints when inferring from readonly tuples

* Fix lint issues

* T assignable to readonly [...T] and [...T] assignable to T

* Consistent tuple normalization

* Create variadic tuple types from array literal expressions

* Accept new baselines

* Array literals have tuple types when contextual type is readonly

* Accept new baselines

* Optional elements before required elements become required elements

* Update logic for rest parameters and spread arguments

* Revert special case of contextual readonly array type

* Accept new baselines

* Fix lint issue

* Switch entirely to createTupleType based on element flags

* Don't infer readonly tuple types when inferring to variadic elements

* Handle mapped types applied to generic tuple types

* Handle constraint of indexed access type with generic tuple type

* Accept new baselines

* Address CR feedback

* Simplify indexed access types involving generic tuple types

* Propagate checkMode into getSpreadArgumentType

* Guard against missing globalArrayType

* Inference to [...T, ...U] based on implied arity of T

* Accept new baselines

* Add tests

* Emit .d.ts from tests

* Address CR feedback
@w0rp
Copy link

w0rp commented Jun 26, 2020

This is out of the scope for this pull request, but I think it's the next most obvious thing to ask about for something to come next. Is there an open issue for supporting some way to compute the intersection of all types that are elements of a variadic type tuple, like T[0] & T[1] & ...? One use would be to declare Object.assign for any number of arguments.

What's interesting is that while I couldn't find a way to compute the intersection, computing the union of all of the elements was easy. I was able to make this pointless and fun function to return a random element.

const pickOne = <T extends unknown[]>(...args: [...T]): T[number] => {
    return args[Math.floor(Math.random() * args.length)]
}

console.log(pickOne({y: 3}, 4, ['foo'])
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jun 26, 2020

@w0rp I think you can use T[number], then pass that through something to convert a union type to an intersection type.

@TylorS
Copy link

TylorS commented Jun 26, 2020

@w0rp There's some small differences between that union to intersection than being able to use & directly that are probably worth noting, but may be fine in some cases. Essentially, UnionToIntersection<A | B> does not always equal A & B - https://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBDAnmYcCqAmAkgHjQPjgF4BYAKDjgAo05QZgA7AEwGc4BDRxOAfmoDWALnQBKYoQBuEAJbM4IxsEnAo4+kzbUqwuDMYAzVXCziiU2c3H8sCuEpVRy5JCjgAVAIzF02HAFdGAUYIAHdGOAAfOABvOAMICEV-AFsAI2MAX3wXZFR3DB9A4LCIgDJY+MTk9KygA

I've come up with an alternative using recursive conditional types which gives you more control over the transformation, by being able to customize then ToConsList type to work with any specific types you may be using. I've been using this to aggregate dependencies needed to perform a set of effects. I hope it helps 😄

@w0rp
Copy link

w0rp commented Jul 1, 2020

@TylorS Thank you. That is pure genius. That's a pretty perfect solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

You can’t perform that action at this time.