Type assertions can hide type errors in TypeScript
The type checking provided by the TypeScript compiler is a great tool when working with JavaScript. Even more so when dealing with large, long-lived projects with many developers. Still, sometimes it can fail to warn you about a type error. In this post, I describe a simplified case that happened to me recently when doing some large scale code refactoring.
Let us start with the following types to describe a dialog box:
export interface Button {
label: string;
handler: () => void;
}
export interface Dialog {
message: string;
buttons: Button[];
}
export interface ExtendedDialog extends Dialog {
title: string;
}
The following function can display both a Dialog
and an ExtendedDialog
:
export function display(dialog: Dialog) {
// ...
}
Depending on how we call the function, the TypeScript compiler may not detect a type error. It correctly detects a type error when you pass the Dialog
base type to the function (also in the buttons
array):
display({
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: "error" },
{ label: "No", handler: () => {} },
],
});
As expected, the above code fails with the following error:
Type
string
is not assignable to type() => void
.The expected type comes from property
handler
which is declared here on typeButton
The same happens if we pass an invalid instance of ExtendedDialog
:
display({
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: "error" },
{ label: "No", handler: () => {} },
],
title: "Question",
});
But what if we want to make sure that we pass an instance of ExtendedDialog
and not Dialog
? The code above obviously does not care: there is no error if the title
property is missing. Naively, we could try to use a type assertion:
display({
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: "error" },
{ label: "No", handler: () => {} },
],
title: "Question",
} as ExtendedDialog);
However, that would not be a good idea. The above code compiles even though the first item in the buttons
array still has a type error in the handler
property. And not only that. It compiles even if the title
property is not present:
display({
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: () => {} },
{ label: "No", handler: () => {} },
],
} as ExtendedDialog);
According to the documentation, this last scenario is an expected behavior. As I understand it, the invalid type of the handler
property should be reported:
TypeScript only allows type assertions which convert to a more specific or less specific version of a type. This rule prevents "impossible" coercions.
This is also confirmed by the following example:
display({
message: 42,
buttons: [
{ label: "Yes", handler: () => {} },
{ label: "No", handler: () => {} },
],
title: "Question",
} as ExtendedDialog);
Compilation fails with the following error:
Conversion of type
{ message: number; buttons: { label: string; handler: () => void; }[]; title: string; }
to typeExtendedDialog
may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression tounknown
first.Types of property
message
are incompatible.Type
number
is not comparable to typestring
.
So what is a better way to type check an ExtendedDialog
instance when passing it to a function that requires a Dialog
instance? Explicitly declaring a variable of type ExtendedDialog
works just fine. The compiler recognizes the error in the handler
property anyway:
const dialog: ExtendedDialog = {
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: "error" },
{ label: "No", handler: () => {} },
],
title: "Question",
};
display(dialog);
The above code fails to compile with the following error:
Type
string
is not assignable to type() => void
.The expected type comes from property
handler
which is declared here on typeButton
And the compiler also detects a missing property declared only in ExtendedDialog
:
const dialog: ExtendedDialog = {
message: "Do you agree?",
buttons: [
{ label: "Yes", handler: () => {} },
{ label: "No", handler: () => {} },
],
};
display(dialog);
This time the compilation fails with the following error:
Property
title
is missing in type{ message: string; buttons: { label: string; handler: () => void; }[]; }
but required in typeExtendedDialog
.
You can find all the code from the post in my GitHub repository, compile it yourself and further experiment with it.
Sometimes it may be tempting to use type assertions in your TypeScript code. However, you are likely to unintentionally prevent the compiler from detecting certain type errors in your code. In most cases, it's better to use a more appropriate language construct instead, as I did in my case with the explicitly typed variable.