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.