Skip to content

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

Description

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions