Skip to content

Type Inference support in JSX with Conditional Types #25157

Closed
@ksaldana1

Description

@ksaldana1

TypeScript Version: Testing in both 2.9.2 && 3.0.0-dev.201xxxxx

Search Terms:
generics, jsx, conditional types

Code
I am trying to write a typesafe wrapper around React-Router's Link component. I would like to make the component generic to my routes map, while providing a clean API for consumers of the component.

If the route has no parameters, I want the component to only require the routeName prop, while routes with parameters require both the routeName and params associated with that route object passed in as props.

import * as React from 'react';
import { Route as IRoute, RouteParams, param } from '../interfaces/types';
import { Link as ReactRouterLink } from 'react-router-dom';
import { route } from '../route';

type LinkWithNoParams<T extends Record<string, IRoute<any>>, K extends keyof T> = {
  routeName: K;
};

type LinkWithParams<T extends Record<string, IRoute<any>>, K extends keyof T> = {
  routeName: K;
  // extracts the "param" values from a route tuple
  params: RouteParams<T[K]>;
};

export type LinkProps<
  T extends Record<string, IRoute<any>>,
  K extends keyof T
> = keyof RouteParams<T[K]> extends never ? LinkWithNoParams<T, K> : LinkWithParams<T, K>;

function hasParams(e: any): e is LinkWithParams<any, any> {
  return e && e.params;
}

export const createLink = <T extends Record<string, IRoute<any>>>(routes: T) => {
  function Link<K extends keyof T>(props: LinkProps<T, K>) {
    if (hasParams(props)) {
      return <ReactRouterLink to={routes[props.routeName].create(props.params)} />;
    } else {
      return <ReactRouterLink to={routes[props.routeName].create({})} />;
    }
  }

  return Link;
};
// Used as our discriminant
enum RouteNames {
  VIEW = 'VIEW',
  VIEW_DETAILS = 'VIEW_DETAILS',
}

const Routes = {
  [RouteNames.VIEW]: route(['view']),
  [RouteNames.VIEW_DETAILS]: route(['view', param('id'), param('otherId')]),
};

const Link = createLink(Routes);

// Conditional Type Sanity Check
type ViewRouteParams = keyof RouteParams<typeof Routes[RouteNames.VIEW]>; // type is never
type ViewDetailsRouteParams = keyof RouteParams<typeof Routes[RouteNames.VIEW_DETAILS]>; // type is "id" | "otherId"
type ViewLinkProps = LinkProps<typeof Routes, RouteNames.VIEW>; // type is { routeName: RouteNames.VIEW }
type ViewDetailsLinkProps = LinkProps<typeof Routes, RouteNames.VIEW_DETAILS>;
// type is { routeName: RouteNames.VIEW_DETAILS, params: { id: string, otherId: string } }


// "correct" behavior
const correct = <Link routeName={RouteNames.VIEW} /> // no error
const correct2 = <Link routeName={RouteNames.VIEW} params={{}} />// params does not exist on type ...
const correct3 = <Link<RouteNames.VIEW_DETAILS> routeName={RouteNames.VIEW_DETAILS} />; // error of "property params is missing in type.."
const correct4 = (
  <Link<RouteNames.VIEW_DETAILS> routeName={RouteNames.VIEW_DETAILS} params={{}} />
); // error of "property id is missing in type..."

// "incorrect" behavior
const SHOULD_FAIL_BUT_DOESNT = <Link routeName={RouteNames.VIEW_DETAILS} />; // expect failure "property params is missing in type..."
const SHOULD_NOT_FAIL_BUT_DOES = <Link routeName={RouteNames.VIEW_DETAILS} params={{}} />;
// property params doesnt not exist on type `IntrinsicAttributes & LinkWithNoParams

Expected behavior:
Link does not require explicit generic argument--can infer based off the routeName prop passed to the JSX component.

Actual behavior:
Link required explicitly passing in generic argument.

Related Issues:
Possibly #23412

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs More InfoThe issue still hasn't been fully clarified

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions