diff --git a/api_guard/dist/types/index.d.ts b/api_guard/dist/types/index.d.ts index abbd15a343..cd6a43d8d7 100644 --- a/api_guard/dist/types/index.d.ts +++ b/api_guard/dist/types/index.d.ts @@ -257,6 +257,7 @@ export declare function fromEvent(target: HasEventTargetAddRemove | ArrayL export declare function fromEvent(target: HasEventTargetAddRemove | ArrayLike>, eventName: string, resultSelector: (event: T) => R): Observable; export declare function fromEvent(target: HasEventTargetAddRemove | ArrayLike>, eventName: string, options: EventListenerOptions): Observable; export declare function fromEvent(target: HasEventTargetAddRemove | ArrayLike>, eventName: string, options: EventListenerOptions, resultSelector: (event: T) => R): Observable; +export declare function fromEvent>(target: E | ArrayLike, eventName: T): Observable>; export declare function fromEvent(target: NodeStyleEventEmitter | ArrayLike, eventName: string): Observable; export declare function fromEvent(target: NodeStyleEventEmitter | ArrayLike, eventName: string): Observable; export declare function fromEvent(target: NodeStyleEventEmitter | ArrayLike, eventName: string, resultSelector: (...args: any[]) => R): Observable; diff --git a/spec-dtslint/observables/fromEvent-spec.ts b/spec-dtslint/observables/fromEvent-spec.ts index 8ef58e387c..69b89b6265 100644 --- a/spec-dtslint/observables/fromEvent-spec.ts +++ b/spec-dtslint/observables/fromEvent-spec.ts @@ -101,3 +101,24 @@ it('should support a jQuery-style source', () => { it('should support a jQuery-style source result selector', () => { const a = fromEvent(jQueryStyleSource, "something", () => "something else"); // $ExpectType Observable }); + +/** + * Huan(202111): Correct typing inference for Node.js EventEmitter as `number` + * @see https://github.com/ReactiveX/rxjs/pull/6669 + */ +it('should successful inference the first argument from the listener of Node.js EventEmitter', (done) => { + class NodeEventeEmitterTest { + addListener(eventName: 'foo', listener: (foo: false) => void): this + addListener(eventName: 'bar', listener: (bar: boolean) => void): this + addListener(eventName: 'foo' | 'bar', listener: ((foo: false) => void) | ((bar: boolean) => void)): this { return this; } + + removeListener(eventName: 'foo', listener: (foo: false) => void ): this + removeListener(eventName: 'bar', listener: (bar: boolean) => void ): this + removeListener(eventName: 'foo' | 'bar', listener: ((foo: false) => void) | ((bar: boolean) => void)): this { return this; } + } + + const test = new NodeEventeEmitterTest(); + + const foo$ = fromEvent(test, 'foo'); // $ExpectType Observable + const bar$ = fromEvent(test, 'bar'); // $ExpectType Observable +}); diff --git a/spec-dtslint/util/NodeEventEmitterDataType-spec.ts b/spec-dtslint/util/NodeEventEmitterDataType-spec.ts new file mode 100644 index 0000000000..b90aa0a301 --- /dev/null +++ b/spec-dtslint/util/NodeEventEmitterDataType-spec.ts @@ -0,0 +1,96 @@ +import { + NodeEventEmitterNameDataPair, + NodeEventEmitterDataType, + NodeEventEmitterDataTypeUnknown, + AnyToUnknown, +} from '../../src/internal/util/NodeEventEmitterDataType'; + +it('NodeEventEmitterDataType smoke testing', () => { + const fooEvent = 'fooEvent'; + const barEvent = 'barEvent'; + + type FOO_DATA = typeof fooEvent; + type BAR_DATA = typeof barEvent; + + class NodeEventEmitterFixture { + addListener(eventName: 'foo', listener: (foo: FOO_DATA) => void): this + addListener(eventName: 'bar', listener: (bar: BAR_DATA) => void): this + addListener(eventName: 'foo' | 'bar', listener: ((foo: FOO_DATA) => void) | ((bar: BAR_DATA) => void)): this { return this; } + + removeListener(eventName: 'foo', listener: (foo: FOO_DATA) => void ): this + removeListener(eventName: 'bar', listener: (bar: BAR_DATA) => void ): this + removeListener(eventName: 'foo' | 'bar', listener: ((foo: FOO_DATA) => void) | ((bar: BAR_DATA) => void)): this { return this; } + + /** + * TODO: JQueryStyle compatible in the future + */ + // on(eventName: 'foo', listener: (foo: number) => void): void + // on(eventName: 'bar', listener: (bar: string) => void): void + // on(eventName: 'foo' | 'bar', listener: ((foo: number) => void) | ((bar: string) => void)): void {} + + // off(eventName: 'foo', listener: (foo: number) => void ): void + // off(eventName: 'bar', listener: (bar: string) => void ): void + // off(eventName: 'foo' | 'bar', listener: ((foo: number) => void) | ((bar: string) => void)): void {} + } + + it('should get emitter name & data types correctly', () => { + // $ExpectType "fooEvent" + type Foo = NodeEventEmitterDataType; + // $ExpectType "barEvent" + type Bar = NodeEventEmitterDataType; + }); + + it('should get name & data from NodeEventEmitterNameDataPair', () => { + // type EVENT_PAIR = TypeEventPair + type EVENT_PAIR = NodeEventEmitterNameDataPair; + + // $ExpectType "foo" | "bar" + type EVENT_NAME = EVENT_PAIR[0]; + // $ExpecTType FOO_DATA | BAR_DATA + type EVENT_DATA = EVENT_PAIR[1]; + }); + + it('should get `unknown` for `process` events by NodeEventEmitterDataTypeUnknown', () => { + // $ExpectType unknown + type Exit = NodeEventEmitterDataTypeUnknown< + Pick< + typeof process, + 'addListener' | 'removeListener' + >, + 'exit' + >; + }); + + it('should get `never` for `process` events by NodeEventEmitterDataType', () => { + // $ExpectType never + type Exit = NodeEventEmitterDataType< + Pick< + typeof process, + 'addListener' | 'removeListener' + >, + 'exit' + > + }); +}); + +it('AnyToUnknown smoke testing', () => { + it('should only convert any to unknown', () => { + type T_ANY = AnyToUnknown + type T_UNKNOWN = AnyToUnknown + + type T_BOOLEAN = AnyToUnknown + type T_NULL = AnyToUnknown + type T_OBJ = AnyToUnknown + type T_STRING = AnyToUnknown + type T_UNDEFINED = AnyToUnknown + type T_VOID = AnyToUnknown + type T_NEVER = AnyToUnknown + + // $ExpectType unknown + type UNKNOWN_TYPE = T_ANY & T_UNKNOWN + + type KNOWN_TYPE = T_VOID | T_BOOLEAN | T_STRING | T_UNDEFINED | T_NULL | T_OBJ | T_NEVER + // $ExpectType true + type T = unknown extends KNOWN_TYPE ? never : true + }); +}); diff --git a/spec/observables/fromEvent-spec.ts b/spec/observables/fromEvent-spec.ts index 9fe29b1b64..2b8edfdbbd 100644 --- a/spec/observables/fromEvent-spec.ts +++ b/spec/observables/fromEvent-spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { expectObservable } from '../helpers/marble-testing'; -import { fromEvent, NEVER, timer } from 'rxjs'; +import { fromEvent, NEVER, Observable, timer } from 'rxjs'; import { mapTo, take, concat } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; @@ -432,4 +432,5 @@ describe('fromEvent', () => { expect(nodeList[0]._removeEventListenerArgs).to.deep.equal(nodeList[0]._addEventListenerArgs); expect(nodeList[1]._removeEventListenerArgs).to.deep.equal(nodeList[1]._addEventListenerArgs); }); + }); diff --git a/src/internal/observable/fromEvent.ts b/src/internal/observable/fromEvent.ts index 8044ff9da9..44a670f4a1 100644 --- a/src/internal/observable/fromEvent.ts +++ b/src/internal/observable/fromEvent.ts @@ -4,6 +4,7 @@ import { mergeMap } from '../operators/mergeMap'; import { isArrayLike } from '../util/isArrayLike'; import { isFunction } from '../util/isFunction'; import { mapOneOrManyArgs } from '../util/mapOneOrManyArgs'; +import { NamedNodeEventEmitter, NodeEventEmitterDataTypeUnknown } from '../util/NodeEventEmitterDataType'; // These constants are used to create handler registry functions using array mapping below. const nodeEventEmitterMethods = ['addListener', 'removeListener'] as const; @@ -78,6 +79,17 @@ export function fromEvent( resultSelector: (event: T) => R ): Observable; +/** + * Automatically infer overloaded Node.js EventEmitter event types: + * get event data type (`typeof args[0]` of the listener) by event name. + * + * @see https://github.com/ReactiveX/rxjs/pull/6669 + */ +export function fromEvent>( + target: E | ArrayLike, + eventName: T +): Observable>; + export function fromEvent(target: NodeStyleEventEmitter | ArrayLike, eventName: string): Observable; /** @deprecated Do not specify explicit type parameters. Signatures with type parameters that cannot be inferred will be removed in v8. */ export function fromEvent(target: NodeStyleEventEmitter | ArrayLike, eventName: string): Observable; diff --git a/src/internal/util/NodeEventEmitterDataType.ts b/src/internal/util/NodeEventEmitterDataType.ts new file mode 100644 index 0000000000..491ee62019 --- /dev/null +++ b/src/internal/util/NodeEventEmitterDataType.ts @@ -0,0 +1,220 @@ +/* eslint-disable no-use-before-define */ +/* eslint-disable max-len */ + +/** + * Node.js EventEmitter Add/Remove Listener interface + */ +interface L { + (name: N, listener: (data: D, ..._: any[]) => any): any; +} + +/** + * + * Overload function inferencer + * + * - L: Listener interface with Add/Remove methods + * - N: Name of the event + * - D: Data of the event + * + */ +interface L1 extends L {} +interface L2 extends L, L {} +interface L3 extends L, L, L {} +interface L4 extends L, L, L, L {} +interface L5 extends L, L, L, L, L {} +interface L6 extends L, L, L, L, L, L {} +interface L7 + extends L, + L, + L, + L, + L, + L, + L {} +interface L8 + extends L, + L, + L, + L, + L, + L, + L, + L {} +interface L9 + extends L, + L, + L, + L, + L, + L, + L, + L, + L {} + +type EventNameDataPair1 = AddRemoveListener extends L1 ? [N1, D1] : never; +type EventNameDataPair2 = AddRemoveListener extends L2 + ? [N1, D1] | [N2, D2] + : never; +type EventNameDataPair3 = AddRemoveListener extends L3 + ? [N1, D1] | [N2, D2] | [N3, D3] + : never; +type EventNameDataPair4 = AddRemoveListener extends L4< + infer N1, + infer N2, + infer N3, + infer N4, + infer D1, + infer D2, + infer D3, + infer D4 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] + : never; +type EventNameDataPair5 = AddRemoveListener extends L5< + infer N1, + infer N2, + infer N3, + infer N4, + infer N5, + infer D1, + infer D2, + infer D3, + infer D4, + infer D5 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] | [N5, D5] + : never; +type EventNameDataPair6 = AddRemoveListener extends L6< + infer N1, + infer N2, + infer N3, + infer N4, + infer N5, + infer N6, + infer D1, + infer D2, + infer D3, + infer D4, + infer D5, + infer D6 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] | [N5, D5] | [N6, D6] + : never; +type EventNameDataPair7 = AddRemoveListener extends L7< + infer N1, + infer N2, + infer N3, + infer N4, + infer N5, + infer N6, + infer N7, + infer D1, + infer D2, + infer D3, + infer D4, + infer D5, + infer D6, + infer D7 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] | [N5, D5] | [N6, D6] | [N7, D7] + : never; +type EventNameDataPair8 = AddRemoveListener extends L8< + infer N1, + infer N2, + infer N3, + infer N4, + infer N5, + infer N6, + infer N7, + infer N8, + infer D1, + infer D2, + infer D3, + infer D4, + infer D5, + infer D6, + infer D7, + infer D8 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] | [N5, D5] | [N6, D6] | [N7, D7] | [N8, D8] + : never; +type EventNameDataPair9 = AddRemoveListener extends L9< + infer N1, + infer N2, + infer N3, + infer N4, + infer N5, + infer N6, + infer N7, + infer N8, + infer N9, + infer D1, + infer D2, + infer D3, + infer D4, + infer D5, + infer D6, + infer D7, + infer D8, + infer D9 +> + ? [N1, D1] | [N2, D2] | [N3, D3] | [N4, D4] | [N5, D5] | [N6, D6] | [N7, D7] | [N8, D8] | [N9, D9] + : never; + +interface HasNodeEventEmitterAddRemove { + addListener(name: N, listener: (data: D, ...args: any[]) => void): this; + removeListener(name: N, listener: (data: D, ...args: any[]) => void): this; +} + +/** + * Get the event name/data pair types from an event emitter + * + * @return `['foo', number] | ['bar', string]` + */ +type EventNameDataPair['addListener']> = + | EventNameDataPair9 + | EventNameDataPair8 + | EventNameDataPair7 + | EventNameDataPair6 + | EventNameDataPair5 + | EventNameDataPair4 + | EventNameDataPair3 + | EventNameDataPair2 + | EventNameDataPair1; + +/** + * Convert the `any` type to `unknown for a better safety + * + * @return `AnyToUnknown -> unknown` + * + * TODO: huan(202111) need to be tested more and confirm it has no bug in edge cases + */ +type AnyToUnknown = unknown extends T ? unknown : T; + +// the [eventName, eventData] types array +type NodeEventEmitterNameDataPair> = EventNameDataPair; + +/** + * + * Tada! Get event emitter data type by event name 8-D + * + */ +type NodeEventEmitterDataType, T> = Extract, [T, any]>[1]; + +// Convert `never` to `unknown` +type NodeEventEmitterDataTypeUnknown, T> = NodeEventEmitterDataType extends never + ? unknown + : NodeEventEmitterDataType; + +interface NamedNodeEventEmitter { + addListener(name: N, handler: (data: NodeEventEmitterDataType, N>, ...args: any[]) => any): this; + removeListener(name: N, handler: (data: NodeEventEmitterDataType, N>, ...args: any[]) => any): this; +} + +export type { + AnyToUnknown, + NamedNodeEventEmitter, + NodeEventEmitterDataType, + NodeEventEmitterDataTypeUnknown, + NodeEventEmitterNameDataPair, +};