Joshua's Cheatsheets - TypeScript Cheatsheet, Common Errors, and More
Light
help

Resources

The Ultimate resource - Basarat's Guide. Almost no point in using any other cheatsheet!

Other:

Global install

npm install typescript -g

Quick init (create tsconfig.json)

tsc --init

Getting around index issues

A common issue with TS is index methods, for example, with the global window object. If an object has no declared index signature, TS can't know the return type when accessing by a key, so any is inferred (details). In certain environments, trying the follow code will throw an error:

window['globalVar'] = true;

Element implicitly has an 'any' type because index expression is not of type 'number'.ts(7015)

If you are OK with less strict type checking, this is usually easily fixed by adding "suppressImplicitAnyIndexErrors": true to the tsconfig.json file.

For keeping strict type checking, you have a few options:

Option A) Merging Interfaces (safest)

Merging interfaces is the safest way to do this, because it preserves the original window in its full form (as defined in lib.dom.d.ts) and then augments it with your additions.

You can do this by using the following:

// If using an ambient declaration file only for types
interface Window {
	myProperty: string;
	myMethod: () => {};
}

// If your ambient declaration file uses `import` or `export`, you need to wrap with `global`:
declare global {
	interface Window {
		myProperty: string;
		myMethod: () => {};
	}
}

Option B) Individual casting to any

In each place where you reference window (or the object with an unknown index signature), you can cast to any:

(window as any)['globalVar'] = true;

Option C) Explicit index by any-string signature on interface

Warning: This whole option (adding a generic index signature) is less safe than any of the above approaches, but lets you touch window in a very ad-hoc way without having to explicitly define each property.

As outlined in this SO answer, the usual solution is to simply tell TS about the index signature:

interface Window {
	[index:string]: any;
}

Note [key:string] is the same as [index:string] - TS actually doesn't care about the word used; the syntax is what matters.

Note: You might be wondering why we don't also declare [index:number]. This is because JS actually only uses a string index on objects! It will call .toString() automatically on any index you pass!

However, in some environments, this still might not satisfy TS. Another thing you can try is explicitly merging the interfaces. Like so:

interface Window {
	[index:string]: any;
}
declare var window:Window;
window['globalVar'] = true;

Although it might seem counter-intuitive at first, this is actually be a less sound solution than just casting window to any before picking off a value. The reason that here, you are polluting the global Window interface, and suddenly every window property becomes any, regardless if TS already knew a type beforehand.

One in-between solution is to merge the interface, but only on a new variable interface, thus leaving the global window alone:

interface WindowAsAny extends Window {
	[index:string]: any;
}
var windowAsAny:WindowAsAny = window;
windowAsAny['globalVar'] = true;

Or, if you wanted to take advantage of ambient declaration files to make this available in any TS file, you could do something like...

*.d.ts file:

declare interface WindowAsAnyType extends Window {
	[index:string]: any;
}

declare var windowAsAny:WindowAsAnyType;

unknown Type (vs any)

Introduced with version 3.0 of TS, unknown has been growing in popularity, and for good reason; it is a much safer alternative to using any for a type that is unknown.

Short Summary

In the shortest explanation, both any and unknown can accept any value, but unknown can only be assigned to other unknown variables or any variables - assigning it to an actual type requires a type guard to make it not unknown.

Practical Application

In practical use, whenever a value is unknown, we need to do something to explicitly detect type or else TS will complain. For example:

let myUnknown: unknown;

myUnknown = 'Hello!';

// Throws error
myUnknown.toUpperCase();

if (typeof myUnknown === 'string') {
	// Works, because we explicitly checked type
	myUnknown.toUpperCase();
}

Another use for unknown, which usually should be avoided, is as an escape hatch when TS has the wrong inferred or explicit type. Sometimes this is needed if a third-party library incorrectly cast a return type somewhere:

/**
 * Pretend this is bad third party code,
 * that returns a number, but,
 * tells TS it returns a string
 */
function badFunction(): string {
	const num = 2;
	return num as any;
}

let wronglyTyped = badFunction();

// Works
(wronglyTyped as any)

// Fails: "neither type sufficiently overlaps"
(wronglyTyped as number)

// Works
(wronglyTyped as unknown as number)

Deeper Analysis

An easy way to start thinking about unknown is first thinking about the danger of any. The any type says two very important things:

  1. Anything is assignable to any
  2. any is assignable to anything

TS basically says that any is a shape-shifter: it can convincingly become anything, on both the inside and outside, and therefore can be accepted anywhere. Maybe that is a good way to think of any, as a creepy imitator that can sneak past checkpoints completely undetected; it should be avoided whenever possible.

Unknown, on the other hand, follows the first rule of any, accepting any value, but breaks the second rule - it is only assignable to any or unknown typed variables.

Continuing our shape-shifter example, this is TS saying that unknown is also a shape-shifter, but only on the inside; on the outside, it has a big question mark on its shirt, and our checkpoints need to stop it and ask for ID to see what it really is.

Blood Types Analogy

A nice fitting analogy is blood types, if you are familiar with those.

  • any is like being both a universal recipient (type AB+) and a universal donor (type O-)
  • unknown is like being just a regular universal recipient (type AB+)

    • Can receive from all others, but can only donate to other unknowns (or any)

Further reading

Object Index Types

TS Docs

Usually, you want to add an explicit index signature, like this:

interface FuncHolder {
	[index: string]: Function
}

^ - This explicitly says that all the object's keys must be strings, and the values should all be functions.

The generic object index would just be [index: string]: any.

An index must be either a string or number.

You can also extend Object explicitly:

interface FuncHolder extends Object {
	[index: string]: Function;
}

Index by any

If you are trying to type a "loose" object, where any string or number can be the key for any value, there are a few ways to do that.

You can add the index signature as part of your interface:

interface Person {
	name: string;
	[k: string]: any;
}

Or, combine with Record:

type Person = Record<string, any> & {
	name: string;
}

// You can also use Record<string, unknown> for better type safety

// This method is handy when combining arguments in functions declarations that combine objects:
function makeUser(person: Record<string, any> & { email: string }) {
	const userId: string = getOrCreateUserId(person.email);
	return {
		userId,
		...person
	}
}

What about a mixed index type of string or number?

Actually, you can just use string as the index type for that, since in JS, numbers are automatically converted/coerced to strings before using as lookup to obj anyways! See TS Docs and this S/O for details.

Only allowing certain keys

You can specify that only certain keys are allowed by doing something like this:

enum StrKeys {
	name,
	state
}
type Person = {
	[K in StrKeys]?: string;
}
const joseph: Person = {
	// Permitted
	[StrKeys.name]: 'Joseph',
	[StrKeys.state]: 'Washington',
	// This will throw an error
	[StrKeys.city]: 'Seattle'
}

Or, different enum type:

enum StrKeys {
	name = 'name',
	state = 'state'
}
type Person = {
	[K in StrKeys]?: string;
}
const joseph: Person = {
	// Permitted
	name: 'Joseph',
	state: 'Washington',
	// This will throw an error
	city: 'Seattle'
}

Only allowing indexing by an original key of the object

Let's say we created an object like so:

const myIceCream: {
	isLowCal: boolean;
	flavor: string;
	slowChurn: boolean;
	servingSizeGrams: number;
} = {
	isLowCal: false,
	flavor: 'Cookie Dough',
	slowChurn: true,
	servingSizeGrams: 60
}

When we access by a constant string, in a simple manner, TS is smart enough to know whether or not the string we are indexing by is an original key of our object. For example:

// This works
myIceCream['isLowCal'] = true;

// This throws
myIceCream['inStock'] = true;

However, if you try something where TS can't directly infer the value of your index string, you will get an error (if using no-implicit-any):

Object.keys(myIceCream).forEach((key, val) => {
	myIceCream[key] = val;
});
// Element implicitly has an 'any' type because expression of type 'string' can't be used to index type...

Although a contrived example, the above is completely valid code, so how do we make it work? Here are some options:

Add a generic index signature

Similar to solutions for other indexing issues, we can simply add a generic index option:

const myIceCream: {
	isLowCal: boolean;
	flavor: string;
	slowChurn: boolean;
	servingSizeGrams: number;
	[index: string]: boolean | number | string;
} = {
	isLowCal: false,
	flavor: 'Cookie Dough',
	slowChurn: true,
	servingSizeGrams: 60
}

Use keyof

An option that is more advanced, but avoids any, is to use the keyof type guard to explicitly state that key is a keyof the type associated with myIceCream.

The cleanest way to do this is to change the type annotation for myIceCream from inline, to a separate interface, that way we can use the keyof type guard. Like so:

interface IceCreamOption {
	isLowCal: boolean;
	flavor: string;
	slowChurn: boolean;
	servingSizeGrams: number
}

const myIceCream: IceCreamOption = {
	isLowCal: false,
	flavor: 'Cookie Dough',
	slowChurn: true,
	servingSizeGrams: 60
};

Object.keys(myIceCream).forEach((key) => {
	const fk: keyof IceCreamOption = key as keyof IceCreamOption;
	console.log(myIceCream[fk]);
});

However, there is a cheat if you don't want to separately declare the interface for myIceCream.... use keyof typeof {value}. This lets you basically use a value as a type, which is normally forbidden in TS:

Object.keys(myIceCream).forEach((key, value) => {
	const fk = key as keyof typeof myIceCream;
	console.log(myIceCream[fk]);
});

Thanks to this S/O Answer.

Here is a good dev.to post on keyof tips.

Issue with assignment when using keyof index on object

A problem with both of the above solutions is that they still don't like assignment. If you try to do:

myIceCream[fk] = value;

You'll get kind of a cryptic error like Type 'string' is not assignable to type 'never'..

This is where it starts to get really complicated, to the point of this not being a very clean solution. This might be a place to start if you are looking to use this.

Cast as any

For a lazy solution, we can just explicitly cast as any, which is basically just telling TS that we are aware we are doing something less safe, and the shape of myIceCream could be anything, including something indexable by key.

Object.keys(myIceCream).forEach((key, val) => {
	(myIceCream as any)[key] = val;
});

Typing while destructuring

There are two main options for declaring types while destructuring. First, lets pretend we have an object that TS can't infer types from, maybe the result of JSON.parse():

let book = JSON.parse(`
{
	"name": "Frankenstein",
	"author": "Mary Shelley",
	"release": 1818
}
`);

If we want to destructure, and grab name, author, etc., one option is with explicit types inline with the assignment:

const { author, name, release }: {
	author: string,
	name: string,
	release: number
} = book;

Another option is to simply tell TS about the structure of the incoming object, so TS can infer types automatically:

interface Book {
	name: string,
	author: string,
	release: number
}

let book:Book = JSON.parse(`
{
	"name": "Frankenstein",
	"author": "Mary Shelley",
	"release": 1818
}
`);

// Since book is properly type annotated with full types, we can safely destructure without accidentally getting `any` as inferred type
const { author, name, release } = book;

Typedef and advanced types

Advanced types docs

If you are describing an object, you probably want to use interface:

interface BasicPerson {
	age: number;
}

If you are trying to set up a named alias for what a complex type looks like, you can use type aliases (docs):

/* Bad - you really should use an interface for this... */
type BasicPerson = {
	age: number;
}
/* This is more normal usage - things like unions, complex types, aliasing long types, etc. */
type Person = ThirdPartyLib.SubClass.SubClass.Enum.Value;
type NullablePerson = Person | null;

From the TypeScript docs: Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.

Function as a type

Although you are free to use Function as a type, such as:

let upperCaser: Function;
upperCaser = (t:string)=>t.toUpperCase();

... often it is recommended to fill in the full function signature as the type (this can reduce implicit issues, as well as prevent errors related to mismatched signatures):

let upperCaser: (input:string)=>string;
upperCaser = (t:string)=>t.toUpperCase();

Strict null checks and query selectors

Strict null checks (e.g. disallowing the assignment of something that could be null to a non-null typed var) can cause issues, and for me, that came up around query selectors, like document.getElementById(). If I'm doing something like writing a SFC, I can be pretty dang sure that an element with the ID I want exists, since I control both the TS and the DOM, but TypeScript does not know that. And it is a lot of extra boilerplate to wrap all those query selector calls in IF() statements and turning strictNullChecks off globally might not be the best solution.

A quick workaround is to use ! as a post-fix expression operator assertion - a fancy way of saying that putting ! at the end of a variable asserts that the value coming before it is not null.

Here is it in action, with code that will not error with strictNullChecks enabled:

let myElement: HTMLElement = document.getElementById('my-element')!;

Including external type definition files (e.g. @types/...)

For telling TSC about external type definitions, there are a few options.

  • Easiest: Control automatic imports through your tsconfig.json file compilerOptions:

    • compilerOptions.types:

      • If compilerOptions.types is not defined, then TSC will auto-import any @types/___ defs from node_modules.
      • If it is defined, it will only pull what you specify. E.g.: types: ["@types/googlemaps"]
    • Details
  • On a per file basis, you can use a "triple-slash directive"

    • This must be placed at the very top of the file
    • Example: /// <reference types="googlemaps"/>
    • Example: /// <reference path="../types.d.ts"/>

Warning: Referencing a globally installed types module via a triple slash directive is currently only supported if you use an absolute path (e.g. /// <reference path="C:/.../@types/azure/index.d.ts" />). By design, The TypeScript compiler does not search the global installed modules directory when resolving typings. See this issue for details.


tsconfig.json

TSConfig In-Depth: Compiler Options

Resources:

Error: No inputs were found in config file (when using glob patterns)

This is kind of a "d'oh!" type error. You'll see this when you setup an empty TS project and specific "include" or "files", but don't actually have any .ts files matched by those glob patterns yet. Make sure:

  • The pattern makes sense (see tsconfig -> details)
  • You have an actual TS file that exists and is matched by the pattern. Create an empty one to remove this error if you don't.

Using Multiple TS Config Files

If you have multiple outputs, such as debug vs production, or want to separate declaration file generation from regular TS -> JS compiling, multiple TS config files can make the task easier.

To use multiple tsconfig.json files, just point the compiler at the one you want to use each time, via the --project or -p flag.

For example: tsc --project production.tsconfig.json

Pro-tip: If you create a *.tsconfig.json file, add "$schema": "https://json.schemastore.org/tsconfig" as a top level property so you get intellisense 🤓

TSC / TSConfig Gotchas and Tips

  • There is no --include option on the CLI to match the config file; you pass the file(s) as the last option to tsc, optionally with some wildcards

    • E.g. npx tsc src/*.ts
    • You can pass more than one file at a time: npx tsc fileA.ts fileB.ts ...
    • Glob support appears to be very limited, and quoted patterns do not work
    • It sounds like it might use your terminal for expanding patterns, instead of handling it itself...
  • Rather than dealing with lots of messy CLI options for multiple runs in the same project, it is often far easier to just create multiple configs.

    • See section above for tips
    • You can even create multiple configs that extend a base config, using the extends feature
    • For example, this is an easy way to split up compile options for different sets of files (e.g. src vs __tests__);

      • files, include, and exclude will always override when extending a config

'this' implicitly has type 'any' because it does not have a type annotation.ts(2683)

You will most likely get this error when passing in anonymous function as an argument to another function (aka lambda). For example, in trying to bind this to a click listener, this code will throw that error:

let targetElem: HTMLElement = document.getElementById('streetView')!;
targetElem.addEventListener('click',function(){
	this.myMethod();
}.bind(this));

How do we get around this? If you can't annotate the type of this explicitly (it can be complicated!), the easiest solution is just to not use bind, and instead use an arrow function, which automatically lexically binds this to the outer closure:

let targetElem: HTMLElement = document.getElementById('streetView')!;
targetElem.addEventListener('click',() => {
	this.myMethod();
});

Details


Type annotating object literals

If you have a lot of objects with a repeated structure, obviously the best solution is to write a common interface (DRY 🧠). But what if you just want to quickly use an object literal, such as when returning an object from a function? Of course, you could just use the normal object literal without using types, and TS will infer them, but what if you want type annotations?

Option A) Multi-part declaration

let user: {
	name: string,
	openTabs: number,
	likesCilantro: boolean
} = {
	name: 'Joshua',
	openTabs: 81,
	likesCilantro: true
};

Option B) Cast prefix operator

let user = {
	name: <string> 'Joshua',
	openTabs: <number> 81,
	likesCilantro: <boolean> true
}

Option C) Cast as operator

This is required with TSX (JSX), since Option B will not work.

let user = {
	name: 'Joshua' as string,
	openTabs: 81 as number,
	likesCilantro: true as boolean
}

Enums

A reminder about how enums work in TS (ignoring string enums for a second):

Enums are converted in TS to an object with both string and numerical keys (so value can be looked up by either value or key):

enum colors {
	'red',
	'green',
	'blue'
}
// becomes, at run-time
colors = {
	0: 'red',
	1: 'green',
	2: 'blue',
	red: 0,
	green: 1,
	blue: 2
}

Casting Enum Option to String

When you reference an enum value by key, TS returns a number. To get the string, you actually feed the number back into the number, to lookup the string by number key.

enum colors {
	'red',
	'green',
	'blue'
}
let colorGreenString:string = colors[colors.green];
console.log(colorGreenString);
// > green

Casting string to enum option

Assuming a string based enum, we might want to take an input string and check if matches an enum key, or cast to get the integer value that corresponds. How do we do this?

Since an enum is mapped by both string and numerical index, this can be pretty easy. Essentially we are looking up the integer based on the string key. The casting of the lookup string to "any" might not be necessary depending on your TS settings.

enum colors {
	'red',
	'green',
	'blue'
}
let colorBlueEnumVal:number = colors['blue'];
console.log(colorBlueEnumVal);
// > 2

The above isn't always a working solution, especially when dealing with noImplicitAny. This seems to be the most accepted solution at the moment:

enum colors {
	'red',
	'green',
	'blue'
}
let colorBlueEnumVal:number = colors[('blue' as keyof typeof colors)];
console.log(colorBlueEnumVal);
// > 2

Comparing Enum Values

If we just want to check if a string is equal to an enum value or not, we can use the trick above to convert our enum to a string and then compare:

enum colors {
	'red',
	'green',
	'blue'
}
let pick = 'green';
let match = pick === colors[colors.green];
console.log(match);
// > true

Of course, you could also convert on the key side - however, this tends to be a little trickier, especially with noImplicitAny:

enum colors {
	'red',
	'green',
	'blue'
}
let pick = 'green';
let match = colors[pick as keyof typeof colors] === colors.green;
console.log(match);
// > true

Get Typescript Enum as Array

Since enums are stored as an objet with both string and numerical keys (see warning above) you need to filter the object while converting it to an array, so you don't get mixed duplicates (ref). Two options for converting a TypeScript enum to an array:

Enum to Array - Filter on keys

function enumToArr(enumInput){
  return Object.keys(enumInput).filter(key=>isNaN(Number(key))===true);
}

Enum to Array - Filter on Values

function enumToArr(enumInput){
  return Object.values(enumInput).filter(val=>isNaN(Number(val))===true);
}

String Enums

A string enum looks like this:

enum Books {
	WizardOfOz = 'wizard-of-oz',
	AliceInWonderland = 'alice-in-wonderland'
}

One of the problems with string enums in TS is that they do not get an automatic reverse-mapping generated for them at runtime, unlike other enum types. So, if I have a string and want to look up the enum key, I'm going to run into some issues...

Here are some options open to us:

  • Use a union type instead - this is my personal preference whenever possible, as these are often very readable as well - s/o
  • Build the enum as a plain JS object instead, and skip TS - s/o
  • Build our own reverse mapping, keep TS enum - s/o

String Enums - Comparing Values

For comparison, as opposed to a reverse lookup, things are a little easier. Basically, all you need to use is {string value corresponding to enum key} as EnumName.

Here is a full example, using the Books enum from above:

function getRandomBook() {
	return Math.random() < 0.5 ? 'wizard-of-oz' : 'alice-in-wonderland';
}

const pick: Books = getRandomBook() as Books;

if (pick === Books.AliceInWonderland) {
	console.log('Down the rabbit hole!');
}
else {
	console.log("I don't think we're in Kansas anymore...");
}

TypeScript Enums - Further reading:


Dealing with Legacy JS, Ambient Declarations, Errors, "Oh My!"

If you are dealing with a large TS project, chances are that at some point you are going to need to integrate with parts (packages, components, random snippets, etc) that are vanilla JS and do not have an already made @types declaration package (hint hint, maybe you can make one!).

Could not find a declaration file for module '___'

You have a few options here, but the easiest is usually to throw "ambient declarations" in a `.d.ts` file.

Within a declaration file, you declare things that you want to tell the TypeScript compiler about. Note that this actually does not generate any final JS - it really is to avoid type errors and make your coding experience better. You can declare all kinds of things; variables, classes, etc.

Tip: Make sure allowJs is set to true in your tsconfig.json if you are importing JS!

* 📖 - This (post from Atomist) is a great summary of all the different options for addressing TS7016 / "Could not Find Declaration File" error

Legacy JS Ambient Declaration File - Example:

Let's pretend we have a legacy JS file we need to import into TS.

lib/legacy-javascript.js:

class SimpleClass {
	constructor(yourName) {
		this.name = yourName;
	}
	greet() {
		return "Hello " + this.name;
	}
	static sayGoodbye() {
		return "Goodbye on " + new Date().toLocaleDateString();
	}
}
export default SimpleClass;

export function multiple(inputA,inputB){
	return inputA * inputB;
}

export var creator = 'Joshua';

We can create a *.d.ts file with any name, but to make it easier to find later, we'll follow the same pattern:

legacy.d.ts:

declare module "lib/legacy-javascript" {
	export default class SimpleClass {
		constructor(yourName:string);
		greet(): string;
		static sayGoodbye(): string;
	}
	export function multiple(a:number,b:number): number;
	export var creator:string;
}

Note that you don't have to do it like this, wrapping all your declarations as exports within a declared module - you can use top-level, and use as many declares as you would like. The issue really has to do with scope and file resolution.

Essentially, if you don't use the named module pattern, you are likely to end up polluting the global type scope, which is usually a bad thing. Sometimes it is necessary or helpful though, especially when dealing with injected framework globals.

WARNING: If you add an (top-level) import or an export to your ambient declaration file, it changes from acting like a script (which adds to the global scope) to acting like a module (which keeps local scope). In that scenario, to make types truly global, you have to put wrap them in declare global {}. Thank you to this excellent S/O answer for clearing that up.

Do not use declare global {} wrapper unless you have an import or export in the file.

Ambient Declarations In-Depth

Ambient Declarations - Ambient Module Declarations CommonJS Interop

If you are using ambient module declarations, you need to be particularly careful about import / export syntax, and how default is used.

For example, if you are trying to provide types for a CommonJS library, and the library uses a singular root default export, then you need to make sure you have:

declare module 'js-lib' {
	export = class Main {
		// ...
	}
}

... and NOT export default class Main {}. And the consuming files should use the import Main = require('js-lib'); syntax.

Ignoring these rules will likely result in static type issues, like getting typeof import as the only type, or even runtime exceptions, such as not being able to find the module if you used the wrong import syntax.

Ambient Declarations - Declared Module Not Getting Picked Up Inside Global Module

An issue I've run into in the past is trying to mix import, declare global, and declare module, all within the same ambient declaration file.

The root problem is that when you add an import statement to a *.d.ts declaration file, the following happens:

  • The file stops acting like a top-level script (global), and starts acting like an external module (local). This in turn leads to the following:

    • To make something globally available, without an explicit import, you now have to wrap the declaration in declare global {}
    • If you have ambient module declarations - declare module "my-module" {} - inside the file, they will all of a sudden stop working

Here is the starting example:

// Top level import
import { ChildProcess } from 'child_process';

declare global {
	declare namespace NodeJS {
		interface Global {
			RUNNING_PROCESSES?: {
				server?: ChildProcess;
				watcher?: ChildProcess;
			};
		}
	}
}

// THIS DOES NOT WORK! No effect!
declare module 'my-legacy-package' {
	export function getString(): string;
}

To reiterate, the ambient module declaration in the file has stopped working, and code that tries to use import('my-legacy-package') or require('my-legacy-package') will get the dreaded "could not find a declaration file" error!

There are two ways to deal with this. The first, and easiest, is to simply split up your declarations into multiple files; for example, anything that augments globals goes in globals.d.ts, and anything that is pure ambient module stuff goes in types.d.ts.

A much more complex solution has to do with avoiding top-level imports (). If you move your import statement from the top-level / file scope, to within a declaration section, it keeps the file from turning into that weird module state that requires the use of declare global {}, and allows for mixing global declarations / augmentations with ambient module declarations.

Click to view / hide a full example of how to accomplish this

Here is a specific example. You can go from this:

// Top level import
import { ChildProcess } from 'child_process';

declare global {
	declare namespace NodeJS {
		interface Global {
			RUNNING_PROCESSES?: {
				server?: ChildProcess;
				watcher?: ChildProcess;
			};
		}
	}
}

// THIS DOES NOT WORK! No effect!
declare module 'my-legacy-package' {
	export function getString(): string;
}

To this:

// Notice that by moving the import, we no longer have to wrap with `declare global {}`!
declare namespace NodeJS {
	import { ChildProcess } from 'child_process';
	interface Global {
		RUNNING_PROCESSES?: {
			server?: ChildProcess;
			watcher?: ChildProcess;
		};
	}
}

// This works just fine now!
declare module 'my-legacy-package' {
	export function getString(): string;
}

Thanks to these S/O answers for pointing me in the right direction: 1, 2.

WARNING: Adding ambient module declarations will remove TS errors about missing modules, but not runtime exceptions in the generated JS (if the module is really missing). Make sure you are using the right import syntax, paths, etc.

Ambient Declarations - Common Issue: Not Included in Compilation

Since I've been burned by this multiple times, I want to emphasize that you need to be careful when using ambient declaration files especially if you are coding something that is going to be compiled and/or published. Here is the key to remember:

WARNING: Ambient declaration (*.d.ts) files are, by default, only guides to the compiler; they are not considered for output file generation and do not emit any JS.

Note: This is by design.

Why does this matter? Well, if you are using declaration: true with TSC to generate output declarations for the consumer of your code, types that are only defined within an ambient declaration will be omitted in the generated files (e.g. /dist).

In practical terms, this means if you publish your library as an NPM package, transpiled to JS, types that are only defined via ambient are going to show up as any in the consumers...

How to Include Ambient Declaration Files in Compiled Output

Ironically, the frequent answer to the question of how to get the TSC to copy *.d.ts files to output is... to not write them as *.d.ts ambient declarations!

To clarify, your options are:

  • Rewrite *.d.ts ambient files as regular *.ts files that use export to share types with other files

    • Only downside is that you have to write import statements for types you want to use
    • This is actually how most libraries seem to handle it, often using types.ts or /types/my-type.ts
    • You can use import / export in *.d.ts to turn it into a module, but this isn't really best practice
  • Manually copy your *.d.ts file(s) from src to dist (or whatever your export folder is)

    • A little messy, and could lead to some issues without proper checks in place

Further reading:

Ambient Declarations - Further Reading / Tutorials

Module Resolution

TS docs has a whole page on this topic.

Augmenting external types, modules, etc.

Most third-party type declaration files (e.g. what you would find in @types) will make sure to scope the declaration of types by using namespaces / modules. This is a good thing; it avoids type name collision that would be guaranteed without it.

However, all this use of modules, namespaces, aliasing, etc., all makes augmenting / merging / overriding types rather complicated.

The best place to start is by reviewing the official handbook section on "Declaration Merging".

Some general rules that seem to apply:

  • For declare module "my-lib" {} to work, it must be declared inside another module.

    • This happens automatically if you have a top-level import / export inside your *.d.ts file (remember, imports & exports automatically convert an ambient file into a module file)
  • If you aren't seeing your module augmentation being merged with the original (instead it is just replacing it), make sure you have an import of the original module in the same file
  • Moving import statements inside a nested declare module {} block is actually a valid strategy
  • Namespaces don't merge across distinct modules

    • Either don't place namespace in module, or wrap with declare global {}

Further reading:

Reminder: Don't mix up export patterns

Usually with TS, you are dealing with the ES module pattern for imports/exports. This means named exports and imports, with one default and named exports using export. However, you often see people mixing this up with CommonJS, which uses exports. This can lead to all sorts of issues, especially with ambient declarations:

*.d.ts:

class MyClass {
	//...
}
export = MyClass;
export var myString: string;
// The above line will cause this to fail!

An export assignment cannot be used in a module with other exported elements.ts(2309)

This error actually makes sense, because my code defines a singular export, but then tries to re-assign it from the class to the var export.

If you want multiple exports, just prefix each with export, and you can use export default for the default. If you are going to use CommonJS, don't mix them, and instead define a singular exports object that holds everything you want to export, and then use require() on the other end.

If you are still having issues, the following config options can mess with ambient declaration file resolution and imports:

  • typeRoots
  • types
  • baseUrl
  • paths
  • include

Another thing to remember: module names must be quoted: declare module "widget" {}, not declare module widget {}. Otherwise, you'll get "file is not a module" error

CommonJS Interoperability Issues

To be honest, the whole ES Module / CommonJS / TypeScript interop situation always seems very confusing to me, so I won't pretend to understand it fully. However, I will provide a warning; there are some known issues around this topic.

CommonJS Default Export Interop Issue

If on the CommonJS / require() side of things, you are seeing that the inferred type on an import is just typeof import('.../my-lib') or any, and maybe also seeing something like This expression is not callable [...] ___ has not call signatures when trying to call a function that is a default export, you are likely running head-first into a common interop issue (around default exports).

  • Try the default interop syntax: import myImport = require('my-lib');
  • Try changing const myImport = require('my-lib'); to const myImport = require('my-lib').default;
  • If you are the library author, try changing export default ___ to export = ____ in your type definition file (see links below for context)

    • This also applies if you are the consumer, and using an ambient module declaration

Relevant links:

Another (related) error that you might also see is:

This module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export. ts(2497)

Another version of this error is:

error TS2497: Module '___' resolves to a non-module entity and cannot be imported using this construct

This might even come up as a run-time error, with something like:

UnhandledPromiseRejectionWarning: TypeError: mymodule1.default is not a function

Here are things to try with this error:


Common things I forget / How do I:

  • Declare type for a value returned from a promise?

    • Use generics

      • Promise<string>
  • Default argument for a function

    • Don't use the optional flag - just set equal to:

      function multiply(round = false, inputA, inputB) {
      	const result = inputA * inputB;
      	return round ? Math.round(result) : result;
      }
      multiply(null, 2.5, 1.3);
      // Result will be 3.25

Type Guarding

First, what is type guarding, and how can it benefit us?

Answer:

  • Often, in the code that we write, we introduce areas where the type becomes intentionally ambiguous; the variable could be one of several types

    • For example, we might write a function that returns either an error or a success type:

      • type Checker = () => ErrorResult | SuccessResult;
  • In these instances, if the type is determined at runtime, TS will have trouble statically determining the type inside the function body, since it is indeterminate.

    • A common bypass around this, without using type guards, is to simply cast as the type needed - e.g. (result as SuccessResult).mySuccessMethod()
  • Type guards are:...

    • A way to determinately check a TS type at runtime, and,
    • Tell the TS system about the determined type

Type guarding is often also called, or overlaps with, narrowing type. This just means going from a more broad type definition, such as string | number | boolean, to a more narrow one, such as string | number.

Here are some great resources on type guarding and inference: TS handbook, 2ality.com

Primitive guarding

If your are trying to determine a type, and the possible types are all primitives, we can just use the standard JS typeof operator.

For example:

function speak(input: string | number) {
	if (typeof (input) === 'string') {
		console.log(`Your phrase is ${input}`);
	}
	else if (typeof (input) === 'number') {
		console.log(`Your # is ${Math.floor(input)}`);
	}
}

Without the typeof guard above, Math.floor(input) would cause a type error to be thrown, since it only accepts a numerical input, and without the type-guard, TS can't determine if input is typeof string or number, since we have typed it as a union of both!

Class guarding

TypeScript has a handy built-in for checking if a value is an instance of a class type: instanceof. How handy!

class Res {
	public res: string;
	constructor(res:string) {
		this.res = res;
	}
}

class ErrorResult extends Res {
	public showErr() {
		console.error(this.res);
	}
}

class SuccessResult extends Res {
	public showResult() {
		console.log(this.res);
	}
}

function handleRes(res: ErrorResult | SuccessResult) {
	if (res instanceof ErrorResult) {
		res.showErr();
		return false;
	} else {
		res.showResult();
		return true;
	}
}

Custom guarding

In any scenario where our type is more advanced than a primitive union, class, or anything that can be handled by instanceof or typeof, we need to use some custom guarding logic if we need to tell TS about which type is determined at runtime.

The easy way to do this is with type predicates - that is, a value that explicitly tells TS the type of a thing, with the syntax thing is Type.

Often, this is used be declaring a function that takes an ambiguous input type, and returns a boolean type predicate. For example:

interface Book {
	bookTitle: string;
	author: string;
}
function isBook(thing: Book | any): thing is Book {
	if ('bookTitle' in thing && 'author' in thing) {
		return true;
	}

	return false;
}

The value here is that, in cases where an input to a function or method is a union between something else and a type that can be checked by a custom type predicate, we can use our predicate checker to narrow the type.

Once it is narrowed and TS knows it matches our type thanks to our custom type predicate, we can do things like access input.bookTitle without an error, whereas that would throw an error if TS still thinks the type is a union.

Here is a full example, expanding on the above:

interface Book {
	bookTitle: string;
	author: string;
}

interface Film {
	filmTitle: string;
	director: string;
}

function isBook(thing: Book | any): thing is Book {
	if ('bookTitle' in thing && 'author' in thing) {
		return true;
	}

	return false;
}

function checkoutMedia(media: Book | Film) {
	if (isBook(media)) {
		console.log(`Book ${media.bookTitle} by ${media.author} has been checked out.`);
	} else {
		console.log(`Film ${media.filmTitle}, directed by ${media.director}, has been checked out.`);
	}
}

NodeJS Specifics with TypeScript

Extending NodeJS Globals with TypeScript

The answer to this is almost identical to what I outlined for extending the global window object and getting around index issues.

To summarize, the best option (for keeping type-safety) is to merge interfaces. For NodeJS, this also involves merging the namespace, since that is how Global is typed (NodeJS.Global).

Here is how you can use that approach:

// If in a pure types file (not module):
declare namespace NodeJS {
	interface Global {
		myProperty: string;
		myMethod: () => {};
	}
}

// If in a file that uses `import` or `export`, that is a "module", and you need to wrap the namespace merge with `global` to signify that the change should affect *everywhere* and not just module
declare global {
	declare namespace NodeJS {
		interface Global {
			myProperty: string;
			myMethod: () => {};
		}
	}
}

Also, this S/O answer is a great summary.


Fun advanced types

Using a interface property (nested) as a type

This is so cool! You can re-use a property of an interface as a standalone type, simply by accessing via bracket notation. This is a lifesaver when a third-party library only exports interfaces, but you need to get to a nested type!

interface Person {
	name: string;
	age: number;
	email: {
		address: string;
		vendor: string;
		lastSent: number;
		hasBounced: boolean;
	}
}

// Easy!
type Email = Person['email'];

// You can even grab nested!
type EmailVendor = Person['email']['vendor'];

Thank you to StackOverflow, for the billionth time, for the solution

Using an Array Element as a Type

This is similar to the above trick, as it also uses bracket notation to re-use a nested type.

interface Person {
	name: string;
	computingDevices: ComputingDevices;
}

type ComputingDevices = Array<{
	model: string;
	isActive: boolean;
	lastPing: number;
}>;

// We can reuse array element easily
const compy386: ComputingDevices[0] = {
	model: 'Compy 386',
	isActive: false,
	lastPing: 1100505600
};

// We can even combine with the nested type index trick
const lappy486: Person['computingDevices'][0] = {
	model: 'Lappy 486',
	isActive: false,
	lastPing: 1246345200
};

Any, but exclude undefined or null

What if we want to say that a variable should be basically any primitive except for null and undefined?

There is a long GH issue asking for this as a built-in type: Issue #7648

-- Hackish Solution --

You can very easily just define a union type:

type HasValue = string | number | bigint | boolean | symbol | object;
// Or, if you want to allow null and just check for defined
type Defined = string | number | bigint | boolean | symbol | object | null;

Note: Depending on support, you might want to add or remove bigint as part of the union type. Needing to update primitives is another downside to this approach.


Omit From a Union, Using Exclude

How do you specify a type that is equal to an existing union, but with certain elements removed? Or, put another way, omitting certain elements from a specific union?

With the useful Exclude helper!

It looks like Exclude<Type, ExcludedUnion>, and you can use it like so:

type vehicles = 'boat' | 'plane' | 'car' | 'bus' | 'truck';

type automobiles = Exclude<vehicles, 'boat' | 'plane'>;

Fun fact, the Omit helper, for interfaces, builds on this helper!


Make specific properties (or a single property) optional (as opposed to Partial<T>)

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

Credit: S/O Answer

Restrict Array Values to Keys of (keyof) Interface

Use

interface MyInterface {
	myKeyA: string;
	myKeyB: number;
}
const myArr: Array<keyof MyInterface> = ["myKeyA", "myKeyB"];

Don't use keyof MyInterface[] as the type, because TS reads that keyof Array<MyInterface>, which doesn't make sense.

Use Function Arguments Parameters as Types

This is so cool. You can use the Parameters utility type!

function packageMovie(
	actors: string[],
	pitchDeck: {
		tagLines: string[],
		runtime: number,
		endorsedBy: string[]
	},
	releaseYear: number
) {
	// something something
}

// What if we want to reuse pitchDeck as a type?
// We can! Using `Parameters`!
const myBadMoviePitch: Parameters<typeof packageMovie>[1] = {
	tagLines: ['Why did I just watch that?', 'Waste of time!'],
	endorsedBy: ['no one', 'people against good movies'],
	runtime: 62
}

Use Function Return Value as Type

TypeScript has a handy utility type for extracting the type of a function / method's return - ReturnType<T>. This works for more than just primitive types! However, if T is an actual function, not a type, you need to use it in combo with typeof. For example:

const getFido = () => {
	return {
		name: 'fido',
		says: 'bark',
		favPartOfTree: 'bark'
	}
};

const rufus: ReturnType<typeof getFido> = {
	name: 'rufus',
	says: 'woof'
}
// TS picks up on the return type, and even catches we left out a property:
// > Property 'favPartOfTree' is missing in type '{ name: string; says: string; }' but required in type '{ name: string; says: string; favPartOfTree: string; }'.(2741)

For a function that returns a Promise, such as an async function, extracting the inner type of that returned promise takes a little work and the use of infer:

// https://stackoverflow.com/a/59774789/11447682
type UnpackedPromise<T> = T extends Promise<infer U> ? U : T;

const getValAsync = async () => {
	return 'hello!';
}

const myStr: UnpackedPromise<ReturnType<typeof getValAsync>> = 'Goodbye!'

How to say that a property of T either has a value, or does not exist at all, but is not possibly undefined?

  • This is in contrast to Partial<T> or type Some = {[K in keyof T]?: any}
  • Basically asking for "Partial<T> without undefined"

Sample Input:

interface User {
	name: string;
	username: string;
	id: number;
	age: number;
	nickName: string;
	lastLogin: number;
	hasTwoFactor: boolean;
}

How do we construct a type of BasicUser that specifies that username and id definitely have values, but the others might not?

Option A: Use Pick

This works well, but requires that we explicitly pass in all properties that definitely have a value. Example:

type UserBasic = Pick<User, 'username' | 'id'> & Partial<User>;

Option B: Leave off type declaration

This is counter-intuitive, but explicitly typing something can sometimes lead to a narrowing of types, to where leaving off the type would have actually let something pass through. Here is an example with our User data.

function noUndefined(input: Record<string, string | number | boolean | symbol | object | null >) {
	//
}

// This will trip the compiler - fail
() => {
	const joshuaPartial: Partial<User> = {
		username: 'joshuatz',
		id: 20
	};
	noUndefined(joshuaPartial);
}


// This will pass, even though the actual data is identical
() => {
	const joshuaPartial = {
		username: 'joshuatz',
		id: 20
	};
	noUndefined(joshuaPartial);
}

Downside: In the above example, you are losing a lot of type information for joshuaPartial.


Overloading / Overriding functions and methods

Overloading is a tricky thing in TS, and in general should be avoided whenever possible.

Remember: TS is really a superset of JS, and JS does not support function overloading (if you define a function multiple times, it just takes the last). So, accordingly, there are a lot of restrictions in TS for implementing overloads.

Basic rules of function overloading

This page (by HowToDoInJava) does a great job of explaining when TS will allow overrides, but I'll summarize here as well. For an overload to work, this general rule needs to be true:

The final function declaration, which actually contains the implementation, must be compatible with all the preceding function declarations (in arguments, and in return types).

Knowing this, the general rules you can find about implementing TS function overloading make more sense:

  • All function declarations should have the same name
  • More specific declarations should come before more general ones:

    • E.g. function handler(input: string) should come before function handler(input: any)
  • Combine signatures that differ only by adding additional trailing arguments, by creating one signature with those additional arguments as optional

    // Bad
    function sum(numA: number, numB: number);
    function sum(numA: number, numB: number, numC: number);
    
    // Better
    function sum(numA: number, numB: number, numC?: number);
  • The last very last declaration, the most generic one, should be able to represent the signatures of all those above it

    • This means both arguments and return types

Also, the TS docs has a section on function overloading under their do's and don'ts, which covers some of the above.

Be careful using function overloading; malformed overloads are an easy way to dig yourself into a hole with hard to diagnose bugs and confusing stack-traces (UnhandledPromiseRejectionWarning being one of them)

Method overloading

In general, method overloading (functions in classes) work very similarly to function overloading. However, if you extend a base class, you might run into some interesting issues around overriding methods.

The biggest catch comes with arrow functions (common in React classes):

"Since they are not defined on the prototype, you can not override them when subclassing a component"
    -@oleg008 - Arrow Functions in React

This is also why you can't call members that are defined as arrow function class properties with super when you are subclassing / extending. Actually, in general there are a lot of caveats - see "arrow functions in class properties might not be as great as we think".


Troubleshooting / Assorted Issues

  • Methods that return nullable unions (e.g. string | null) don't seem to be getting checked properly (e.g. always inferred as T, when it should be T | null)

    • Make sure the strictNullChecks compiler option is set to true. This affects both TS and JS checking.
    • This affects JSDoc too, even if you explicitly document a return type as a null union
  • argument of type ____ is not assignable to parameter of type 'never'

    • This most often comes up when trying to push to an array, where the array was created without an explicit type. In those instances, TS will default to never[].

      • To fix for arrays, you can...:

        • Explicitly type the array: const myArr: string[] = []; (this is good for type-safety anyways!)
        • Instantiate the array with myArr = Array() instead of myArr = []
        • Cast before accessing: (myArr as string[]).push('new string!');
        • Set strictNullChecks to false (😢)
    • Most of the other types this comes up is when trying to create unions / merged interfaces, where there ends up being no overlap and a never type is inferred for something.

      • For example:

        const emptyObj = {};
        let propVal: keyof typeof emptyObj;
        propVal = 'hello'; // Type '"hello"' is not assignable to type 'never'
  • How to get the TypeScript compiler (tsc) to stop transpiling (especially with JS input to JS output)

    • Make sure target is set to an ES version that corresponds with how you wrote your source code. For example, if you used async / await, then target needs to be es2017 or higher to avoid transpiling to __awaiter polyfills.
  • Object is possibly 'undefined'.ts(2532), even after check

    • Make sure you are using the right kind of check:

      • Example: Trying to get nested object - if ('myProp' in myObj) actually allows undefined to pass through! change to if (myObj.myProp !== undefined)
    • Workaround: If you are sure that the object exists, you can use the ! (post-fix expression operator assertion), to assert that it does and bypass the error:

      • myObj.myProp!
    • Check this thread for tips
  • TSC is not respecting the rootDir setting with outDir, and producing an output with rootDir nested instead of flat

    • Make sure you are not importing / requiring any files outside the rootDir.

      • This is easy to do accidentally, if you do something like import package.json from {projectRoot}/src.
      • There are some workarounds for this, if you can use a relative path that doesn't change once it outputs to outDir:

        • Easiest: Change ESM syntax to CommonJS - import * as PackageRaw from '../package.json' becomes const PackageRaw = require('../package.json');
        • You can find other workarounds (for example, using multiple tsconfigs) on this S/O
    • TSC does not automatically exclude files outside rootDir for consideration

      • you can manually do this with exclude, include, or files

        • I like include, since it lets you use glob patterns as opposed to files
      • You can check which files are being included by using tsc --listFiles --noEmit
  • Source maps are not getting resolved with debuggers

    • Are you definitely outputting source maps? Make sure sourceMap is on.
    • Manually check the *.js.map files that TSC is emitting
    • Any chance you have any malformed function overloads?

      • I personally ran into a bizarre issue with TSC, where it compiled fine, but threw UnhandledPromiseRejectionWarning errors and threw uncaught exceptions that VSCode could not trace back through source maps (it would break inside generators); the issue ended up being a malformed function signature override
    • Additional resources:

  • IDE (VScode, etc.) shows zero type errors, and / or no errors at all, and yet code refuses to compile and/or get strange errors with ts-node like TypeError: myfile_1.myFunction is not a function that don't apply

    • Check for a circular dependency!

      • A clue that this might be the case is if the error only throws for a subset of files, or even a single file with ts-node
    • If it is caused by a circular dependency, or you are trying to detect if it is, you can read my notes on dealing with those here
  • Error trying to import JS library: Could not find a declaration file for module '{lib-name}'. '{lib-name}/{entryPointFilePath}' implicitly has an 'any' type. Try `npm install @types/{lib-name}` if it exists or add a new declaration (.d.ts) file containing `declare module '{lib-name}';`ts(7016)

    • Often the issue is exactly what the error is stating; your library author did not export types (e.g. *.d.ts files), and you don't have any local declarations to fill in the gaps. With strict rules, this will get flagged, since TS has to default to any.
    • The easiest solution is if you can find already created types (e.g. from @types) you can install 😄
    • Check out my section on dealing with legacy JS
  • Error when importing stylesheets: Cannot find module './___.css' or its corresponding type declarations.ts(2307)

    • This is because the TypeScript system, by default, cannot understand (or infer) non TS files
    • Similar to JS imports with no exported types
    • Couple of options to fix:

      • No type safety (but error goes away): Change import ... to a require() statement (or import style = require(...))
      • Barely any type safety: Keep import statement the same, but add an ambient declaration for *.css files, like so
      • Type-Safe CSS (!): Use a Webpack plugin that generates type definitions for your stylesheets, automatically! You can find some great guides on how to accomplish this here and here

Debugging

Resources

You can also use ts-node for simplified debugging; docs

Watching Files and Triggering Tasks

Trying to chain together build tasks to execute based on source code changes is tricky with TypeScript. For starters, although tsc --watch will track source code changes and re-build, it does not accept arguments for a command / callback to execute when it does so.

  • Best solution: Use tsc-watch

    • It starts tsc in watch mode, but also lets you execute any command of your choice when it rebuilds
    • Easy to use, fast
  • More manual: Use nodemon as watcher

    • This is slightly complex; you have to be careful on how you arrange the commands
    • You might be tempted to do something like concurrently "tsc --watch" "nodemon --watch dist ....", but this is going to work poorly due to TSC touching the output files even when there are not changes
    • It is best to have Nodemon watch *.ts files instead of TSC, and have it trigger a compile and then whatever else you want: {"watch" : "nodemon -e js,ts --ignore dist --exec \"tsc && echo 'recompiled!'\""}

Assorted Tips and Tricks

  • Use TypeScript import aliases

    • These are awesome; you can use something like import {User} from '@models' instead of import {User} from '../../../mvc/models
    • There are a bunch of guides out there on how to use this.
Markdown Source Last Updated:
Mon Sep 14 2020 16:53:37 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 19 2019 17:06:24 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby