Issue
I would like to create a function that takes two arguments. a string, which is the key to determine the second parameter type, which is an object. The correlation between the string and the object is typed in a separate type. I'd like to have full autocomplete and type safety when calling the function (I have achieved that) and in the function, by using if or switch statements! the type of the object should be derived by the string parameter.
Here is a simplified example:
export type TLoginParameters = {
username: string;
otherrequiredparam: string;
optionalparam: number;
}
export type TLogoutParameters = {
detailsForLogginOut: string;
}
export type TStateMap = {
login: TLoginParameters;
logout: TLogoutParameters;
};
const edit = <K extends keyof TStateMap , T extends TStateMap[K]>(type: K, entry:T ) => {
switch(type) {
case "login": {
// i expected autocomplete for TLoginParameters- but it doesnt work :(
const prop = entry.username;
break;
}
}
}
// autocomplete works here
const result = edit("login", {username: "test123", otherrequiredparam: "test", optionalparam: 123 });
I tried a different type definition for the function
<O extends TConfigTypeMap, K extends keyof O, V extends O[K]>( type: K, entry:V ) =>
but the result is the same.
Solution
TypeScript is currently unable to use control flow anlysis (the kind of narrowing you get when you check type
in a switch
statement) with generic type parameters. Right now when you check type
, the apparent type of type
can be narrowed from K
to something like K & "login"
, but K
itself stays unaffected. For all the compiler knows, K
might be some wider type than "login"
, like the full union type "login" | "logout"
. Indeed it is possible for someone to call edit()
with a first argument like Math.random()<0.5 ? "login" : "logout"
, and then you couldn't assume that type
being "login"
implies that entry
is TLoginParameters
.
There are various open issues in GitHub asking for improvements to this situation, such as microsoft/TypeScript#27808 to restrict calls so that K
could not be a full union, but would be restricted to exactly one member of the union. For now, though, it's not part of the language. If you want to use generics, you can't easily use control flow analysis. If you want to use control flow analysis, you can't easily use generics.
Your example doesn't actually show a need for generics, though. Instead it looks like you want the type
and entry
parameters to be treated as destructured members of a discriminated union type. Like, if it were a single variable v
of type {type: "login", entry: TLoginParameters} | {type: "logout", entry: TLogoutParameters}
, then you could check v.type
and it would narrow v.entry
.
Luckily you can get this effect by viewing edit()
's parameter list as a discriminated union of tuple-typed rest parameters. Like this:
type EditParams =
{ [K in keyof TStateMap]: [type: K, entry: TStateMap[K]] }[keyof TStateMap]
/* type EditParams =
[type: "login", entry: TLoginParameters] |
[type: "logout", entry: TLogoutParameters]
*/
const edit: (...[type, entry]: EditParams) => void = (type, entry) => {
switch (type) {
case "login": {
const prop = entry.username;
break;
}
}
}
Here EditParams
is a discriminated union of tuple types, and the input to edit
is a rest parameter of type EditParams
. When you implement edit
, the compiler treats type
and entry
as members of that union of tuple types, and when you check type
, it automatically narrows entry
appropriately.
This works exactly how you want, with the unfortunate downside that it's kind of ugly or at least confusing. Still, this is the best we can do for now.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.