microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.76k stars 12.46k forks source link

Interface that extends another no longer constrains types like the original #60008

Open steveluscher opened 1 month ago

steveluscher commented 1 month ago

🔎 Search Terms

interface interface constrain extend interface

🕗 Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/PQKhAIGUFEBUFUAK4C8b0c1z4TAFABmArgHYDGALgJYD2p4lAngA4CmAPLAHwAUAlAC5wscAG9wAJzaVikhqWIAbJeACGAZ3BkA1qVoB3BppEBucAF98zduGgA3NqUoBZNS1TgASm3K1JACYcGpSS1KQA5gA0do7O3KbWrGzgADLUIU5sklwOTpTgbAAelE4BWnnxnry8bPaUwrCVlPyo3OD2tNQBrQA+4uAAFmqkAUpszby0AEYAVr4NIs1CHV0Blon4xSz+BeGlkoRq5CmwyQHN0AC21JQHuXGu7oUlZRWPbiztYvjgf+oBC6PdKZUjZB75M62YqlUZaHRsJi0QhLD7uPi-f5YmxsRrNKFsKKYrF-JQZWHZYQgik5JpolgAbTpkOSAF1uESSf9aCwaPQNAB+YQAQUBzWpWUkAHleXRSFp+tNaLRxiNOf8Vp1uoksdIrrRHOLyZKIc4CS9YeVwAikSjmc5PhiuX8cXjHgT1VyyaDKWljWDac1Pkz8WyOcSSTy+fKhbF8hKAzLowrwEqVWw1RHNWtElZQBBoAA5AAiUDgSFQ2Cr1ZQuAI+HzvwggFByKAjW5MRhsEJNgg4rjnS43O7giSEZXCZqWbh8VoaNQ0DSEajdkSDx7XW73McTuPOafmYDAcDN-AN48AAUoGgAtNsFnfJJJ-El2AP2ED8puRzkJNM1JIk6PNOs7gPOi7LquBKfs437buA460EB+QHuAR4nmeja4Ce4DCmSJjIowgwpP41AROEaiqDivavikADCypKEOW7gvaTweDCbx7ux7S1tBzE-qaPGbP2DEqgJ8GIch+4WDOAhgQuGSQVo-EbsOkm7lOsmHsep4Rs6BmGUZxkmaZJnoVet73lQj7PpItEcGJTFqSxv6pgB0kFLJoHgUpK4qeuX7qaOCGacB2lobpmFgE24CADLk4AAJLONkRwnERC5gdQVwsEonacXCREkWE5GkJRjDJDR+ypcc9GMclBxpRMLmCWxnwWlxQboh1hWqUFrlCY6AxWA5TkNTVJwSSFUncSB8m+Uu-lrh+U1uTNWkJJFGH6WZu17ftB1GRZ153kU7A2dkdmjfVKWHLVq0cH+Hmzd582KYtUGBbBwVrWFKERehcXRSAsUJeNd3pZQwwFAVVpQ8VZEUVRyTqKMAJWiExCEIQVW3U14BjXjtWIEoxAaMWGQsAu5CDA9bXPLD7z5ENjPLWwMGUHBrFdV84gRgElPU7TjxCearM2oR9NfLU9husz7ghu6bLZtq+AjaJN2NcTpPk4LlA0w9O5IS9clzu9ylsxzXO-cbG06dth2O07zsu38x1WWdD6XS+GsquDTUk2TFMaFT+vC-1glPYBJs+ebS19d9A1G55qGA8DsWtgxLCdlTIThBERXgH4AQpOEmRqOshGw-nhfVRDKQGP4OgaLjWvpVnTCIJopRWz9g0M68hU898EYVxzCbc0r0KD1aEt2sPvA7S6yRy2aySeiS3o0lS-qT-LjJsQS7Ib1iUZyoKIpisCu-SrK-LgIqjEZqQG8qwEOr-HqBrNfGN+iyj4tESSwXkvCq7BV6UA9KArekod4+kDPSRWLJ2DH1AWffksYjTwKTOfB+qYn6ZixG-D+fwBYhyFs0f+09LTwiAfPekfA6gQODIfZWwgtTvzVmeDW2cu6ZF7knUKttwqmwUhBeOX1OZ92Tibe2elXYKMUftd2p1zqUFsj7ZIjkeSd27uzQ27lo4bVjuIz6K0WoaWEf9Ta6FmxAA

💻 Code

/** SETUP ========================== */
function type<T>(): T { return null as unknown as T; }
type EventMap = Record<string, Event>;
type Listener<TEvent extends Event> = ((evt: TEvent) => void) | { handleEvent(object: TEvent): void };

export interface TypedEventEmitter<TEventMap extends EventMap> {
    addEventListener<TEventType extends keyof TEventMap>(
        type: TEventType,
        listener: Listener<TEventMap[TEventType]>,
        options?: AddEventListenerOptions | boolean,
    ): void;
    removeEventListener<TEventType extends keyof TEventMap>(
        type: TEventType,
        listener: Listener<TEventMap[TEventType]>,
        options?: EventListenerOptions | boolean,
    ): void;
}
/** END SETUP ========================== */

/**
 * ✅ Sanity test
 */
type<TypedEventEmitter<{ foo: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

// @ts-expect-error
type<TypedEventEmitter<{ bar: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

/**
 * ✅ Alias of the original type
 */
type CoolEventEmitter<TEventMap extends EventMap> = TypedEventEmitter<TEventMap>;

type<CoolEventEmitter<{ foo: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

                                                                                      // @ts-expect-error
type<CoolEventEmitter<{ bar: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

/**
 * ❌ Interface that simply extends the original type
 */
interface CoolInterfaceEventEmitter<TEventMap extends EventMap> extends TypedEventEmitter<TEventMap> { }

type<CoolInterfaceEventEmitter<{ foo: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

                                                                                               // @ts-expect-error
type<CoolInterfaceEventEmitter<{ bar: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ❌

/**
 * ❌ Interface that extends the original type and adds stuff
 */
interface CoolInterfacePlusDispatchEventEmitter<TEventMap extends EventMap> extends TypedEventEmitter<TEventMap> {
    dispatchEvent<TEventType extends keyof TEventMap>(ev: TEventMap[TEventType]): void;
}

type<CoolInterfacePlusDispatchEventEmitter<{ foo: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

                                                                                                           // @ts-expect-error
type<CoolInterfacePlusDispatchEventEmitter<{ bar: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ❌

/**
 * ✅ Copy pasting the code instead of extending the interface works
 */
interface CopyPastedEventEmitter<TEventMap extends EventMap> {
    addEventListener<TEventType extends keyof TEventMap>(
        type: TEventType,
        listener: Listener<TEventMap[TEventType]>,
        options?: AddEventListenerOptions | boolean,
    ): void;
    removeEventListener<TEventType extends keyof TEventMap>(
        type: TEventType,
        listener: Listener<TEventMap[TEventType]>,
        options?: EventListenerOptions | boolean,
    ): void;
    dispatchEvent<TEventType extends keyof TEventMap>(ev: TEventMap[TEventType]): void;
}

type<CopyPastedEventEmitter<{ foo: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

                                                                                            // @ts-expect-error
type<CopyPastedEventEmitter<{ bar: Event }>>() satisfies TypedEventEmitter<{ foo: Event }>; // ✅

🙁 Actual behavior

As soon as the original interface is extended – whether the extender adds properties, does not add properties, uses the type parameter, or doesn't use the type parameter – I seem to lose the ability to constrain types in the same way as the original interface can.

🙂 Expected behavior

I would expect an interface that extends another to constrain types in exactly the same way as the original.

Additional information about the issue

No response

RyanCavanaugh commented 1 month ago

Shorter repro

declare function type<T>(): T;
interface KeyCaller<T> {
    callMe<K extends keyof T>(arg: K, val: T[K]): void;
}
interface KeyCaller2<T> extends KeyCaller<T> { }
type Foo = { foo: string };
type Bar = { bar: number };

// Error, good
type<KeyCaller<Foo>>() satisfies KeyCaller<Bar>;
// No error, bad
type<KeyCaller<Foo>>() satisfies KeyCaller2<Bar>;