Skip to content

Tuple & nominal not recognised as tuple in spread parameter #45371

Open

Description

@Dionysusnu

Bug Report

🔎 Search Terms

Nominal, Spread, Tuple,

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about spread and intersections

⏯ Playground Link

Playground link with relevant code

💻 Code

declare let a: [number, number, number] & { _nominal_a: never }
function b(...params: [number, number, number]) {}
b(...a) // Error: A spread argument must either have a tuple type or be passed to a rest parameter.

🙁 Actual behavior

The error in the code example is thrown, because TypeScript does not detect it as being a tuple.
In roblox-ts, we use a nominal marker type to indicate a special kind of tuple. This bug means that this tuple type can then not be spreaded into a fixed argument function. Our issue: roblox-ts/roblox-ts#1537

🙂 Expected behavior

This is a tuple type, and should therefore be recognised as such.

Activity

  1. S0AndS0 commented on Dec 31, 2021

    @S0AndS0

    I recently ran into this error too, and worked around it by hinting the type via the as keyword, eg.

    function mergeDictionaries(...dictionaries: { [key: string]: unknown }[]): {
      [key: string]: unknown;
    } {
      return Object.assign(
        ...(dictionaries.map((dictionary) => {
          return JSON.parse(JSON.stringify(dictionary));
        }) as [{ [key: string]: unknown }])
      );
    }

    ... without the [{ [key: string]: unknown }]) bit, an error similar to the following pops;

    <file>.ts|<C> col <L> error| A spread argument must either have a tuple type or be passed to a rest parameter.
    

    What I find somewhat trubling, beyond TypeScript loosing the plot in the first place, is that Array<T> will produce the same error, eg.

    function mergeDictionaries(...dictionaries: { [key: string]: unknown }[]): {
      [key: string]: unknown;
    } {
      return Object.assign(
        ...(dictionaries.map((dictionary) => { /* ← Error on this line */
          return JSON.parse(JSON.stringify(dictionary));
        }) as Array<{ [key: string]: unknown }>) /* ← Why is [{}] required? */
      );
    }
  2. Dionysusnu commented on Dec 31, 2021

    @Dionysusnu
    Author

    What I find somewhat trubling, beyond TypeScript loosing the plot in the first place, is that Array<T> will produce the same error, eg.

    This usually makes sense, because you don't know the Array's length, which is why it's (as the correct behaviour) not considered a tuple.
    That's part 1, and then part 2, I believe this not working in this case is a separate missing behaviour, which is that normal parameters need to be filled manually, not being considered to be part of the rest parameter. What I mean by that is that a function like a(obj: object, ...other: object[]) will not allow a call like a(...objects) where objects is Array<object>, despite this being type-compatible.

  3. S0AndS0 commented on Jan 1, 2022

    @S0AndS0

    Hmm, I supose I'll have to dig deeper into how TypeScript parses things. Because I'm still a bit befuddled that as Array<{ [key: string]: unknown }> throws errors but using the same hint for a parameter doesn't present issues, eg.

    function mergeDictionaries(
      ...dictionaries: Array<{ [key: string]: unknown }>  /* ← Totally okay */
      // ...dictionaries: [{ [key: string]: unknown }]    /* ← this is fine too */
    ): {
      [key: string]: unknown;
    } {
      return Object.assign(
        ...(dictionaries.map((dictionary) => {
          return JSON.parse(JSON.stringify(dictionary));
        }) as [{ [key: string]: unknown }])
        // }) as { [key: string]: unknown }[])       /* ← Nope */
        // }) as Array<{ [key: string]: unknown }>)  /* ← no joy either */
      );
    }
  4. darklight9811 commented on Jan 12, 2022

    @darklight9811

    I'm mapping a dispatch into a object and this is freaking my mind.

    // not actual example but close
    Object.fromEntries(
      Object.entries(actions)
        .map(entry => [
          entry[0],
          (...args: Parameters<typeof entry[1]>) => dispatch(entry[1](...args))
        ])
    )

    There is no reason for that be a problem, mainly because not allowing arrays as spread arguments should be a linter problem, not a compiler problem.

  5. Dionysusnu commented on Jan 13, 2022

    @Dionysusnu
    Author

    Made a Playground Link for your code example, and I'm failing to spot any issue in this. In any case, it's probably similar to the above comment by S0AndS0, and as explained not a related problem.

  6. scottwillmoore commented on Apr 23, 2022

    @scottwillmoore

    I am not sure if this is related, or supposed to occur... However, when attempting to brand or flavor a tuple it appears that TypeScript is no longer able to restrict property access past the length of the tuple.

    Example

    declare const id: unique symbol;
    type Id<T> = { readonly [id]: T };
    
    type Brand<T, U> = T & Id<U>;
    type PartialBrand<T, U> = T & Partial<Id<U>>; // Also known as Flavor.
    
    declare const pointId: unique symbol;
    type Point = PartialBrand<[number, number], typeof pointId>;
    
    const point = (x: number = 0, y: number = 0): Point => [x, y];
    
    const add = (a: Point, b: Point, c: Point = point()): Point => {
      c[0] = a[0] + b[0];
      c[1] = a[1] + b[1];
      return c;
    };
    
    const a = point(0, 1);
    const b = point(2, 3);
    const c = add(a, b);
    
    console.log(c);
    
    c[0] = 4;
    c[1] = 5;
    c[2] = 6; // Should be an error!
    
    console.log(c);

    Playground Link

    It appears that the expansion of the type includes [x: number]: number. Maybe I should open this a separate issue?

    type DeepExpand<T> = T extends object ? T extends infer O ? { [K in keyof O]: DeepExpand<O[K]> } : never : T;
    type ExpandPoint = DeepExpand<Point>;
    
    // type ExpandPoint = {
    //   [x: number]: number;
    //   ...
    // }
  7. TomRaaff commented on Aug 21, 2023

    @TomRaaff

    Here is another instance of (probably) the same error:
    Playground link

    A TS2556 (A spread argument must either have a tuple type or be passed to a rest parameter.) error is thrown whenever I try to spread the contents of a tuple into another function. Even though that tuple is inferred as the correct type.

    I have tested this on all versions available in the playground.

  8. RA80533 commented on Feb 21, 2024

    @RA80533

    It appears that the expansion of the type includes [x: number]: number. Maybe I should open this a separate issue?

    Scott Moore (@scottwillmoore) Good observation. Once the index signature is removed, the tuple behaves as expected.

    // https://stackoverflow.com/a/66252656
    type RemoveIndex<T> = {
      [ K in keyof T as
        string extends K
        ? never
        : number extends K
        ? never
        : symbol extends K
        ? never
        : K
      ] : T[K];
    };
    
    // -snip-
    type PartialBrand<T, U> = RemoveIndex<T> & Partial<Id<U>>; // Also known as Flavor.
    //                        ^^^^^^^^^^^

    Playground link

    // -snip-
    type Point = PartialBrand<RemoveIndex<[number, number]>, typeof pointId>;
    //                        ^^^^^^^^^^^

    Playground link

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

    BugA bug in TypeScriptDomain: IntersectionIncorrect behavior under `T & U` constructs

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions