Skip to content

Generic wrapper interface no longer assignable  #31804

Closed

Description

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.

Activity

  1. weswigham commented on Jun 26, 2019

    @weswigham
    Member

    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.

  2. added
    BugA bug in TypeScript
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Jun 26, 2019
  3. ChiriVulpes commented on Jun 29, 2019

    @ChiriVulpes
    ContributorAuthor

    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... ¯\_(ツ)_/¯

  4. ChiriVulpes commented on Jul 2, 2019

    @ChiriVulpes
    ContributorAuthor

    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
  5. weswigham commented on Jul 2, 2019

    @weswigham
    Member

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

  6. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptDomain: Conditional TypesThe issue relates to conditional typesFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions