Categories
Uncategorized

Common TypeScript errors and how to fix them

At work we recently changed the builds for our project to fail when new TypeScript errors are added, so I took some time to clean up a bunch of old TS errors in our codebase, some of which have been there for over 5 years. There were close to 600 errors when I started (down to about 30 now), and I’ve found that most of them fall into a few general categories. I’m not a TS expert, but I thought I’d write this post to explain the most common issues I’ve seen and suggest ways to solve them in case you come across similar in your own code.

Finding and reading TypeScript errors

First, let me explain how to find and read errors, because this was not obvious to me when I started working with TypeScript. Some folks may already see errors show up in their editor if you’re running the TypeScript language server, but if you want to list errors explicitly, you can run the CLI command tsc -p your-project/tsconfig.json --noEmit (you may need to prefix this command with either yarn or npm run depending on your local environment).

It’s important to note that unlike linters, the TypeScript compiler has to run on a whole project; it cannot be used to look for errors in just one file.

Reading TypeScript errors can be daunting. They are often filled with so much information that it can be very difficult to even understand what TS is complaining about, let alone how to fix it. The first thing to keep in mind is that often TS will put the actual problem somewhere near the end of the error message, so look there first.

Argument of type '{ name: string; title: string; flavor: "green"; }' is not assignable to parameter of type '{ name: string; title: string; dogs: number; flavor: "grape" | "green"; beverage: "coffee" | "tea"; }'.
  Type '{ name: string; title: string; flavor: "green"; }' is missing the following properties from type '{ name: string; title: string; dogs: number; flavor: "grape" | "green"; beverage: "coffee" | "tea"; }': dogs, beverage

The above error is saying, “you’re passing an object to a function, but the object is missing two properties: dogs and beverage“. Notice that the useful information is right at the end. I usually read TS errors from the bottom up.

Many errors come down to TypeScript saying,

  • “I don’t know what type of data this is”
  • “this object is supposed to have a property, but it’s not here”
  • “you are passing an object with a property to a function which doesn’t know about that property”
  • “this variable or property is supposed to be of type X, but you’ve provided a value of a different type”. (This last one can often be the most confusing, because types themselves can sometimes be really complex.)

Let’s look at some specific examples.

Handling falsy variables

Probably the most common error I see is something like this:

const username = getUsername();
doSomethingWithUsername( username ); // Error: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'.

This happens when the data comes from a place (in this case, getUsername()) that has a chance to fail (it might return a falsy value like undefined or null) and you’re passing that data to a function (here, doSomethingWithUsername()) which requires a non-falsy value (here, a string).

In this case, we can solve the error by handling the failure case. Sometimes that means reporting the error to the user, but often we know that such failures are temporary (eg: during the first render of a React component before data has loaded) so we could simply provide a fallback:

const username = getUsername();
doSomethingWithUsername( username ?? '' );

Another common fix for that issue is to alter the function you’re calling so that it accepts falsy values and handles them itself; in this case changing the parameter type from string to string | undefined | null and then adding a guard:

function doSomethingWithUsername( username: string | undefined | null ): void {
  if ( ! username ) return;
  // ...
}

Also, depending on your context, you can add early returns or other guards to make sure that your data exists before using it:

const username = getUsername();
if ( username ) {
  doSomethingWithUsername( username );
}
// or...
const user = username ? getUser( username ) : undefined;

Remember that falsy values are not all alike. You might know that undefined, null, false, '', and 0 are all false in some context, but TS requires you to be precise. You may see an error like this:

doSomethingWithUsername( username ); // Argument of type 'string | undefined' is not assignable to parameter of type 'string | null'. Type 'undefined' is not assignable to type 'string | null'.

That error is saying “your variable is either a string or undefined, but you are trying to pass it to a function that accepts a string or null, and null and undefined are different”.

Solving this error is usually as easy as either changing the function definition to accept more falsy types, or providing a fallback in the accepted type:

doSomethingWithUsername( username ?? null );

Related to the above, we often gloss over implicit type conversions in JavaScript that TS will not allow.

function doThing(x: string|number|undefined) {
	return x > 12; // Object is possibly 'undefined'.
}

While JS is fine with the expression undefined > 12, TS prefers you to be precise:

function doThing(x: string|number|undefined) {
	return x && x > 12;
}

And keep in mind that binary shortcuts like && and || don’t always do what we assume. We can usually consider them to return a boolean but they may not. Notice what happens if we put a return type on this function.

function doThing(x: string | number | undefined): boolean {
	 return x && x > 12; // Type 'string | number | boolean | undefined' is not assignable to type 'boolean'. Type 'undefined' is not assignable to type 'boolean'.
}

To fix this you may have to explicitly cast your implied boolean value to an actual boolean.

function doThing(x: string | number | undefined): boolean {
	 return Boolean( x && x > 12 );
}

Another example of using non-booleans as booleans is the Array.prototype.filter function. A common way to remove falsy values from an array is to filter it through the Boolean function (or an identity function), but for some reason TypeScript isn’t smart enough to know that this removes the falsy values.

const data = ['one', null, 'two'];

data
.filter( Boolean )
.forEach( ( x: string ) => console.log( x ) ) // Argument of type '(x: string) => void' is not assignable to parameter of type '(value: string | null, index: number, array: (string | null)[]) => void'. Types of parameters 'x' and 'value' are incompatible. Type 'string | null' is not assignable to type 'string'. Type 'null' is not assignable to type 'string'.

Instead we need a type guard (see more on them below) to explicitly tell TypeScript that the falsy values are being removed. We have one in our project called isValueTruthy.

import { isValueTruthy } from '@automattic/wpcom-checkout';

const data = ['one', null, 'two'];

data
.filter( isValueTruthy )
.forEach( ( x: string ) => console.log( x ) )

Something is not a string

Another common error I saw is when a React component expects a prop to be a string, and someone provides a React element instead.

function NameBlock({name}: {name: string}) {
    return <div>Name: {name}</div>;
}

function MyParent() {
    return <NameBlock name={ <span>Person</span> }/>; // Error: Type 'Element' is not assignable to type 'string'.
}

In this case, it’s actually ok to for the variable to be a React component, even though the original author may not have considered that. If the variable is going be rendered as a React child, we can change the function definition to accept ReactNode instead, which includes both strings and components.

import type { ReactNode } from 'react';

function NameBlock({name}: {name: ReactNode}) {
    return <div>Name: {name}</div>;
}

function MyParent() {
    return <NameBlock name={ <span>Person</span> }/>;
}

A variation of this issue which comes up sometimes is when you have a number that you want to pass to something that is looking for a string, or vice versa.

function doSomething( phone: number ) { /* ... */ }
const userInput = '1234567890';
doSomething( userInput ); // Argument of type 'string' is not assignable to parameter of type 'number'.

In that case you can usually change the type explicitly by using functions like String() and parseInt().

function doSomething( phone: number ) { /* ... */ }
const userInput = '1234567890';
doSomething( parseInt( userInput, 10 ) );

Incorrect inference from JavaScript

TypeScript tries its best to guess the types of code that’s in an imported JavaScript file. Sometimes it gets this wrong. A pretty common instance of this is when a function has optional parameters.

// In a JS file:
function doSomethingOptional( name ) {
	if (name) { console.log( name ) }
}

// In a TS file:
doSomethingOptional(); // Expected 1 arguments, but got 0.

In this case, we need to convince TS that the parameter is optional. One quick way to do this is to set a default value.

// In a JS file:
function doSomethingOptional( name = '' ) {
	if (name) { console.log( name ) }
}

// In a TS file:
doSomethingOptional();

Another way to do this is to use JSDoc to define the types, a technique that is nearly as powerful as using TypeScript directly. To mark a variable as optional in JSDoc, put square brackets around its name.

// In a JS file:
/**
 * @param {string|undefined} [name] the name
 */
function doSomethingOptional( name ) {
	if (name) { console.log(name) }
}

// In a TS file:
doSomethingOptional();

A second common problem is when JSDoc has already been used but isn’t very specific.

// In a JS file:
/**
 * @return {object} something probably
 */
export function doSomething() { /* ... */ }

// In a TS file:
const data = doSomething();
data.name = 'person'; // Property 'name' does not exist on type 'object'.

The solution here is to improve the JSDoc. Again, you can use most TS syntax (although if you run into a problem you can also use Google Closure syntax, like using Object.<string, string> instead of Record<string, string>).

// In a JS file:
/**
 * @return {{name: string}} something probably
 */
export function doSomething() { /* ... */ }

// In a TS file:
const data = doSomething();
data.name = 'person';

You can also import and use types from other files by using import() within JSDoc.

// In a JS file:
/**
 * @return {import('./my-types-file').DataWithName} something probably
 */
export function doSomething() { /* ... */ }

// In a TS file:
const data = doSomething();
data.name = 'person';

Finally, it’s worth mentioning that TypeScript can also infer types (in most cases) from React PropTypes, if they’re set on a non-TS React component. So if you see an error about some prop type being wrong on a JS component used in a TS file, check the PropTypes of the component!

Using Array.prototype.includes on constant arrays

There’s a bunch of code in our project that defines arrays of strings as constants. That’s very helpful for knowing what values are allowed, but it makes it difficult to use includes() on the array.

const NUMBERS = <const>[
	'one',
	'two',
];

function isValid(num: string): boolean {
	return NUMBERS.includes( num ); // Argument of type 'string' is not assignable to parameter of type '"one" | "two"'.
}

Basically TS is saying, “we don’t know if your random string is in the array”, which is of course the whole point of using includes(). This is hotly discussed in TS circles, apparently, but for our purposes we have to convince TS that it’s ok to do, which means either claiming that the variable is part of the array or that the array is made of strings. Here’s the latter solution:

const NUMBERS = <const>[
	'one',
	'two',
];

function isValid(num: string): boolean {
	return ( NUMBERS as ReadonlyArray< string > ).includes( num );
}

Using objects as dictionaries

In JS we use objects for all sorts of purposes, and one popular technique is to use an object as a dictionary of key/value pairs where we don’t know the keys ahead of time.

const data = {};

function addValue( key: string, value: string ) {
	data[ key ] = value; // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. No index signature with a parameter of type 'string' was found on type '{}'.

}

You have to explicitly tell TypeScript that the object is going to be used as a dictionary, which it calls a Record. You also have to specify the types of the object’s keys and values using “generics” (see more about these below); the first generic in Record is the key type (string) and the second is the value type (also string), so the type becomes Record<string, string>.

const data: Record< string, string > = {};

function addValue( key: string, value: string ) {
	data[ key ] = value;
}

Key types are nearly always strings, but if you don’t know what the value type is going to be, you can usually use the special type unknown. The downside is that you may have to further clarify the type later on, but it’s safe to do so (and much better than using the any type which you should probably avoid because it defeats the purpose of using TypeScript).

const data: Record< string, unknown > = {};

function addValue( key: string, value: string ) {
	data[ key ] = value;
}

I don’t know the type

There’s a big category of JS code that operates on any sort of data, which is often hard to express in TypeScript. There’s a lot to say about this, but for now we’ll just examine a very simple case, the identity function. As pure JavaScript, it will cause a type error.

function identity( value ) { // Error: Parameter 'value' implicitly has an 'any' type.
	return value;
}

But how can we add types for this, when we don’t know what the parameter is going to be? As mentioned above, you may be tempted to use the type any, but this is a trap as it opts out of TypeScript altogether. There’s a better way: this is precisely what “generics” are for. You can use them as variables within TypeScript code, separate from JavaScript variables. Instead of numbers and strings, they hold types.

We know that the return type of the identity function is the same type as its argument, so we can create a variable for that type – a generic – and then use that same variable as the return type. Generics are often single capitalized letters, but they can be words just like JS variables.

function identity< T >( value: T ): T {
	return value;
}

Here we’re creating a new generic called T. We then say that the type of the function’s argument is of that (variable) type. This allows us to say that the function returns a value of that same type. We’ve fully typed the function without even knowing what the data is! In most cases, TS can infer the types of generics from their call sites.

identity( 'hello' ); // TypeScript automatically infers that T is a string here.
identity< number >( 25 ); // Or you can explicitly set the value of the generic.

identity< number >( 'hello' ); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.

The signature of the Record type described above (simplified) is Record< Key, Value >. There are other “Utility types” which can do all sorts of things; check them out!

Another common example of using a generic is the React useState hook. The hook can store and return data of any kind, so how does it know the type of data it holds? It defines a generic.

const [ state, setState ] = useState( 'value' ); // TypeScript infers that state is a string.
setState( 'other value' );

const [ state, setState ] = useState< number >(); // You can explicitly set the value of the generic.
setState( 'other value' ); // Error: Argument of type '"other value"' is not assignable to parameter of type 'SetStateAction<number | undefined>'.

Of course, sometimes you really have no idea what’s in a variable and you don’t want to make any assertions about it, such as the data returned from an HTTP API call. For those cases, you can use unknown, which forces any code that tries to use that variable to first verify its type.

function getData(): unknown { /* ... */ }

const data = getData();
console.log( data.name ); // Error: Object is of type 'unknown'.

There’s a bunch of ways to assign proper types to unknown variables, usually called “type guards” (worth reading about for other reasons!). Unfortunately asserting unknown is not as easy as it could be.

Here’s one way to resolve the error by using the as keyword, but be very careful with this technique because you can easily use it to lie to TS and cause bugs. This keyword tells TS to change the type of a variable. Notice that I told TS the data might be undefined and I have an if statement to guard against the assumption I’ve made.

function getData(): unknown { /* ... */ }

const rawData = getData();
const data = rawData as { name: string } | undefined;
if ( data && data.name ) {
	console.log( data.name );
}

Here’s a similar version which uses typeof and an inline as. Always consider the JS code that will be produced when compiled. In this example, if typeof data is “object”, and it is not falsy (null is also an object), we can safely evaluate data.name even if we are wrong about the type we put on the right hand side of as. Use this powerful keyword with care.

function getData(): unknown { /* ... */ }

const data = getData();
if ( data && typeof data === 'object' ) {
	console.log( ( data as { name: string } ).name );
}

Optional component props

Sometimes an author will create a React component with typed properties, but forget to specify which ones might be optional. This happens a lot when converting from PropTypes to TypeScript because PropTypes are optional by default. It’s also very common on boolean types because we assume that false will be the default (undefined will be the default, which is falsy but not the same).

function MyComponent(
  { name, isImportant }: { name: string, isImportant: boolean }
) { /* ... */ }

function MyParent() {
    return <MyComponent name="human" />; // Property 'isImportant' is missing in type '{ name: string; }' but required in type '{ name: string; isImportant: boolean; }'.
}

The solution here is to change the prop type to be optional by appending a question mark.

function MyComponent(
  { name, isImportant }: { name: string, isImportant?: boolean }
) { /* ... */ }

function MyParent() {
    return <MyComponent name="human" />;
}

Types are powerful but unforgiving

In many TypeScript projects it’s still possible to create non-TypeScript files, so if you’re feeling uncomfortable making a TS file, just create a JS file instead. However, even if you do, you may want to keep the types of your data in mind and perhaps add JSDoc comments to help your future self.

In general, we always write code with types in mind, even if they’re unconscious assumptions; TypeScript is merely a means of making our assumptions explicit. Often that can be annoying because it forces us to drag them out of our thoughts, deal with contradictions, and figure out what syntax to use to coerce those assumptions into code, when all we really want is to add a feature or fix a bug.

The trade-off, however, is that many mistakes we might otherwise make are rendered impossible. Perhaps more importantly, no other developer (nor our future selves) will ever be able to accidentally break our assumptions. Using TypeScript is a document of how our code is meant to be used and a guard around the logic we write to be sure that it is used as intended. TypeScript requires you to be precise, but it pays you back with stability. I hope it serves you well.

Photo by Randy Fath on Unsplash

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s