Issue
It's very easy to check for exhaustiveness of sum types in TypeScript.
type Sum =
| { tag: 'num'; value: number }
| { tag: 'str'; value: string };
const len = (sum: Sum): number => {
switch (sum.tag) {
case 'num': return sum.value;
case 'str': return sum.value.length;
default: {
const unhandled: never = sum;
throw new Error(`Unhandled sum ${unhandled}`);
}
}
};
Now, if I add a new variant to the Sum type then sum will no longer be assignable to unhandled. Hence, we'll get a compile time error for non-exhaustiveness.
How can I do the same for product types in TypeScript? Consider the following example.
type Product = {
num: number;
str: string;
};
const repeat = (product: Product): string => {
const { num, str } = product;
return str.repeat(num);
};
Now, if I add a new property to the Product type then I want the TypeScript compiler to report an error for non-exhaustiveness, because the new property hasn't been de-structured and used. How do I do that?
Plus points if the code throws a runtime error for non-exhaustiveness.
Solution
Let's start by throwing a runtime error for non-exhaustiveness. We can do this by destructuring the rest properties, and throwing an error if it has one or more enumerable keys.
const repeat = (product: Product): string => {
const { num, str, ...props } = product;
if (Object.keys(props).length > 0) {
throw new Error(`Unhandled props ${props}`);
}
return str.repeat(num);
};
Next, in order for TypeScript to check for exhaustiveness at compile time we can do the following.
const repeat = (product: Product): string => {
const { num, str, ...props } = product;
const unhandled: {} extends typeof props ? {} : never = props;
if (Object.keys(unhandled).length > 0) {
throw new Error(`Unhandled props ${unhandled}`);
}
return str.repeat(num);
};
Here's how it works.
- The empty object type
{}is only assignable totypeof propsiffpropsis an empty object. - Hence, when
propsis an empty object then the type ofunhandledis{}and everything is all right. - However, when
propsis not an empty object then the type ofunhandledisneverand we get a compile time error.
Thus, the above code will check for exhaustiveness of the product type at the time of de-structuring. If a new property is added to the Product type then props will no longer be assignable to unhandled and we'll get a compile time error for non-exhaustiveness.
Additionally, you can turn on the @typescript-eslint/no-unused-vars rule to make sure that all the de-structured properties are used. Make sure to set the ignoreRestSiblings option to false.
Answered By - Aadit M Shah
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.