Skip to content

Possible regression in overloads with optional this types in callbacks #53080

Closed
@chriskrycho

Description

@chriskrycho

Bug Report

I believe #52838 #52387 is the cause of a “regression” in Ember’s type test suite, but I also am very willing to believe that this is a bug in how Ember’s types are written which this change caught, but I wanted to flag it up.

Here's the type test (source):

expectTypeOf(
  bind(foo, function (_foo: number, _bar: boolean, _baz?: string): number {
    expectTypeOf(this).toEqualTypeOf<Foo>();
    return 1;
  })
).toEqualTypeOf<(foo: number, bar: boolean, baz?: string) => number | void>();

Here's the set of overloads, including the implementation overload (source):

export function bind<
  T,
  F extends (this: T, ...args: any[]) => any,
  A extends PartialParams<Parameters<F>>
>(
  target: T,
  method: F,
  ...args: A
): (...args: RemainingParams<A, Parameters<F>>) => ReturnType<F> | void;
export function bind<F extends AnyFn, A extends PartialParams<Parameters<F>>>(
  method: F,
  ...args: A
): (...args: RemainingParams<A, Parameters<F>>) => ReturnType<F> | void;
export function bind<
  T,
  U extends keyof T,
  A extends T[U] extends AnyFn ? PartialParams<Parameters<T[U]>> : []
>(
  target: T,
  method: U,
  ...args: A
): T[U] extends AnyFn
  ? (...args: RemainingParams<A, Parameters<T[U]>>) => ReturnType<T[U]> | void
  : never;
export function bind(...curried: any[]): any {
  // ...
}

Prior to this change, this type checked. After this change, it reports:

No overload matches this call.
  Argument of type '[]' is not assignable to parameter of type 'never'.
  Overload 2 of 3, '(method: AnyFn, ...args: never): (...args: never) => any', gave the following error.
    Argument of type 'Foo' is not assignable to parameter of type 'AnyFn'.
  Overload 3 of 3, '(target: Foo, method: "test"): (_foo: number, _bar: boolean, _baz?: string | undefined) => number | void', gave the following error.
    Argument of type '(this: Foo, _foo: number, _bar: boolean, _baz?: string | undefined) => number' is not assignable to parameter of type '"test"'.ts(2769)
index.ts(295, 17): The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.

Prior to this change, it was resolving against the first overload:

export function bind<
  T,
  F extends (this: T, ...args: any[]) => any,
  A extends PartialParams<Parameters<F>>
>(
  target: T,
  method: F,
  ...args: A
): (...args: RemainingParams<A, Parameters<F>>) => ReturnType<F> | void;

I have tried adding an overload which has the same form but does not explicitly include the this type on the F constraint, but at that point I'm in overload-ordering hell.

🔎 Search Terms

  • overloads
  • this type

🕗 Version & Regression Information

This changed between versions 5.1.0-dev.20230301 and 5.1.0-dev.20230302.

⏯ Playground Link

Playground link with relevant code

💻 Code

import { expectTypeOf } from 'expect-type';

// ----- TEST CODE ----- //
class Foo {
  literallyAnythingToAvoidAnEmptyClass() {}
}

let foo = new Foo();

expectTypeOf(
  bind(foo, function (_foo: number, _bar: boolean, _baz?: string): number {
    expectTypeOf(this).toEqualTypeOf<Foo>();
    return 1;
  })
).toEqualTypeOf<(foo: number, bar: boolean, baz?: string) => number | void>();

// ----- OVERLOAD DEFINITIONS ----- //

export function bind<
  T,
  F extends (this: T, ...args: any[]) => any,
  A extends PartialParams<Parameters<F>>
>(
  target: T,
  method: F,
  ...args: A
): (...args: RemainingParams<A, Parameters<F>>) => ReturnType<F> | void;
export function bind<F extends AnyFn, A extends PartialParams<Parameters<F>>>(
  method: F,
  ...args: A
): (...args: RemainingParams<A, Parameters<F>>) => ReturnType<F> | void;
export function bind<
  T,
  U extends keyof T,
  A extends T[U] extends AnyFn ? PartialParams<Parameters<T[U]>> : []
>(
  target: T,
  method: U,
  ...args: A
): T[U] extends AnyFn
  ? (...args: RemainingParams<A, Parameters<T[U]>>) => ReturnType<T[U]> | void
  : never;
// overload implementation
export function bind(...curried: any[]): any {
  // ...
}


// ----- TYPE UTILITIES ----- //

export type AnyFn = (...args: any[]) => any;

type PartialParams<P extends any[]> = P extends [infer First, ...infer Rest]
  ? [] | [First] | [First, ...PartialParams<Rest>]
  : // This is necessary to handle optional tuple values
  Required<P> extends [infer First, ...infer Rest]
  ? [] | [First | undefined] | [First | undefined, ...PartialParams<Partial<Rest>>]
  : never;

type RemainingParams<PartialParams extends any[], All extends any[]> = PartialParams extends [
  infer First,
  ...infer Rest
]
  ? All extends [infer AllFirst, ...infer AllRest]
    ? First extends AllFirst
      ? RemainingParams<Rest, AllRest>
      : never
    : // This is necessary to handle optional tuple values
    Required<All> extends [infer AllFirst, ...infer AllRest]
    ? First extends AllFirst | undefined
      ? Partial<RemainingParams<Rest, AllRest>>
      : never
    : never
  : PartialParams extends []
  ? All
  : never;

🙁 Actual behavior

Wellllll the overload isn't working anymore.

🙂 Expected behavior

I think I expected this overload to keep working.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions