Issue
The title is pretty confusing, so let me explain.
What I want to archive
This referrers to the code below. I want to type the route
property based on the types
property to be either a string
or a function that returns a string
.
The code
First let's start with a working example.
// A typescript hack to define types without providing actual values
const defineArgTypes = <
T extends {
args?: ArgumentsBase | null;
}
>() => null as any as T;
interface ArgumentsBase {
queryParams?: Record<string | number | symbol, any>;
}
type BaseActionMap = Record<
string,
{
types?: { args?: ArgumentsBase };
}
>;
type ActionMapItem<Item extends BaseActionMap[string]> = {
types?: Item['types'];
};
type ActionMap<BaseConfig extends BaseActionMap> = {
[Property in keyof BaseConfig]: ActionMapItem<BaseConfig[Property]>;
};
type ConfigMapItem<Item extends BaseActionMap> = {
route: Item['types'] extends { args: ArgumentsBase }
? (args: Item['types']['args']['queryParams']) => string
: string;
};
type ConfigMap<AMap extends ActionMap<any>> = {
[Property in keyof AMap]: ConfigMapItem<AMap[Property]>;
};
const defineActions = <Data extends BaseActionMap>(
actions: Data,
config: ConfigMap<Data>
) => {
// code inside here is not relevant to the question
};
const config = defineActions(
{
getTodos: {},
getTodo: {
types: defineArgTypes<{ args: { queryParams: { id: string } } }>(),
},
},
{
getTodo: {
route: (d) => `todos/${d.id}`,
},
getTodos: {
route: 'todos',
},
}
);
In the code above you can see, that I need to define the "actions (getTodos, getTodo)" 2 times.
Is there a way to reduce this to the following without loosing conditional types for the route
properties?
const config = defineActions(
{
getTodos: {
route: 'todos',
},
getTodo: {
types: defineArgTypes<{ args: { queryParams: { id: string } } }>(),
route: (d) => `todos/${d.id}`,
},
}
);
Solution
What you are looking for is a discriminated union. That's basically a type that is the union of two types that have a common property--the "discriminant--that Typescript can use to narrow the union. So, simplifying your example slightly, you want a type like this:
type Config = {
types?: undefined
route: string
} |
{
types: {id:string}
route: (args:{id:string})=>string
}
The types
property is the discriminant. If it's undefined, TS will know to narrow the type to the first member of the union. If it's {id:string}
, TS will narrow to the second member of the union. You could, of course, have more than three options.
Now, it looks like you want route
in the second case to actually use some of the typing from types
, so we'll make this union use a generic, so it looks like this. But it's still fundamentally a discriminated union.
type Config<T extends {id: string}> = {
types?: undefined
route: string
} |
{
types: T
route: (args:T)=>string
}
Then it's just a matter of using it in your defineActions
function with some appropriate type parameters so TS can infer the types of your generics and voila:
function defineActions<T extends {id:string}, U extends Record<string,Config<T>>>(configs:U){}
const config = defineActions(
{
getTodos: {
route: 'todos',
},
getTodo: {
types: { id: "myId" },
route: (d) => `todos/${d.id}`,
},
}
);
Here it is in a playground
OK, one little caveat: you might wonder--how does TS know that types
is the discriminant vs route
? The answer is--it' doesn't. In fact, given that types
can be undefined, TS is actually probably using route
to discriminate, but it doesn't really matter here because your types don't overlap. If you wanted to make sure it discriminated based on types
, you would want to make the rest of the type different, e.g.:
type Config<T extends {id: string}> = {
types?: undefined
basicRoute: string
} |
{
types: T
complexRoute: (args:T)=>string
}
But I don't think that's needed for what you are trying to do here.
I've obviously simplified your example, but I don't think I've done it in any way that's material to your question. Please clarify, if you think I missed something.
Answered By - sam256
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.