'How to define a conditional type based on a presence of a property?
I am building a type for a router API. The route type at the moment looks like this:
interface RouteConfig<
ParentPath extends string = string,
Path extends string = string,
Props extends PreparedRouteEntryProps = { params: {}; search: {} },
AssistMode extends boolean = boolean
>
children?: ReadonlyArray<RouteConfig<`${ParentPath}${Path}`>>;
component?: () => Promise<ComponentType<Props>>;
path: Path;
preload?: (
routeParameters: RouteParameters<`${ParentPath}${Path}`>,
searchParameters: Record<string, string[] | string>
) => AssistMode extends true ? AssistedPreloadData : UnassistedPreloadData;
redirectRules?: (
namedParameters: RouteParameters<`${ParentPath}${Path}`>,
searchParameters: Record<string, string[] | string>
) => string | null;
}
I want to improve developer experience by adjusting the type based on what is already defined. Namely:
- If
redirectRulesis set, thencomponentmust not be set. - If
redirectRulesis unset, thencomponentmust bet set. - If
preloadis set, thencomponentshould be() => Promise<ComponentType<A>>. - If
preloadis unset, thencomponentshould be() => Promise<ComponentType<B>>.
I am familiar with unions and tagged unions in typescript. However, I cannot wrap my head around how to write such a type. It is particularly confusing because component is affected by preload and redirectRules.
The way I thought it would be is something along the lines of... (simplified types)
type RouteBase = {
children?: string;
path: string;
};
type BranchA = {
redirectRules: () => string;
component: never
};
type BranchB = {
redirectRules: never;
};
type BranchC = {
component: () => number,
preload: () => string;
};
type BranchD = {
component: () => string;
preload: never;
};
type RouteConfig = RouteBase & (BranchA | BranchB) & (BranchC | BranchD);
but that throws errors when it should not, e.g.
Solution 1:[1]
Figured it out:
type RouteBase = {
children?: string;
path: string;
};
type BranchA = {
component: () => string;
preload?: never;
redirectRules?: never;
};
type BranchB = {
component: () => boolean;
preload: string;
redirectRules?: never;
};
type BranchC = {
component?: never;
preload?: never;
redirectRules: string;
};
type RouteConfig = RouteBase & (BranchA | BranchB | BranchC);
const a: RouteConfig = { // should work
children: '',
path: '',
redirectRules: ''
};
const b: RouteConfig = { // should work
children: '',
path: '',
preload: '',
component: () => true,
};
const c: RouteConfig = { // should work
children: '',
path: '',
component: () => '',
};
const d: RouteConfig = { // should break
children: '',
path: '',
component: () => true,
};
const e: RouteConfig = { // should break
children: '',
path: '',
preload: '',
component: () => '',
};
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Gajus |
