Type assertions can hide type errors in TypeScript

February 25th 2022 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 type Button

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 type ExtendedDialog may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to unknown first.

Types of property message are incompatible.

Type number is not comparable to type string.

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 type Button

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 type ExtendedDialog.

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.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License