Whenever you start working with a new programming language or framework, you need to learn a new collection of error messages and how to resolve them. Sometimes those messages are obvious, and others only become obvious as you gain experience with the new technology. Here we look at some of the common warnings you might find when you first start working with TypeScript, what they mean, and how to fix the errors within your source code.

Issue: Type fails to narrow

Perhaps you decided to go the extra mile and strongly type a variable, only to have your TypeScript compiler persistently declare that 42 is not in type Answer. You soon realize no matter how many node modules you install or extra development tools and packages you get, your computer will never be capable of deep thought. The error message starts to flicker before your eyes:

Type 'number' is not assignable to type 'Answer'.

It’s strange, because the variable in question definitely looks like it falls within the Answer type:

type Answer = 42 | '42';
const answer = 42;
const result: Answer = answer;

Never fear! The answer is actually quite simple: assigning const answer = 42 implicitly types it as number, even though the specific value could be in type Answer. There are even two easy ways to force answer to be of type Answer: explicitly typing it when it is created, or casting it when it is used.

const answer: Answer = 42;

or

const result: Answer = answer as Answer;

Issue: Flexibly typing objects

You would never describe yourself as a TypeScript “wizard” — that’s just asking for trouble — but you know a thing or two about creating good interfaces. You know where you stand, and just to make a point, you could easily type your position in either two-dimensional or three-dimensional coordinates:

interface Position {
    x: number;
    y: number;
    z?: number;
}

const point2d: Position = { x: 7, y: 6 };
const point3d: Position = { x: 3, y: 2, z: 7 };

Especially with optional properties, you thought errors beginning with Object literal may only specify known properties were a thing of the past. Unfortunately, you have just been called upon to create an interface for all allowed HTML properties, and now you keep running into errors like Object literal may only specify known properties, and 'aria-labelledby' does not exist in type TagProperties for anything that begins with aria-* or data-*. It has dawned on you that it is impossible to directly include all possible tag attributes in your interface, but you’re not sure where to go from there.

Along come index signatures to the rescue! These provide much greater flexibility, but at the cost of reduced control. For example, the following interface will allow the addition of any string property to an object typed TagProperties:

interface TagProperties {
    id: string;
    disabled: boolean;
    tabIndex: number;
    ...
    [key: string]: any;
}

It has the benefit of still allowing a type definition for the value, but the index signature means that any string index may be added to TagProperties, including invalid HTML properties.

Issue: Third-party libraries and ambient type declarations

Realistically, any project with any amount of complexity will use a number of third-party libraries, not all of which will necessarily be written in TypeScript. As per usual, the highest number of new grey hairs comes from trying to coax either tools or third-party libraries to play nicely with each other. Luckily, choosing TypeScript need not contribute to rising sales of hair dye or gin.

The error itself will probably look something like this:

Could not find a declaration file for module 'moduleName'. '/path/to/moduleName/lib/api.js' implicitly has an 'any' type.

First check DefinitelyTyped, where typings exist for nearly all common third-party JavaScript libraries. Assuming you can find the one you wish to use, install it with:

npm install @types/moduleName --save

This is all that is required to make those pesky declaration file errors disappear! Sit back and enjoy the immediate savings in wigs and alcohol.

Issue: Conflicting function overloads

Do all the other issues in this post seem obvious and overly basic? Then settle in, double your daily caffeine dosage, and get ready to debug some function overloads.

The first question to ask when creating, editing, or debugging function overloads is “do I really need function overloads?” There’s a decent change that union types or generics might be able to create the same type constraints more intuitively. For example, this:

fooMethod(a: string, b: string): void;
fooMethod(a: number): void;
fooMethod(a: any, b?: string): void;

could just as easily be:

fooMethod(a: string | number, b?: string): void;

A more complex example, where the return type depends on the type of the parameters, could be updated from this:

fooMethod(a: string): string;
fooMethod(a: number): number;
fooMethod(a: any): any;

to this:

type FooA = string | number;
fooMethod<T extends FooA>(a: T): T;

However there are certainly cases where overloading is necessary, primarily when multiple parameters have interdependent types. In these cases, you may encounter the following error after about three cups of coffee:

Overload signature is not compatible with function definition

This refers to the fact that the implementation signature (also known by its more technical term, “the last one”) must be compatible with all the overloaded type signatures. For example, the following would create the above error for the second overload:

function fooMethod(a: string): string;
function fooMethod(a: number): number;
function fooMethod(a: any): string {
	return 'foo';
}

This is because the return type number is not compatible with the last return type of string. It could be solved by converting the last type signature to fooMethod(a: any): any or fooMethod(a: any): string | number.

Issue: <any> or: how I learned to stop worrying and love type casting

TypeScript’s version of nuclear proliferation is a codebase liberally sprinkled with <any> or as any. Sure, everything could turn out fine, but it will likely attract a bevy of concerned experts placing bets on what will explode first.

Type casting (or, more acurately, type assertion) is powerful because it forces the compiler to interpret the value in question as the provided type. This can be dangerous because you will no longer see type errors when you have missed something:

interface MovieCast {
    strangelove: string;
    president: string;
    captain: string;
}
const cast: MovieCast = {} as MovieCast;
// hmmm.... forgot anything?

On the other hand, there will always be edge cases and sometimes brute force really is the best solution. Here are two common situations when casting may be the best option:

  • You know a DOM query will return an input element, but TypeScript doesn’t (and can’t).
      // It is necessary to cast to HTMLInputElement, or 'value' will not be available on inputElement
      const inputElement = document.querySelector('.input') as HTMLInputElement;
      const value: string = inputElement.value;
      
  • You need to stub a simpler version of a complex interface for testing

Other error messages?

What error messages do you commonly run into? Share your thoughts in the comments, and we’ll update this post periodically.

Or if you are stuck on a particularly thorny TypeScript problem, and want to avoid the temptation to just put an

<any>

type on it, contact us to learn how we can help.