Destructuring intersection types by type in TypeScript

ยท

3 min read

Wow, that's a lot of t's! ๐Ÿ˜…

This is a short post regarding a question I had while writing TypeScript on a React project.

I'm by no means a TypeScript expert, I only know the basics, but while I was writing a component in React and TypeScript, a component which wrapped another third party component, I wanted to be able to use my own component's props and also pass on the other (wrapped) component's props in a type-safe way (at least type-safe at compilation time, you'll see why soon).

The component looked like this:

interface UserButtonProps {
  image: string;
  name: string;
  email: string;
  icon?: React.ReactNode;
}

export default function UserButton(
  props: UserButtonProps & UnstyledButtonProps // UnstyledButtonProps is part of @mantine/core, a 3rd party library
) {
  const { image, name, email, icon, ...others } = props;
  const { classes } = useStyles();

  return (
    <UnstyledButton className={classes.user} {...others}>
    // ...
  );
}

This works, and others has all the properties from the rest of the intersection of UserButtonProps & UnstyledButtonProps, without the image, name, email and icon properties. others becomes its own structural type.

But, then, what if I forgot to destructure icon, which is part of the UserButtonProps? Then others would contain it. This can happen when intersecting more than 2 types too. others would contain properties from two other types, if only the first was destructured completely.

Now, because TypeScript is a structurally typed language, we can define the structure of our objects, even destructured ones, so when I remembered that, I rewrote the above as:

export default function UserButton(
  props: UserButtonProps & UnstyledButtonProps
) {
  const { image, name, email, icon } = props;
  const { ...ubProps }: UnstyledButtonProps = props;
  const { classes } = useStyles();

  return (
    <UnstyledButton className={classes.user} {...ubProps}>
    // ...
  );
}

Now ubProps, which I substituted for others, is not it's own new type, but reuses the UnstyledButtonProps type.

But, there's a gotcha! ubProps is only structurally typed in TypeScript. JavaScript is not aware of the types! That means that const { ...ubProps }: UnstyledButtonProps = props; is just the same as writing const { ...all } = props. all would practically be a copy of props. You can verify this in the Babel Repl or the TypeScript Playground.

So, the above works if you want the TypeScript compiler to throw an error at you when you try to access properties on ubProps that are not defined by the UnstyledButtonProps type/interface. Once compiled (transpiled), the actual ubProps object contains all the properties from the destructured object (props), because that is how destructuring works in JavaScript.

Closing note

The TypeScript type system is not what I'm used to, but in the end perhaps it doesn't matter. For example, even if UnstyledButton gets passed a superset of its props, because that component is also written in TypeScript it will only (be able to) access the properties it defined in its UnstyledButtonProps props type.

That's safe enough for now but I hope JavaScript and TypeScript can improve their type systems to be more sound, in the future.

ย