Skip to content

Type Inference support in JSX with Conditional Types #25157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ksaldana1 opened this issue Jun 22, 2018 · 3 comments
Closed

Type Inference support in JSX with Conditional Types #25157

ksaldana1 opened this issue Jun 22, 2018 · 3 comments
Labels
Needs More Info The issue still hasn't been fully clarified

Comments

@ksaldana1
Copy link

ksaldana1 commented Jun 22, 2018

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

@mhegazy
Copy link
Contributor

mhegazy commented Jun 25, 2018

Can you share the two files involved in the repro as well, or simplify the repro a bit.

@mhegazy mhegazy added the Needs More Info The issue still hasn't been fully clarified label Jun 25, 2018
@ksaldana1
Copy link
Author

ksaldana1 commented Jun 25, 2018

@mhegazy Sorry, that's quite an oversight by me. I am trying to work on a simpler repro right now, but for now here are the two additional files:
../interfaces/types.ts
../route.ts

There's some pretty crazy type-level stuff going on with the UnionToIntersection type, and what I'm trying to do is inherently pretty complicated--so I'm not entirely surprised at the inference engine not being super psyched. I will try to get a more minimal reproduction to simplify this.

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified
Projects
None yet
Development

No branches or pull requests

3 participants