Stack Exchange
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 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.
Playground

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: () => '',
};


You don't need the optional never props unless you require unconditional property access (e.g. before discrimination).
