Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript property does not exist on extended type

I have a react component which either takes the to (react-router-dom prop) or href prop depending on where I want to redirect the user; whether it's within the app or to an external url. Typescript complains about my type with

Property 'to' does not exist on type 'LinkType'.

Property 'href' does not exist on type 'LinkType'.

This is how I defined my type

import React from 'react';
import { Link } from 'react-router-dom';

interface LinkBase {
  label: string;
  icon?: JSX.Element;
}

interface RouterLink extends LinkBase {
  to: string;
}

interface HrefLink extends LinkBase {
  href: string;
}

type LinkType = RouterLink | HrefLink;

export function NavLink(props: LinkType) {
  const { to, label, icon, href } = props;  // TS complains about those to and href

  return(
    {to ? (
      <Link to={to}>{label}</Link>
    ) : (
      <a href={href}>{label}</a>
    )}
  );
}
like image 318
LazioTibijczyk Avatar asked Apr 16 '21 14:04

LazioTibijczyk


1 Answers

Because LinkType is a union, HrefLink and RouterLink are both assignable to LinkType. HrefLink does not have "to" on it, and RouterLink does not have "href" on it. Therefore typescript can not guarantee that either of those properties exists on the union. You can use a type guard to do what you're trying to while also satisfying TypeScript.

import React from 'react';
import { Link } from 'react-router-dom';

interface LinkBase {
  label: string;
  icon?: JSX.Element;
}

interface RouterLink extends LinkBase {
  to: string;
}

interface HrefLink extends LinkBase {
  href: string;
}

type LinkType = RouterLink | HrefLink;

function isRouterLink(v: unknown): v is RouterLink {
  return v && Object.prototype.hasOwnProperty.call(v, 'to');
}

function isHrefLink(v: unknown): v is HrefLink {
  return v && Object.prototype.hasOwnProperty.call(v, 'href');
}

export function NavLink(props: LinkType) {
  const { label, icon } = props; 

  if (isRouterLink(props)) {
    const { to } = props; // typechecks now
    return (<> ... </>)
  }

  if (isHrefLink(props)) {
    const { href } = props;

    return (<> ... </>);
  }

  return (<> ... </>);
}

Another option would be using a "tagged" union to get around the need of a type guard function. These go by some different names depending on who you're talking to. Other names you can find these by are "discriminating union" and "disjoint union" to name a couple.

interface LinkBase {
  label: string;
  icon?: JSX.Element;
}

interface RouterLink extends LinkBase {
  type: 'router';
  to: string;
}

interface HrefLink extends LinkBase {
  type: 'href';
  href: string;
}

type LinkType = RouterLink | HrefLink;

export function NavLink(props: LinkType) {
  const { label, icon } = props; 

  if (props.type === 'router') {
    const { to } = props; // typechecks now
    return (<> ... </>);
  }

  if (props.type === 'href') {
    const { href } = props;

    return (<> ... </>);
  }

 return (<> ... </>); 
}

So then if you inspect the type of LinkType['type'] you'll see 'router' | 'href', and TypeScript can automatically narrow the type for you if you do an if statement on this type property of LinkType.

like image 117
Alex D Avatar answered Nov 09 '22 06:11

Alex D