Skip to content

Inference does not extend to generics of types, causing widened covariant types, and errors for contravariant types #44328

Description

@erikbrinkman

Bug Report

When a generic type is constrained by another generic type, the inference doesn't extend to the second generic type unless it is explicitly included in the function signature. In short, <T, U extends I<T>>(_: U) will always infer unknown for T. I couldn't find any issues referencing this, but the behavior has to be known about. For most types, e.g. covariant ones, this isn't strictly incorrect, it just produces a wider type than a user might expect. However, for contravariant types, this will actually caused a rejection of valid code. Both instances can be "fixed" by manually specifying both types in the signature, e.g. <T, U extends I<T>>(_: U & I<T>) but this is clunky and may not be obvious to some people (i.e. me).

I could believe that this is also a design limitation, but given the semantics of extends, I would think it's possible to "insert" these extra intersections whenever a type is constrained by a generic. Most times it will be meaningless, but in this instance it will allow inference to pick up the appropriate type.

🔎 Search Terms

inference, bound, constraint

🕗 Version & Regression Information

  • This is the behavior in every version I tried (3.3, 4.2, beta, nightly), and I reviewed the FAQ for entries about ________ (I'm not sure what should go in this blank, but I did not find any references in the FAQ)

⏯ Playground Link

Playground link with relevant code

💻 Code

Admittedly, but examples are a little contrived, but I have run into them in actual code, where I need references to both types.

Covariant example

interface Covariant<T> {
    val: T
}

declare function covariantSimple<T, C extends Covariant<T>>(inp: C): { inp: C, inner: T };
covariantSimple({ val: { foo: 5 } }).inner.foo;
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ object is unknown
// but object should / would normally be inferred to be of type { foo: number }

// manually specifying the intersection in the type tells typescript how to do the inference
declare function covariantIntersection<T, C extends Covariant<T>>(inp: C & Covariant<T>): { inp: C, inner: T };
covariantIntersection({ val: { foo: 5 } }).inner.foo;  // no error

Contravariant example

interface Contravariant<T> {
    (val: T): boolean;
}

declare function contravariantSimple<T, C extends Contravariant<T>>(inp: C): C;
contravariantSimple(({ val }: { val: boolean }) => val);
//                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Argument of type '...' is not assignable to parameter of type 'Contravariant<unknown>'
// same error, but since the type is contravariant, failure to infer a narrower type for T causes a type error

// like before, manually specifying the intersection allows typescript to infer properly
declare function contravariantIntersection<T, C extends Contravariant<T>>(inp: C & Contravariant<T>): C;
contravariantIntersection(({ val }: { val: boolean }) => val); // no error

🙁 Actual behavior

When a generic type is constrained by another generic type, the inference doesn't extend to the second generic type unless it is explicitly included in the signature.

🙂 Expected behavior

The inference magically works!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions