Skip to content

Proposal: Expand inferFromTypeArguments() candidates to include heritage elementsΒ #60041

@Renegade334

Description

@Renegade334

πŸ” Search Terms

infer heritage extends

βœ… Viability Checklist

⭐ Suggestion

The compiler's stepwise approach to type inference includes a check for the case where it's inferring type parameters, and the inference source and inference target reference the same type. If so, a call to inferFromTypeArguments() grabs the types directly from the inference target. A silly example:

interface Nothing<T> {}
declare function inferFromNothing<T>(arg: Nothing<T>): T;

declare const n: Nothing<boolean>;
inferFromNothing(n); // returns `boolean`

Even though there's absolutely zilch in the Nothing interface from which to infer T, the compiler identifies that the type of e and the type of arg are both references to the generic type Nothing, and infers the type parameter directly via inferFromTypeArguments().

The same does not apply to heritage types, though. Following on from the same example:

interface AlsoNothing<T> extends Nothing<T> {}

declare const a: AlsoNothing<boolean>;
inferFromNothing(a); // returns `unknown`

The type of a and the type of arg are no longer TypeReferences to the same type, so the checker doesn't call inferFromTypeArguments() and goes down its alternative inference strategies. In this instance, inferFromObjectTypes() is chosen, but this obviously has nothing to work with, and yields unknown.

These are nonsense examples, but it's not difficult to construct more complex generic object types where the type parameter doesn't appear as a straightforward property type or return type, which can cause inferFromObjectTypes() to fail. (See the motivating example below for where this originally arose.)

Would it be possible for the compiler to traverse the heritage elements of the inference source, and infer the type parameters directly via inferFromTypeArguments() if it finds the target among those elements?

πŸ“ƒ Motivating Example

The case that prompted this question was related to attempting to infer a type parameter from the EventEmitter class in @types/node.

A simplified reduction:

type EventMap<T> = Record<keyof T, unknown[]>;
declare function inferEventMap<T extends EventMap<T>>(emitter: EventEmitter<T>): T;

interface MyEventMap { event1: [boolean]; event2: [string] }

declare class EventEmitter<Events extends EventMap<Events>> {
    on<T extends string | symbol>(eventName: T, listener: T extends keyof Events ? (...args: Events[T]) => void : (...args: any[]) => void): this;
    emit<T extends string | symbol>(eventName: T, ...args: T extends keyof Events ? Events[T] : any[]): boolean;
    // ...
    eventNames(): Array<string | symbol> & Array<keyof Events>;
}
declare class DerivedEventEmitter<Events extends EventMap<Events>> extends EventEmitter<Events> {}

const baseEventEmitter = new EventEmitter<MyEventMap>();
inferEventMap(baseEventEmitter); // returns MyEventMap

const derivedEventEmitter = new DerivedEventEmitter<MyEventMap>();
inferEventMap(derivedEventEmitter); // returns { event1: any; event2: any }

This is one of those cases where inferFromObjectTypes() just doesn't have enough information to infer the type accurately, even though the type checker should have enough information to identify that the type of derivedEventEmitter is a heritage descendent of EventEmitter.

πŸ’» Use Cases

Hopefully covered above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    SuggestionAn idea for TypeScriptToo ComplexAn issue which adding support for may be too complex for the value it adds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions