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

TypeScript v4.5 regression: cannot use Parameters type on narrowed methods / functions #46855

Closed
alecgibson opened this issue Nov 18, 2021 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@alecgibson
Copy link

Bug Report

πŸ”Ž Search Terms

function, method, generic, parameters, does not satisfy constraint, 4.5

πŸ•— Version & Regression Information

  • This changed between versions 4.4.4 and 4.5.0

⏯ Playground Link

Playground link with relevant code

☝🏼 Switch the version to v4.4.4 and this compiles fine; v4.5.0-beta errors (as does v4.5.2, but the Playground doesn't offer this version)

πŸ’» Code

type Methods<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

class Test {
  foo = '';
  bar(a: string, b: number): void {}
}

function doSomeProxying<
  T,
  Method extends Methods<T>,
  Args extends Parameters<T[Method]>,
>(
  type: T,
  method: Method,
  ...args: Args
): void {}

doSomeProxying(new Test(), 'bar', 'arg', 123);

πŸ™ Actual behavior

Compiler errors in v4.5.0, even though it was fine in v4.4.4.

πŸ™‚ Expected behavior

Expect to be able to use the built-in Parameters<> type on properties I have narrowed down as functions.

@milesj
Copy link

milesj commented Nov 18, 2021

Also ran into this with Parameters, and also ReturnType.

@RyanCavanaugh
Copy link
Member

Bisected to #41821

@RyanCavanaugh
Copy link
Member

So, critically, this code never worked for the reasons it appears to work. This passes without error in 4.4:

type Methods<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
  //     Changed Function to string ^
}[keyof T];

class Test {
  foo = '';
  bar(a: string, b: number): void {}
}

function doSomeProxying<T, Method extends Methods<T>, Args extends Parameters<T[Method]>>(
  type: T,
  method: Method,
  ...args: Args
): void {}

Note that we're taking Parameters<T[Method]> where T[Method] was filtered to be a string-valued property of T.

The type system does not actually recognize Methods<T> as doing the operation "Give me back a type that only has function-valued property keys"; it's a deferred filtering that doesn't induce any higher-order effects. Due to some poor tracking in 4.4 and prior, when evaluating Parameters<T[Method]> we got "too deep" when computing whether the operation would work and produced our internal "Maybe" result, which rounds up to "OK" in this particular case and allows Parameters<T[Method]> even though we couldn't deduce that this was necessarily safe.

There are some things you can write that make this work as expected; they're hopefully at least someone self-explanatory so I'll post them all and you can pick based on what works best for you. Note that the Args type parameter here is unnecessary and actually actively harmful, so I've removed it in some of them.

type Methods<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

class Test {
  foo = '';
  bar(a: string, b: number): void {}
}

type LooseParameters<T> = T extends (...args: infer A) => unknown ? A : never;
function doSomeProxying<T, K extends Methods<T>>(
  type: T,
  method: K,
  ...args: LooseParameters<T[K]>
): void {}
class Test {
  foo = '';
  bar(a: string, b: number): void {}
}

function doSomeProxying<T extends Record<K, (...args: any[]) => unknown>, K extends PropertyKey>(
  type: T,
  method: K,
  ...args: Parameters<T[K]>
): void {}

// @ts-expect-error
doSomeProxying(new Test(), 'foo');
// @ts-expect-error
doSomeProxying(new Test(), 'qua');
doSomeProxying(new Test(), 'bar', "hello", 42);
// @ts-expect-error
doSomeProxying(new Test(), 'bar', 42, 42);
type Methods<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

class Test {
  foo = '';
  bar(a: string, b: number): void {}
}

type Cast<T, U> = T extends U ? T : T & U;
function doSomeProxying<
  T,
  Method extends Methods<T>,
  Args extends Parameters<Cast<T[Method], (...args: any[]) => void>>,
>(
  type: T,
  method: Method,
  ...args: Args
): void {}

// @ts-expect-error
doSomeProxying(new Test(), 'foo');
// @ts-expect-error
doSomeProxying(new Test(), 'qua');
doSomeProxying(new Test(), 'bar', "hello", 42);
// @ts-expect-error
doSomeProxying(new Test(), 'bar', 42, 42);

Long-term there's still a sort of missing feature where you can refer to the keys of an object whose corresponding property types match some other type (which would come with associated assignability effects in generics etc), but in practice this is very rarely strictly needed, as shown by the alternatives above.

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Nov 19, 2021
@RyanCavanaugh RyanCavanaugh removed their assignment Nov 19, 2021
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 4.6.0 milestone Nov 19, 2021
@alecgibson
Copy link
Author

Thanks for the rapid response and clear explanation!

susisu added a commit to susisu/deform that referenced this issue Feb 27, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants