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

Lossy argument names when spreading across parameter types #48049

Closed
mindplay-dk opened this issue Feb 27, 2022 · 7 comments
Closed

Lossy argument names when spreading across parameter types #48049

mindplay-dk opened this issue Feb 27, 2022 · 7 comments
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Milestone

Comments

@mindplay-dk
Copy link

Bug Report

πŸ”Ž Search Terms

argument names spread, parameter names spread

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type Equality = (a: number, b: number) => boolean;

type GOOD = (...args: [...Parameters<Equality>]) => void;

type BAD = (...args: [...Parameters<Equality>, string]) => void;

πŸ™ Actual behavior

As shown here, the argument names are lost in if the argument list expression involves any arguments without names:

image

πŸ™‚ Expected behavior

It should infer the type of BAD as (a: number, b: number, args_2: string) => void.

πŸ€” Workaround

Declaring an extra function type with the additional arguments seems to work:

type Equality = (a: number, b: number) => boolean;

type Additional = (lol: string) => void; // πŸ‘ˆ

type GOOD = (...args: [...Parameters<Equality>]) => void;

type BAD = (...args: [...Parameters<Equality>, ...Parameters<Additional>]) => void;

Inlining a function type seems to work too:

type Equality = (a: number, b: number) => boolean;

type GOOD = (...args: [...Parameters<Equality>]) => void;

type BAD = (...args: [...Parameters<Equality>, ...Parameters<(lol: string) => void>]) => void;

So there are known workarounds, although I would say these are non-obvious - it took me a long time to figure this out, and it's just surprising how the second part of the parameter type expression seems to determine the outcome of the first part.

@mindplay-dk
Copy link
Author

Actually, this workaround didn't work for my case either.

Here's the same workaround wrapped in a WithMoreParams type expression:

type Equality = (a: number, b: number) => boolean;

type WithMoreParams<F extends (...args: any) => any> = (...args: [...Parameters<F>, ...Parameters<(lol: string) => void>]) => void;

type BAD = WithMoreParams<Equality>;

As soon as you're doing it from a function declaration or another type expression, it loses the names again.

Playground link

@mindplay-dk
Copy link
Author

If you're interested in the real-world use-case, here it is:

export type Fact = {
  pass: boolean;
  actual: unknown;
  details: unknown[];
}

type Details = (...details: unknown[]) => void;

export function assertion<F extends (...args: any[]) => boolean>(assert: F)
  : (...args: [...Parameters<F>, ...Parameters<Details>]) => Fact
{
  return (...args: [...Parameters<F>, ...Parameters<Details>]) => ({
    pass: assert(...args.slice(assert.length) as any),
    actual: args[0],
    details: args.slice(1),
  });
}

const isEqual = assertion((a:number, b:number) => a === b);

const fact = isEqual(1, 2, "because reasons");

(it's a factory function in a test-framework - it takes a simple (...args: any) => boolean assertion function, and converts it into a function that accepts additional details and produces a Fact - while it produces a working result, the parameter names are crucial to the understanding of it, and this is essentially useless without the argument names.)

(on that note, being able to infer documentation would be extremely useful in this case as well.)

@IllusionMH
Copy link
Contributor

Tuple either all have labeled elements or none. In your example none are.
See what #39941 (comment) for details.

Much simpler and clear way

type BAD = (...args: [...factoryParams: Parameters<Equality>, detals: string]) => void;

@mindplay-dk
Copy link
Author

I had no idea you could name tuple members! (is that a documented feature? I've never seen this before.)

Using this approach does seem to work:

type Equality = (a: number, b: number) => boolean;

type WithMoreParams<F extends (...args: any) => any> = (...args: [...params: Parameters<F>, ...details: unknown[]]) => void;

type BAD = WithMoreParams<Equality>;

Curiously though, the compiler demands that I name params here - even though the name is unused and discarded.

Apparently, tuple members can only have names if the parent tuple has a name?

But yes, this approach works for my use case as well:

export type Fact = {
  pass: boolean;
  actual: unknown;
  details: unknown[];
}

type Details = (...details: unknown[]) => void;

export function assertion<F extends (...args: any[]) => boolean>(assert: F)
  : (...args: [...params: Parameters<F>, ...details: unknown[]]) => Fact // πŸ‘ˆ fixed!
{
  return (...args: [...Parameters<F>, ...Parameters<Details>]) => ({
    pass: assert(...args.slice(assert.length) as any),
    actual: args[0],
    details: args.slice(1),
  });
}

const isEqual = assertion((a:number, b:number) => a === b);

const fact = isEqual(1, 2, "because reasons");

Thanks @IllusionMH !

I do think there is room for some improvements here.

Maybe the error messages could be more helpful?

It's definitely not clear to me why the obvious solution in my original post only works until you wrap it in a type for reuse. Something seems inconsistent here? If there is some practical reason why this doesn't / can't / shouldn't work, perhaps the 5048 error message ("Tuple members must all have names or all not have names") should appear when this is attempted?

@jcalz
Copy link
Contributor

jcalz commented Feb 28, 2022

I had no idea you could name tuple members! (is that a documented feature? I've never seen this before.)

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements

@RyanCavanaugh RyanCavanaugh added Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript labels Feb 28, 2022
@RyanCavanaugh
Copy link
Member

The short answer is that the parameter -> tuple name feature is "best effort" (since nothing in the type system works/doesn't work depending on these names), and the effort to handle this particular case simply hasn't been done yet

@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 28, 2022
@RyanCavanaugh RyanCavanaugh added the Help Wanted You can do this label Feb 28, 2022
darrachequesne added a commit to socketio/socket.io-client that referenced this issue Jan 17, 2023
This follows [1], in order to keep the label of each argument.

[1]: 33e4172

Related:

- #1570 (comment)
- microsoft/TypeScript#39941
- microsoft/TypeScript#48049
@Vanilagy
Copy link

The original reproduction

type Equality = (a: number, b: number) => boolean;

type GOOD = (...args: [...Parameters<Equality>]) => void;

type BAD = (...args: [...Parameters<Equality>, string]) => void;

is fixed now:

CleanShot 2023-10-19 at 09 31 53

I say this issue can be closed, then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experience Enhancement Noncontroversial enhancements Help Wanted You can do this Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants