Lets gets into example directly.
In the below code you can see keyof
being used, but typescript still computes the type of EVT
as string|number|string
, even when EVT
extends keyof ((typeof Events)[OBJ])
CODE: (AT LAST LINE AUTOCOMPLETE & TYPECHECK WORKS FINE)
export const Events = {
// [event: string]: (...args: any) => void;
account: {
available: (status?: 'online'|'offline',foo?:'test',bar?:'test2') => {
console.error('1');
},
}
}
export class AMQP {
static event<
OBJ extends keyof typeof Events,
EVT extends keyof ((typeof Events)[OBJ]),
FUNC extends ((typeof Events)[OBJ][EVT])
>(
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
AMQP.event('account','available','online','test','test2');
ISSUE (RED FLAG ON HOVER OF 'FUN')
Type 'FUNC' does not satisfy the constraint '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][EVT]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][keyof { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ]]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string] | { account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][number] | { ...; }[OBJ][symbol]' is not assignable to type '(...args: any) => any'.
Type '{ account: { available: (status?: "online" | "offline" | undefined, foo: "test", bar: "test2") => void; }; }[OBJ][string]' is not assignable to type '(...args: any) => any'.(2344)
UPDATE:::
as of now. issue as suggested by @Maciej Sikora by forcing declare FUN as function fixed error.
but here is another example. where it works fine without force declaration only difference is Events is not deep, so it works there but not when object is little more deep, i am still curious to know reasons.
We can fix it by saying explicitly to TS that we only want functions, even though there currently is only that looks like TS is not able to narrow it. Consider:
type E = typeof Events // for readability
export class AMQP {
static event<
OBJ extends keyof E,
EVT extends keyof E[OBJ],
FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT] : never
>(
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
The main point is - FUNC extends E[OBJ][EVT] extends (...a:any[]) => any ? E[OBJ][EVT] : never
. It means that we allow only functions to be picked in second level of the object structure. Looks now TS is not complaining anymore.
Additional plus of such solution is that if we provide a field which will not be an function, there will be no possibility to call the function with that choice.
There are some reasons which can explain it partially. Consider similar example:
const a = {
a: 2
}
const f = <A extends typeof a, K extends keyof A>(a: A, k: K) => {
return a[k] + 1; // error we cannot use +
}
f(a, 'a');
How it is if we cannot use +
if a
has only number value inside. Looks like it is wrong? No its correct as our function says we allow on the type which extends typeof a
, so its not the same as a
, we can have object which has property a
but also another property b
which will be for example boolean. Consider that below code compiles:
const b = {
a: 2,
b: true,
}
f(b, 'b');
So I can use object which structurally has the same fields - a
but also have another with different types. By the same reason your code cannot narrow the type, as we can introduce another object which will be structurally the same, but would have another non-function properties.
In our example we have little bit different case, as we refer to one and single object. Then its not fully clear why on some nested level the inference fails. TS needs additional help with narrowing to function. Why is for me unknown, looks like inference limitations.
The second solution which you should consider is static typying the Events interface. Consider:
type Events = Record<string, Record<string, (...a: any[]) => any>>
export const events = {
account: {
available: (status?: 'online' | 'offline', foo: 'test', bar: 'test2') => {
console.error('1');
},
}
};
export class AMQP {
static event<
INSTANCE extends Events,
OBJ extends keyof INSTANCE,
EVT extends keyof INSTANCE[OBJ],
FUNC extends INSTANCE[OBJ][EVT]
>(
inst: INSTANCE,
obj: OBJ,
event: EVT,
...args: Parameters<FUNC>
) {
}
}
// pay attention below I pass events object explicitly as first argument
AMQP.event(events, 'account', 'available', 'online', 'test', 'test2' );
Pay attention that I have introduce few things here:
Such solution is more flexible, as implementation is not bound to one object, you can pass any object which is structurally correct with Events
interface, also there is not need for conditional types.