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

Generic wrapper interface no longer assignable #31804

Closed
ChiriVulpes opened this issue Jun 6, 2019 · 4 comments · Fixed by #32116
Closed

Generic wrapper interface no longer assignable #31804

ChiriVulpes opened this issue Jun 6, 2019 · 4 comments · Fixed by #32116
Assignees
Labels
Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types Fix Available A PR has been opened for this issue

Comments

@ChiriVulpes
Copy link

TypeScript Version: 3.6.0-dev.20190606

Search Terms:
I didn't know how to search for this issue since it's a bit complex

Code

type AnyFunction = (...args: any[]) => any;
type Params<T> = Parameters<Extract<T, AnyFunction>>;

interface Wrapper<T> {
	call<K extends keyof T>(event: K, ...args: Params<T[K]>): void;
}

interface AWrapped {
	foo(): void;
}

class A {
	foo: Wrapper<AWrapped>;
}

interface BWrapped extends AWrapped {
	bar(): void;
}

class B extends A {
	foo: Wrapper<BWrapped>;
}

Expected behavior:
No errors, as it worked in pre 3.5.0

Actual behavior:

Property 'foo' in type 'B' is not assignable to the same property in base type 'A'.
Type 'Wrapper' is not assignable to type 'Wrapper'.
Property 'bar' is missing in type 'AWrapped' but required in type 'BWrapped'

Why? What was this used for?
Our project uses a setup like this for strongly typed events; we define the events in interfaces, and then classes that emit the events store the generic event emitter instances on them. (The actual implementation is a lot more complex, this is a stripped down version)

Previously, it worked to simply override the property's type, but now that doesn't work anymore, and I can't find a workaround. I think this is might prevent our team from updating because of how much this breaks, at least for a time... The amount that has to be rewritten to undo the dependency on this functionality is not negligible.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jun 25, 2019
@weswigham
Copy link
Member

weswigham commented Jun 26, 2019

The T in Wrapper<T> is now being measured as invariant. Parameters are contravariant positions, but there are concrete instances where two types do not contravary, but the arguments of their methods (which is what we're actually looking at here) do. Unfortunately, typically the arguments of the methods contravary so long as the containing types covary (meaning variance inversion brought on by the infer type within the Parameters helper is lost). The comparison we trip up on is Parameters<Extract<?[any], AnyFunction>> to Parameters<Extract<?[any], AnyFunction>> (the any's show up due to erasure during signature comparison, the ? marks are two different but known related types). Parameters is actually measured as bivariant, but then we simplify Extract<?[any], AnyFunction> to a substitution type, neglect to unwrap it (there's the bug!), and consequently report the types as not assignable, as substitutions on their own have no assignability rules (they are meant to be substituted).

As a workaround (until we take a fix), if you specify Params as:

type Params<T> = Parameters<Extract<Required<T>, AnyFunction>>;

the use of Required will bypass variance-based typechecking (as the Required transform is none of co- nor contra- nor in- nor bi- variant) on the containing types, allowing the assignment to succeed when supplied with correct concrete instances, as is the case here.

@weswigham weswigham added Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types and removed Needs Investigation This issue needs a team member to investigate its status. labels Jun 26, 2019
@ChiriVulpes
Copy link
Author

It seems like defining Params as you suggest doesn't actually work for this purpose, but I'm not sure why.

Simplified example (got rid of the inheritance stuff from B temporarily, better names)

type AnyFunction = (...args: any[]) => any;
type Params<T> = Parameters<Extract<T, AnyFunction>>;

interface Caller<T> {
	call<K extends keyof T>(event: K, ...args: Params<T[K]>): void;
}

interface ACallable {
	foo(thing1: boolean, thing2: number): void;
}

class A {
	caller: Caller<ACallable>;
}

new A().caller.call("foo", true, 1); // works (good)
new A().caller.call("foo"); // errors (good)

Using your suggested change with Required<T>:

type AnyFunction = (...args: any[]) => any;
type Params<T> = Parameters<Extract<Required<T>, AnyFunction>>;

interface Caller<T> {
	call<K extends keyof T>(event: K, ...args: Params<T[K]>): void;
}

interface ACallable {
	foo(thing1: boolean, thing2: number): void;
	bar(): void;
}

class A {
	caller: Caller<ACallable>;
}

new A().caller.call("foo", true, 1); // errors, [boolean, number] is not assignable to never
new A().caller.call("bar"); // errors, [] is not assignable to never

It does make the inheritance work, but then the thing we're trying to inherit and extend doesn't work anymore, so... ¯\_(ツ)_/¯

@ChiriVulpes
Copy link
Author

ChiriVulpes commented Jul 2, 2019

It seems like Required<any function type> just always eats the function type, removing any callable signatures. As a result the arguments can no longer be extracted from it.

I did find an actual workaround tho, and that's using Required on the extracted parameters instead:

Edit... Required isn't good enough because it makes optional parameters required, Partial isn't good enough either for the same reason (makes all required params optional).

However, I found the One True Workaround™, and that's just doing the exact same thing that Partial and Required do, but making no changes. It's time to introduce the new type LiterallyJustTheSameThing:

type AnyFunction = (...args: any[]) => any;
type LiterallyJustTheSameThing<T> = { [K in keyof T]: T[K] };
type Params<T> = Extract<LiterallyJustTheSameThing<Parameters<Extract<T, AnyFunction>>>, any[]>;

interface Caller<T> {
	call<K extends keyof T> (event: K, ...args: Params<T[K]>): void;
}

interface ACallable {
	foo (thing1: boolean, thing2?: number): void;
}

class A {
	caller: Caller<ACallable>;
}

interface BCallable extends ACallable {
	bar (): void;
}

class B extends A {
	caller: Caller<BCallable>;
}

new A().caller.call("foo", true, 1); // works
new B().caller.call("foo", true); // works
new B().caller.call("bar"); // works

@weswigham
Copy link
Member

Hopefully you won't need a workaround for long, since #32116 fixes the root cause of the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Conditional Types The issue relates to conditional types Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants