Currently I infer type of one property by other property value, but I hit the wall, when I've tried to infer type of one property by second property which is narrowed by third property.
I have a types which describes Game Events, each Game Event is described by 3 properties like so:
evt- type of gameevn- name of eventevd- data associated with event
(() => {
const e: AnyEvent = {} as any;
if (e.evt === EventType.CHESS && e.evn == "make_move" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {from: string; to: string;}
//as for now, e.evd is inferred as all possible CHESS event datas:
/*
evd: (EventData & {
from: string;
to: string;
}) | (EventData & {
count: number;
})
*/
}
if (e.evt === EventType.CHESS && e.evn == "goback" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {count: number;}
//as for now, e.evd is inferred as all possible CHESS event datas:
/*
evd: (EventData & {
from: string;
to: string;
}) | (EventData & {
count: number;
})
*/
}
if (e.evt === EventType.QUIZ && e.evn == "answer" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {body: string;}
//as for now, e.evd is inferred as all possible QUIZ event datas:
/*
evd: (EventData & {
body: string;
}) | (EventData & {
rate: number;
})
*/
}
})();
whole code is here: Typescript Playground Link
and also below here:
interface ChessEventList {
make_move: EventData & {
from: string;
to: string;
};
goback: EventData & {
count: number;
};
}
interface QuizEventList {
answer: EventData & {
body: string;
};
rate_current: EventData & {
rate: number;
};
}
//
enum EventType {
CHESS,
QUIZ,
}
interface EventListByType {
[EventType.CHESS]: ChessEventList;
[EventType.QUIZ]: QuizEventList;
}
//
interface EventData {
gameId: string;
}
interface SomeSendOptions {
timeout?: number;
}
function send<
EVT extends keyof EventListByType,
EVN extends keyof EventListByType[EVT],
EVD extends EventListByType[EVT][EVN]
>(
evt: EVT,
evn: EVN,
evd: EVD,
options?: SomeSendOptions
) {
//[...]
}
send(EventType.CHESS, "goback", { gameId: "1", count: 5 });
send(EventType.QUIZ, "answer", { gameId: "2", body: "my answer is..." }, { timeout: 1000 });
send(EventType.QUIZ, "make_move", { gameId: "3", from: "2D", to: "4E" }, { timeout: 1000 }); //not allowed, should lead to error because "make_move" is CHESS event.
//
type AnyEvent<EVENT_TYPE extends EventType = EventType> = {
[EVT in EVENT_TYPE]: {
evt: EVT;
evn: keyof EventListByType[EVT];
evd: EventListByType[EVT][keyof EventListByType[EVT]];
}
}[EVENT_TYPE];
//
(() => {
const e: AnyEvent = {} as any;
if (e.evt === EventType.CHESS && e.evn == "make_move" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {from: string; to: string;}
//as for now, e.evd is inferred as all possible CHESS event datas:
/*
evd: (EventData & {
from: string;
to: string;
}) | (EventData & {
count: number;
})
*/
}
if (e.evt === EventType.CHESS && e.evn == "goback" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {count: number;}
//as for now, e.evd is inferred as all possible CHESS event datas:
/*
evd: (EventData & {
from: string;
to: string;
}) | (EventData & {
count: number;
})
*/
}
if (e.evt === EventType.QUIZ && e.evn == "answer" ) {
e.evd; // here we want the e.evd type to be correctly inferred as: EventData & {body: string;}
//as for now, e.evd is inferred as all possible CHESS event datas:
/*
evd: (EventData & {
body: string;
}) | (EventData & {
rate: number;
})
*/
}
})();
I've come up with idea that works well, but I can't tie it together with EVENT_NAME generic type, which is required because AnyEvent type should has option to narrowing event kind also in declaration, with EVENT_TYPE it's easy, but can't make use of EVENT_NAME:
type AnyEvent<EVENT_TYPE extends EventType = EventType, EVENT_NAME extends keyof EventListByType[EVENT_TYPE] = keyof EventListByType[EVENT_TYPE]> = {
[EVT in EVENT_TYPE]: {
[EVN in keyof EventListByType[EVT]]: {
evt: EVT,
evn: EVN,
evd: EventListByType[EVT][EVN]
}
}[keyof EventListByType[EVT]];
}[EVENT_TYPE];
Here goes final playground with code above implemented and test cases:
I put test cases also here:
(() => {
const e: AnyEvent = {} as any;
if (e.evt === EventType.CHESS && e.evn == "make_move" ) {
e.evd; // here the e.evd type is correctly inferred as: EventData & {from: string; to: string;}
}
if (e.evt === EventType.CHESS && e.evn == "goback" ) {
e.evd; // here the e.evd type is correctly inferred as: EventData & {count: number;}
}
if (e.evt === EventType.QUIZ && e.evn == "answer" ) {
e.evd; // here the e.evd type is correctly inferred as: EventData & {body: string;}
}
// the problem is below:
const e2: AnyEvent<EventType.QUIZ> = {
evt: EventType.QUIZ,
evn: "rate_current", //everything is fine, we haven't narrowed it in line: `const e2: AnyEvent<EventType.QUIZ>`. it can be "answer" or "rate_current"
evd: {gameId: "1", rate: 10}
};
const e3: AnyEvent<EventType.QUIZ, "answer"> = {
evt: EventType.QUIZ,
evn: "rate_current", //should lead to error, because we narrowed it in line: `const e3: AnyEvent<EventType.QUIZ, "answer">` to be only "answer"
evd: {gameId: "1", rate: 10}
};
})();
Looks like I've found solution:
Here in code above I used
neveras a placeholding value, and then inside of[EVT in EVENT_TYPE]: {revealneveras desiredEventListByTypebut with iterating productEVTlike so:EventListByType[EVT]- of course only then when it'sneverotherwise use provided value.