'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 redirectRules is set, then component must not be set.
  • If redirectRules is unset, then component must bet set.
  • If preload is set, then component should be () => Promise<ComponentType<A>>.
  • If preload is unset, then component should be () => Promise<ComponentType<B>>.

I am familiar with unions and tagged unions in . 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.

Playground



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