Description
TypeScript Version: typescript@^3.4.0-dev.20190316
Search Terms: generic constraints, generic inference, generic inheritance
Code
I'm updating my event emitter to type all possible listeners, so that all on
and emit
methods are strictly typed. This is done by using conditional and mapped types, to infer the argument and function signatures from an interface like so:
export interface TaskEvents {
// Define args
fail: [Error];
// Define args and return (entire func)
pass: (value: any) => Promise<any>;
}
This works well in isolation, as defined here: https://github.com/milesj/boost/pull/45/files#diff-af915a397c4f33b89e6fba137b261584
However, when consumed in another package that is using generics to define the events, the type system breaks down. A few examples.
Constraints + default generics (DOESNT WORK)
This approach uses generic constraints and defaults to define the events. This is ideal so that sub-classing can inherit the parents events and everything works correctly.
export interface TaskEvents {
fail: [Error];
pass: [any];
run: [any];
skip: [any];
}
class Task<Events extends TaskEvents = TaskEvents> extends Emitter<Events> {}
task.emit('run', [something]);
However, this doesn't seem to work:
Defaults only (DOESNT WORK)
This approach only uses defaults and no constraints, but obviously doesn't work since the class isn't aware of the explicit events.
class Task<Events = TaskEvents> extends Emitter<Events> {}
task.emit('run', [something]);
Constraints only (DOESNT WORK)
Inverse of the previous example. This has the same problem as the first example.
class Task<Events extends TaskEvents> extends Emitter<Events> {}
task.emit('run', [something]);
No sub-class generics / Super-class explicitly defined (DOES WORK)
This approach does work.
class Task extends Emitter<TaskEvents> {}
task.emit('run', [something]);
However, this is not ideal since sub-classes can't override the defined events. An example as so:
export interface RoutineEvents extends TaskEvents {
command: [string];
'command.data': [string, string];
}
class Routine<Events extends RoutineEvents = RoutineEvents> extends Task<Events> {}
// Should inherit and work
routine.emit('run', [something]);
Expected behavior:
The constraints/defaults on class generics are inherited correctly.
Actual behavior:
They don't seem to be inherited correctly unless explicitly defined.
Playground Link:
PR has a handful of usage: milesj/boost#45
Related Issues:
Looked around a bit, found this?
- inference problem with constraints involving conditionals and generics #29662 - I updated my code to use
unknown
, same issues.