I have a module that provides the ability for callers to subscribe to a discrete set of events. When a caller subscribes for a given event, they provide the event name as the first argument. From that argument, I would like to be able to infer the callback signature.
The implementation uses an RxJS Subject
for each supported event, and creates a subscription to it for every call to myModule.subscribe(eventType)
. A stripped back version of the implementation is shown below (you can also see this running on this TS Playground workspace)
import { Subject, Subscription } from "rxjs";
const EventNames = {
a: "a",
b: "b",
c: "c",
} as const;
type Payloads = {
[EventNames.a]: string;
[EventNames.b]: number;
[EventNames.c]: boolean;
};
type EventTypes = keyof typeof EventNames;
// all possible event objects
type Events<T> = T extends EventTypes ? {type: T, payload: Payloads[T]} : never;
// all possible callbacks
type Callback<T> = T extends EventTypes ? (event: Events<T>) => void : never;
// all possible subjects
type Subjects<T> = T extends EventTypes ? Subject<Events<T>> : never;
const collection = new Map<EventTypes, Subjects<EventTypes>>();
function fnWithConstraint<T extends EventTypes>(
name: T,
cb: Callback<T>
): Subscription | null {
const subject = collection.has(name)
? collection.get(name)
: new Subject<Events<T>>();
if (subject !== undefined) {
collection.set(name, subject);
/* ^ Type '{ type: "a"; payload: string; }' is not assignable
to type 'Events<T>'.
*/
const subscription = subject.subscribe(cb)
/* ^ This expression is not callable. Each member of the union
type '{ (observer?: Partial<Observer<Events<T>>> | undefined):
Subscription; (next: (value: Events<T>) => void): Subscription;
... 1 more ...' has signatures, but none of those signatures
are compatible with each other.
*/
return subscription;
}
return null;
}
fnWithConstraint("b", (event) => console.log(event));
// expect typeof event -> { type: "b"; payload: number; }
I can't get this to compile successfully. The TS Playground I've linked to shows the correct result that I'm after on line:45, but the compiler is complaining about the Subjects type signature, and I can't seem to resolve it. What am I missing?
CodePudding user response:
You just need to overload your function:
import { Subject, Subscription } from "rxjs";
const EventNames = {
a: "a",
b: "b",
c: "c",
} as const;
type Payloads = {
[EventNames.a]: string;
[EventNames.b]: number;
[EventNames.c]: boolean;
};
type EventTypes = keyof typeof EventNames;
// all possible event objects
type Events<T extends EventTypes> = { type: T, payload: Payloads[T] }
// all possible callbacks
type Callback<T extends EventTypes> = (event: Events<T>) => void
// all possible subjects
type Subjects<T extends EventTypes> = Subject<Events<T>>
const collection = new Map<EventTypes, Subjects<EventTypes>>();
function fnWithConstraint<T extends EventTypes>(
name: T,
cb: Callback<T>
): Subscription | null
function fnWithConstraint(
name: EventTypes,
cb: Callback<EventTypes>
): Subscription | null {
const subject = collection.has(name)
? collection.get(name)
: new Subject<Events<EventTypes>>()
if (subject !== undefined) {
collection.set(name, subject);
return subject.subscribe(cb)
}
return null
}
fnWithConstraint("b", (event) => console.log(event));
In this case you should loose the strictness a bit. From the outside, this function now is safe.
Your example don't work because you use T
which is a subtype. IT does not mean that T
is equal to a | b | c
it just means that T
might be any subtype of this union.
Consider this example:
function fnWithConstraint<T extends EventTypes,>(
name: T,
cb: Callback<T>
): Subscription | null {
const subject = collection.has(name)
? collection.get(name)
: new Subject<Events<T>>()
if (subject !== undefined) {
collection.set(name, subject);
return subject.subscribe(cb)
}
return null
}
declare const a:'a' & {__tag:'Hello'}
fnWithConstraint(a, (event) => console.log(event));
event
argument is Events<"a" & { __tag: 'Hello'; }>
.
While TS allows you to use const a
, it is unsafe to use event
argument, fnWithConstraint(a, (event) => console.log(event.type.__tag /** Hello */));
Please keep in mind, that a
for typescript is a regular object which can has his own subtypes. See this: keyof 'a'
. More than 30 properties.
TS will also allow you to use this branded type with mine solution, but event
argument will be valid/safe
can class methods be overloaded in the same way
Yes.
Simple example:
class Foo {
run(arg: string): number
run(arg: number): string
run() {
return 'NOT IMPLEMENTED' as any
}
}
const result = new Foo();
result.run(42) // string
result.run('str') // number