Joshua's Docs - TypeScript Cheatsheet, Common Errors, and More

💡 This is a big document; feel free to use the sidebar table-of-contents (open by clicking the link, or tapping / clicking upper left corner button) to help you navigate around!

Resources

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

Other:

Sometimes if you are stuck on how to type something difficult, another good resource can be looking at type declarations for an existing library you are somewhat familiar with. For example, the types for Lodash

Tooling

See privatenumber/ts-runtime-comparison for a comparison of common TypeScript runtimes

  • TypeScript Compiler (TSC)
  • TS-Node (easy way to execute TypeScript without build step, for local dev)
  • tsx
    • Alternative to ts-node, which uses esbuild under the hood
    • Does not type-check
    • Strongly recommend as alternative to ts-node
  • esrun
    • Alternative to ts-node, wraps esbuild
  • swc-node
    • Alternative to ts-node, uses swc (Rust-based compiler) under the hood
    • Note: ts-node actually supports swc with minimal setup now - see docs
  • esbuild
  • tsconfig/bases (default TSConfig starters)
  • typehole
    • VSCode extension: Captures unknown types from runtime and inserts them into your code
  • lukeed/tsm (TypeScript module loader, for Node)

Global install

npm install typescript -g

Quick init (create tsconfig.json)

tsc --init

Getting around index issues

Unknown Index Signature

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: () => {};
	}
}

If you are trying to strongly-type process.env values (environment variable values) in NodeJS, target ProcessEnv:

namespace NodeJS {
	interface ProcessEnv {
		AUTH_TOKEN: string;
		NODE_ENV: 'development' | 'production';
		DEBUG_PORT?: string;
	}
}

// Same rule as above applies - if this file contains an `export` or
// `import` keyword, then the above most be wrapped in `declare global {}`

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

Explicit Index Signature

You might 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 using a union type with Record:

type Person = Record<'name' | 'state', string>;

const joseph: Person = {
	// Permitted
	name: 'Joseph',
	state: 'Washington',
	// This will throw an error
	city: 'Seattle'
}

Or, by using an enum:

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'
}

💡 There is a big benefit to specifying specific keys; you don't lose intellisense / typing that you would normally lose by adding a generic index of [k: string]: string. (For further note, see S/O #52157355)

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}:

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

	// Can also type-cast while indexing, all on one line
	console.log(myIceCream[key as keyof typeof myIceCream]);
});

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();

... 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();

Not using Function as a type is also enforced by the @typescript-eslint/band-types rule

You can also declare and use explicit function typedefs, like so:

type CustomFuncType = (input: string) => boolean;

const customFuncImplementation: CustomFuncType = (input) => !!input;

Function as Generic Type

For accepting a function type as a generic, you can use the extends keyword to do something like this:

F extends (...args: any[]) => any

Here are some examples:

type Promisify<F extends (...args: any[]) => any> = (...args: Parameters<F>) => Promise<ReturnType<F>>;

function executeAnyFunctionTenTimes<F extends (...args: any[]) => any>(
	functionToRunTenTimes: F,
	...argsToRunWith: Parameters<F>
) {
	for (let x = 0; x < 10; x++) {
		functionToRunTenTimes.apply(functionToRunTenTimes, argsToRunWith);
	}
}

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

Scaffolding a TSConfig File

The two easiest ways to scaffold a tsconfig.json file are to either use:

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 tsconfig.production.json

💡 If you use tsconfig.{NAME}.json instead of {NAME}.tsconfig.json, VSCode will automatically recognize it as a valid TS Config file and provide automatic schema intellisense

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

Multiple TS Config Files with VSCode

Unfortunately, as far as Intellisense and in-IDE type-checking goes, VSCode only allows for one tsconfig.json file per directory (AFAIK) - this could be considered the "root level" config of that specific directory. I have seen multiple comments like this one, that seem to indicate that VSCode resolves config mapping by searching from the bottom up, so keep that in mind when designing a system that uses multiple configs. For example, a standard nested approach might be something like:

.
├── tsconfig.json
├── frontend/
│	└── tsconfig.json
└── backend/
	└── tsconfig.json

💡 You can even combine this with a VSCode multi-root workspace for maximum effect

💡 This approach is really useful for projects that have areas of code that need different libs available - e.g. DOM for frontend, no DOM for backend.

Relevant StackOverflows: A, B

⚠ Warning: If you have a nested config that does not seem to be taking effect (e.g. the one above it seems to be applying), make sure that the files you want it applied to are excluded by the higher-level config file.

Verifying Your TSConfig File

A helpful command for verifying that your tsconfig file is being applied the way you are expecting it to be:

  • tsc --showConfig

You can also verify the list of files that it will run against:

  • tsc --listFiles --noEmit

TSC / TSConfig Gotchas and Tips

  • Tip: Check out tsconfig/bases for common use-cases and the configs to go with them
  • 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

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?

Most people probably prefer Option C for its more concise and readable format (and compatibility with tsx)

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
}

Classes

📄 Main Doc: Handbook - Classes

👉 Given the overlap, you might also be interested in the cheatsheet on JS Classes

Key things to note about TS classes:

  • Everything is public by default, making the modifier not required, unless you want something different
  • You can use private {name} modifier, or JS's #{name} syntax for private fields, but they don't function exactly the same
  • protected acts like private, but any deriving classes (via extends) can also access the field
    • Making the constructor protected means that the class cannot be instantiated directly (outside its containing class), but can be extended
  • readonly is supported, but requires that the value be assigned where declared, or in constructor
  • Getters and setters are supported; use get and set keywords
    • These require output > ES5
  • TypeScript supports "Parameter Properties", which are an awesome way to initialize and assign class members directly via the constructor arguments (see section below)

TS Classes - Parameter Properties

As noted above, Parameter Properties are a special TypeScript feature that lets you initialize and assign class members directly in the function. This can replace a bunch of code that previously was 100% boilerplate. For example:

// Standard way of assignment
// Notice how much extra code we have to type out that is redundant! We are double typing!
class Person {
	private id: number;
	name: string;
	age: number;
	constructor(id: number, name: string, age: number) {
		this.id = id;
		this.name = name;
		this.age = age;
	}
}

With Parameter Properties:

// Lot less code!
class Person {
	constructor (private id: number, public name: string, public age: number) {}
}

Shout-out to Steve Fenton for this blog post on the topic.

If you are interested in assigning to a class the destructuring results of an object, passed as a an argument to the constructor, See this S/O answer and this open issue.

TS Classes - Constructor and Instance Types

Getting Instance Type from Uninstantiated Class

Some might also be basically doing a "reverse typeof" operation, where you need to go backward from type = typeof MyClass to just MyClass

In certain situations in TypeScript, you might end up with a type or variable that is equal to uninstantiated class - something that you need to call new myThing() to instantiate. In these situations, how you derive the base class back out?

Luckily, we have the InstanceType<T> utility type as part of the TypeScript built-ins!

Here is an example:

class MyClass {
	public sayHi(){
		console.log('hi');
	}
}
const uninstantiated = MyClass;
// type of `uninstantiated` becomes `typeof MyClass`, which is the uninstantiated
// class type / with newable constructor.
// You *cannot* do `typeof typeof` to get raw class, but see below for trick!

// Pretending that we don't have direct access to MyClass as a type, we can still
// derive it by using the InstanceType<T> utility
type DerivedMyClass = InstanceType<typeof uninstantiated>; // Type = MyClass

Inferring Constructor Parameters from a Class

TypeScript has a utility type that makes this easy - ConstructorParameters<T>.

For example:

const wrappedFormData = (...args: ConstructorParameters<typeof window.FormData>) => {
    console.log('FormData constructor called', args);
    return new FormData(...args);
}

TS Classes - Expression is Not Constructable

Occasionally, you might run into this type of error:

This expression is not constructable.
Type 'MyClass' has no construct signatures.
ts(2351)

Usually this stems from one of a few different options.

The first option, and most frequent mistake, is that you are accidentally referring to an instance of the class instead of the uninstantiated class (constructor) itself. In that case, you want to use typeof MyClass.

Here is a complete example, showing the error inducing mistake, and then the corrected version (playground):

Show Example
// Pretend this is valid class, from somewhere else
const ImportedClass: any = null;

declare class MyClass {
    sayHi(): void;
}

/**
 * Error example - missing `typeof`
 * This is referencing the instance type, not the raw Class
 */
const WronglyTypedClass: MyClass = ImportedClass;
const wronglyTypedClassInstance = new WronglyTypedClass();
// ^ - Error: This expression is not constructable

/**
 * _Correct_ example - uses typeof
 * References the Class _type_, not instance
 */
const CorrectlyTypedClass: typeof MyClass = ImportedClass;
const correctlyTypedClassInstance = new CorrectlyTypedClass();
// Success!
correctlyTypedClassInstance.sayHi();

The second option, although much less likely, is that it could be exactly what the error says; the actual type definition for the type of MyClass is missing a constructor.

This could happen if you accidentally typed your class as an interface, but otherwise shouldn't happen with declare class code - TS will automatically inject a void constructor type, as that is what JavaScript does anyway

  • In JavaScript, providing a manual constructor is not required, and if omitted, a default constructor is used.
  • Example: This is completely valid, and has an auto-injected constructor signature
    • declare class MyClass {}

TS Classes - Type Definitions for Classes and Constructors

Crafting TypeScript for classes, especially as pure type-definitions and not through actual implementation code, can be tricky.

One particular catch is that if you try to create a class type with class, you can't specify the return type - you will get a Type annotation cannot appear on a constructor declaration error. An alternative declaration syntax is to declare it as an interface with a new property:

declare global {
	const Tree: {
		new(serializedTree: string): HydratedTree;
	}
}

const tree = new Tree('A -> B');

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

Multiple Modules

Within an ambient declaration file it is possible to declare multiple modules, generic modules (covering infinity files matching the pattern), and even declare a module and then import that same module into another.

To write a module declaration that covers a generic set of files, you write the module path with a glob, like this:

declare module 'my-lib/shared-configs/*.js' {
	const config: IConfig
	export = config;
}

When declaring modules, the declarations will take effect even within the same file, can be imported into other declarations. Here is an example:

declare module 'my-lib/nested/delta' {
	// ...
}

declare module 'my-lib' {
	// This will pick up the types declared above
	import Delta = require('my-lib/nested/delta');
	// ...
}

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, without turning on the special interop setting, 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.

If you get the error Invalid module name in augmentation. Module '___' resolves to an untyped module at..., then you probably have a top level import that needs to be moved inside a declaration block - just like in the strategy above.

Ambient Declarations - Avoiding Conflicts and Importing Globals

One major danger of relying on ambient declarations as opposed to explicit imports and exports, is that names can very easily clash and libraries can end up overriding each other's globally declared variables and types.

For example, some types from @types/node will override @types/react-native.

Unfortunately, at this time, there isn't a very clean way to resolve conflicting globals. See umbrella issue #37053.

This is further complicated by the fact that skipLibCheck acts as a binary flag; setting it to true will let you ignore some global conflict issues (but not all), but will also completely disable type-checking related to ambient declaration files.

The main options right now would be to:

  • Hand-code the exact correct global type in your own global file, to override the conflicting third-party definitions
  • Use something like patch-package to remove the troublesome definitions from your dependencies

Ambient Declarations - Common Issue: Not Getting Picked Up

If your ambient declaration file does not seem to be getting picked up, here are a few things to check

  • Make sure compilerOptions.skipLibCheck is not set to true
  • If TSC does not throw an error, but your IDE shows an issue, try reloading your IDE and/or language server

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:

Augmenting Web Browser API Types

If you want to augment any browser API interfaces (e.g. stuff in lib.dom.d.ts), the same basic rules apply as outlined in "getting around index issues".

To reiterate, the safest way is to merge interfaces in an ambient declaration file:

// 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: () => {};
	}
}

For example, here is a practical example where I had to augment the MediaDevices interface (exposed, for example, through navigator.mediaDevices), to add typing for .getDisplayMedia(), which has not been added in lib.dom.d.ts, but is live in Chrome.

interface MediaDevices {
	/**
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
	 */
	getDisplayMedia(constraints?: MediaStreamConstraints): Promise<MediaStream>;
}

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 a default import, in standard syntax: import MyDefault from '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: my_module_1.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
    • For an object, the type (interface) should come after the default object, but it also requires destructuring the properties within the arguments:
      function EnrollPhone({serial, monthDuration = 12} : {serial: string, monthDuration?: number}) {
      	//
      }
  • Using syntax for generics
    • Calling a method
      • Use angle brackets to enclose the type
        • Example: let result = myFunc<IMyInterface>({...})
      • If type can be inferred, you might not need to declare it - e.g. just use let result = myFunc('myStr');
    • Declaring a method
      • Use angle brackets with generic placeholders, and then annotate arguments with same placeholders
        • Example: function myFunc<T>(arg: T) {}
        • Example: function myFunc<T, U>(arg: T, secondArg: U) {}
      • Arrow functions are special!!!
        • Example: const myFunc = <T extends unknown>(input: T) => {};
        • TSX specific example: const MyComponent = <T,>(arg: T) => {}
      • You can also indicate that the return type of a function matches the generic input type
        • Example: function myFunc<T>(arg: T) : T { return arg; }
    • Declaring types:
      • type TakeGeneric<T> = ...
      • interface TakeGeneric<T> { ... }
    • For more uses of generics (such as with classes), refer to the handbook.
  • Functions as types?
    • Generally, (ARGS) => RETURN_TYPE.
    • If you need to type an async function, use (ARGS) => Promise<RETURN_TYPE> (not async () ...)
    • For applying a type to a function / annotating a function with an existing signature / enforcing that a function satisfies another type. For example, if we wanted to say that function double satisfies the signature of type NumberOperation = (i: number) => number;
      • Arrow function: const double: NumberOperation = (i) => i * 2;
      • Regular function: You can't really apply a function type to an entire function declared with function in a one-shot operation; you usually separately type the args and return type
    • See above for using generic slots
  • Define a constructor type signature outside of its implementation (e.g. in ambient declaration)
  • Multiple extends?
    • Comma separate: interface MultiInheritance extends Alpha, Bravo {}

Generics - Dynamic args

type Append<Args extends any[], Arg> = [...Args, Arg];

type WithErrorCallback<T extends (...args: any) => any> =
    (...args: Append<Parameters<T>, MyErrorCallback>) => ReturnType<T>;

type MyErrorCallback = (error: {code: string, msg: string}) => void;

type CapLength = (input: string, cap: number) => void;
type CapLengthWithErrorCallback = WithErrorCallback<CapLength>;
// ^ inferred type should be `(input: string, cap: number, errorCb: MyErrorCallback) => void`

type GetCurrentName = () => string;
type GetCurrentNameWithErrorCallback = WithErrorCallback<GetCurrentName>;
// ^ inferred type should be `(errorCb: MyErrorCallback) => void;

Generics - Examples

For some reason, my brain doesn't remember generics very well, and I constantly have to remind myself of how they can be applied. To help myself, and others, here are some simplified examples that show different ways to use generics.

Nested Generics

This example shows how nested types can be enforced.

const useStore = <T extends Record<string, any>>(input: T) => {
	let copy: T = JSON.parse(JSON.stringify(input));
	const updater = <U extends Partial<T>>(updates: U) => {
		copy = {
			...copy,
			...updates
		};
	}

	return {store: copy, updater};
}

const {store, updater} = useStore({
	updateCount: 23,
	names: ['fred', 'george'],
	leader: {
		name: 'wilson',
		age: 49
	}
});

updater({
	updateCount: ++store.updateCount,
	leader: {
		name: 'fred',
		// TS will throw an error here, correctly enforcing the subtype
		age: 'abc'
	}
});

Generics that Extend Other Generics

Generics can be confusing when you are trying to define a new generic that extends another, that also takes a generic slot.

For example, if we have:

interface WithPrintContent<T> {
    printContent: string;
}

The instinct is to sometimes write a function that accepts a shape like this with T extends WithPrintContent, but if you do this you will get "Generic type ...<...>' requires ... type argument(s).". The correct way is to keep the generic slot completely generic, and move the enforcement of the shape to the actual parameter annotation:

interface WithPrintContent<T> {
    printContent: string;
}

const printBook = <T,>(book: WithPrintContent<T>) => {
    console.log(book.printContent);
}

Conditional Return Types Through Generics

One of the most powerful parts of generics is unlocking strong static-typing for functions where the return type is dependent on its inputs. TypeScript already supports functions that return a union (multiple possible return types), but without using generics, it can't infer when this type could automatically be narrowed based on the arguments.

First, an example without using generics:

class Cat {}
class Dog {}

function callPetOver(call: 'meow' | 'woof') {
	if (call === 'meow') {
		return new Cat();
	}

	return new Dog();
}

const dog = callPetOver('woof');
// ^ The inferred type by TypeScript here is `Cat | Dog`
const cat = callPetOver('meow')
// ^ The inferred type by TypeScript here is `Cat | Dog`

In the above example, we can visually tell by looking at the code that the only possible outcome if we call the function with 'woof' is for the return type to be Dog, but TypeScript says it is Cat | Dog.

By combining generics with with TypeScript's conditional types and the extends keyword, we can improve our example so that the exact return type is inferred based on the call argument:

function callPetOver<T extends 'meow' | 'woof'>(call: T): T extends 'woof' ? Dog : Cat {
	if (call === 'meow') {
		return new Cat();
	}

	return new Dog();
}

const dog = callPetOver('woof');
const cat = callPetOver('meow')

Some of the concepts explored here share similarities with overloading, but that is a different area of TypeScript


Narrowing and Type Guarding

Authoritative guide - TS Handbook: Narrowing

First, what is narrowing and type-guarding, and how can they 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.`);
	}
}

Child Property Narrowing via Stored Variable

I think the issue described here might be fixed when / if PR #46266 lands. I would check to see if that has happened first before reading too much into this section. Nope. #46266 (aka Control flow analysis for destructured discriminated unions) doesn't really cover nested properties, which is why it doesn't fix the below issue.

TypeScript can have some issues with statically type narrowing on child properties that are possibly undefined, especially when trying to indirectly narrow via a stored variable. For example, this currently throws an error:

interface Cart {
    toBuy: number[];
    returns?: number[];
}

// Pretend this is populated from another place
const userCart: Cart = JSON.parse(`CART_PAYLOAD`);

(() => {
    const hasReturns = !!userCart.returns && !!userCart.returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            userCart.returns[0]
            // ||-----> `Error: Object is possibly 'undefined'.`
            //     || ------> `(property) Cart.returns?: number[] | undefined`
        );
    }
})();

Support for indirectly narrowing via a variable was actually added to TypeScript via #44730, which looks like it made it into TS for v4.4.0 and up. However, as the above example shows, this does not seem to work for object properties.

The easiest way around this is to either keep the type narrowing all in the if statements, or pull the child property out as a variable before narrowing. Here are some complete examples:

Show / Hide Code
interface Cart {
    toBuy: number[];
    returns?: number[];
}

// Pretend this is populated from another place
const userCart: Cart = JSON.parse(`CART_PAYLOAD`);

// Gaurding right in the conditional `if` statement works:
(() => {
    if (userCart.returns && userCart.returns.length) {
        console.log(
            'first item to return:',
            userCart.returns[0]
        );
    }
})();


// But, trying to store the result of a check in a variable and then referencing throws an error
(() => {
    const hasReturns = !!userCart.returns && !!userCart.returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            userCart.returns[0]
            // ||-----> `Error: Object is possibly 'undefined'.`
            //     || ------> `(property) Cart.returns?: number[] | undefined`
        );
    }
})();

// Trying to use `typeof` to narrow doesn't fix things
(() => {
    const hasReturns = typeof userCart.returns !== 'undefined' && !!userCart.returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            userCart.returns[0]
            // ||-----> `Error: Object is possibly 'undefined'.`
            //     || ------> `(property) Cart.returns?: number[] | undefined`
        );
    }
})();

// Nope, not this either
(() => {
    const hasReturns = Array.isArray(userCart.returns) && !!userCart.returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            userCart.returns[0]
            // ||-----> `Error: Object is possibly 'undefined'.`
            //     || ------> `(property) Cart.returns?: number[] | undefined`
        );
    }
})();

// A custom type guard works fine too
(() => {
    // Custom type guard
    type CartWithReturns = Required<Cart>;
    function getIsCartWithReturns(cart: Cart): cart is CartWithReturns {
        return !!cart.returns && !!cart.returns.length;
    }

    // Checking against the type guard instantly narrows
    const hasReturns = getIsCartWithReturns(userCart);

    if (hasReturns) {
        console.log(
            'first item to return:',
            userCart.returns[0]
        );
    }
})();

// Pulling out the property first works:
(() => {
    const returns = userCart.returns;
    const hasReturns = !!returns && !!returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            returns[0]
        );
    }
})();

// Also, if this is not a child property, but has the same type, it actually works (on v4.4.0+).
// I believe this works basically the same as the above example, and is because of this - https://github.com/microsoft/TypeScript/pull/44730
(() => {
    const returns: number[] | undefined = JSON.parse('');
    const hasReturns = !!returns && !!returns.length;
    if (hasReturns) {
        console.log(
            'first item to return:',
            returns[0]
        );
    }
})();

Narrowing Through Types Only - Statically Narrowing Without Guards or Control Flow Analysis

Usually when talking about narrowing in TypeScript, the implication is that the type is ambiguous at runtime, so it needs to be narrowed through something that is a part of runtime - i.e., actual code that gets executed, not just TypeScript types. This is why narrowing traditionally takes the form of guards or control flow analysis.

However, there are times when we might know more than the TypeScript compiler and are sure° that a union type can be narrowed without actually checking that this is the case at runtime.

° = Note: It is rather rare that this is the case. In general, you should not make assumptions when it comes to areas of code that handle highly dynamic types, especially if they take user input.

Narrowing statically via types might also be useful if we are trying to use a giant union provided by a library, and we want to extract out sub-types, like this:

/**
 * You sometimes see this large union types in libraries
 */
type PossiblePayloads = {
	name: string;
	status: number;
} | {
	count: number;
	status: number;
} | {
	color: string;
	status: number;
} | {
	recipe: string;
} | {
	error: number;
} | {
	permissions: string[];
	status: number;
} | {
	error: {
		code: number;
		message: string;
	}
}

How do you assert that the type has narrowed? Or pick a sub-type off this giant union?

There are two main ways that I know of. Let's explore both through trying to narrow to / extract out the payload type that has color: string, status: number.

The first method is to narrow using the extract utility type:

type IsDefinitelyColorPayload = Extract<PossiblePayloads, {color: string}>;
// Inferred narrowed type = `{color: string; status: number;}

Another way, although one that really only works with generics, is with TypeScript's conditional types and the extends keyword:

type MatchesColorPayload<T> = T extends {color: string} ? T : never;
type IsDefinitelyColorPayload = MatchesColorPayload<PossiblePayloads>;
// Inferred narrowed type = `{color: string; status: number;}

Type Narrowing - Troubleshooting

  • Type narrowing isn't working with an object property stored via an intermediate variable!
  • Previous type-narrowing is lost inside of a callback (.map(), etc.)
    • This is not a bug, but rather an assumption that TypeScript makes; it assumes that there is a possibility that the value of the variable has changed from the time it was narrowed, until the callback scope is reached.
    • You can use a post-fix operator, narrowing within the callback, or a few different other methods for working around this

NodeJS Specifics with TypeScript

💡 Remember: You are probably going to want to install @types/node if you are using TypeScript with NodeJS

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 the type accessed by a generic index

This is similar to the goal of the above trick; given something like

interface MyInterface {
	[k: string]: MyComplexType;
}

How do we extract the type that is allowed for generic indexes (MyComplexType)?

You can use the keyof (index type query operator):

interface MyInterface {
	[k: string]: MyComplexType;
}

type ExtractedComplextType = MyInterface[keyof MyInterface];

It is important to note that this solution also applies to interfaces that do not explicitly have a generic index signature, but where you are trying to extract the type that is the union of all possible values represented by keyof T lookups. For example:

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

type PersonVal = Person[keyof Person];

/*

Extracted type contains *all* possible nested types that could be accessed through a key:

type PersonVal = string | number | {
	address: string;
	vendor: string;
	lastSent: number;
	hasBounced: boolean;
}

*/

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[number] = {
	model: 'Compy 386',
	isActive: false,
	lastPing: 1100505600
};

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

// You can also index by 0
type ExtractedFirstElemType = ComputingDevices[0]

Any, but exclude undefined or null

If you are looking to exclude undefined from a union, you can do so with Exclude<MyUnionType, undefined>, without involving any of the below hacks. Same thing works for excluding null from a union. For both, use Exclude<MyUnionType, undefined | 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!


Exhaustive Array Checks

Exhaustive checks in TS tend to be notoriously tricky, but for enforcing an exhaustive array declaration of strings, there is a shortcut to avoid trickier approaches, which is to use Tuple types.

// This is a Tuple type
type RequiredMeals = ['Breakfast', 'Lunch', 'Dinner'];

// ERROR: not assignable to type 'RequiredMeals'. -- this is missing Dinner!
const exhaustiveArr: RequiredMeals = ['Breakfast', 'Lunch'];

💡 Tip: You can re-use Tuples as unions, by using a number index, with type DerivedTuple = MyTuple[number]. In the above example, that would be type RequiredMealsUnion = RequiredMeals[number]

The downside to this approach is that it requires that the order of the array elements strictly follows the declaration (must match exactly)


Reusing a Tuple Type as a Union

One of the really neat things about Tuples, is that you can easily derive Union types from them, by indexing via number. Consider the following examples:

// Regular Tuple
type StringNumTuple = [string, number];

type StringOrNumber = StringNumTuple[number];
// Derived type: `string | number`

This might not seem all that helpful, until you consider how it also works with literal tuple types:

// Tuple Literal
type PlaylistGenres = ['Classical', 'Indie', 'Pop', 'Rock'];

const addSongToPlaylist = (title: string, artist: string, genre: PlaylistGenres[number]) => {
	//...
}

addSongToPlaylist('Très Jolie, Op. 159', 'Émile Waldteufel', 'Classical');
// If we tried to add any Genre string not belonging to PlaylistGenres tuple, we would get an error.

Rather than using the Tuple declared as a TS type, we can go the other direction; start with an actual array, and use the as const assertion to turn it into a tuple, then further derive types:

const playlistGenres = ['Classical', 'Indie', 'Pop', 'Rock'] as const; // the `as const` is crucial. Otherwise, type is just inferred as `string[]`

type Genre = typeof playlistGenres[number];
// Inferred: type Genre = "Classical" | "Indie" | "Pop" | "Rock"

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!'

Function with Static Property

Since functions are funny, this is completely valid:

function sayHi() {
	console.log('Hello');
}
sayHi.color = 'blue';

console.log(sayHi.color); // 'blue';

So, how do we represent the type of sayHi in TypeScript, without the implementation?

You have to define it as an interface, with a callable signature, as well as the static property:

interface SayHi {
	(): void;
	color: string;
}

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.


Strongly Typing Errors and using Try Catch

  • Use instanceof to guard
    • E.g. if (error instanceof CustomError && error.code === CustomErrorCodes.AuthFailed) {}

Overriding / Augmenting Properties from Existing Types

This section could use a better name; this is not about ambient declaration augmentation.

Overriding Interface Property Types

By default, when you use & to combine different interfaces, you are actually defining new intersection types, which represent the combination of possibilities.

For example:

interface Instrument {
	name: string;
}

type Saxophone = Instrument & {
	type: 'alto' | 'tenor' | 'baritone' // and so on
}

This works great for extending types or narrowing possibilities when there is overlap, but there are issues if you are trying to replace the types of properties, like so:

interface Piano {
	type: 'grand' | 'baby-grand' // and so on
}

type Saxophone = Piano & {
	type: 'alto' | 'tenor' | 'baritone' // and so on
}

const myInstrument = {} as unknown as Saxophone;
myInstrument.type // ERROR: Property 'type' does not exist on type 'never'.
                  // The intersection 'Saxophone' was reduced to 'never' because property
                 // 'type' has conflicting types in some constituents
                 // (Inferred type is `any`)

The issue is that with an intersection type, TypeScript is trying to reduce the possibilities to just those that overlap / are shared. In the above example, there are zero values that overlap between 'grand' | 'baby-grand' and 'alto' | 'tenor' | 'baritone', so the type is inferred as never, since nothing overlaps and satisfies the type system.

Trying to use interface Saxophone extends Piano {} will have the same issue

Even if something did overlap, an intersection reduces the possibilities, so it might not work how you are expecting it to:

interface Piano {
	holder: 'stand' | 'legs' | 'base';
}

type Saxophone = Piano & {
	holder: 'stand' | 'hand'
}
// The type of `Saxophone.holder` is actually narrowed to ONLY  `stand`, not `stand | hand`,
// because `stand` is the only value that overlaps between `Piano.holder` and `Saxophone.holder`

The solution to actually replacing the type of an interface property instead of intersecting with it? Use Omit<> to exclude the original property type before replacing it with a new one:

interface Piano {
	holder: 'stand' | 'legs' | 'base';
}

type Saxophone = Omit<Piano, 'holder'> & {
	holder: 'stand' | 'hand'
}

Here is a more thorough set of examples (TS Playground):

Show / Hide Example
// Keep in mind that the workarounds required here show why it is better to just have your base interfaces be generic,
// so they can be extended to be more specific, instead of the other way around

interface Bus {
	color: string;
	power: 'overhead-cables' | 'diesel' | 'battery' | 'hybrid';
}

type Bike = Bus & {
	power: 'pedals' | 'battery';
}

const bike = {} as unknown as Bike; // pretend this was created via a real Bike creator
bike.power // `.power` is inferred / narrowed to *only* `battery`, instead of `'pedals' | 'battery'`,
           // because that is the only value that *overlaps* (union) between `Bike` and `Bus`.
           // This is not what we wanted :(

type Skateboard = Bus & {
	power: 'feet' | 'ramp';
}
const skateboard = {} as unknown as Skateboard;
skateboard.power // This is even worse for Skateboard - Error: `Property 'power' does not exist on type 'never'.`
                 // TypeScript is telling us there is no overlap for `.power` between `Skateboard` and `Bus`,
                 // as no values are the same between them

// ==================================================================
// Let's take another crack at this, but this time use `Omit<>` to remove the `.power` property first, before overriding.
// In this case, we are replacing the possible type, instead of trying to merge.
// ==================================================================

type BikeViaOmit = Omit<Bus, 'power'> & {
	power: 'pedals' | 'battery';
}
const bikeViaOmit = {} as unknown as BikeViaOmit;
bikeViaOmit.power // Success! `.power` is inferred as `'pedals' | 'battery'`

type SkateboardViaOmit = Omit<Bus, 'power'> & {
	power: 'feet' | 'ramp';
}
const skateboardViaOmit = {} as unknown as SkateboardViaOmit;
skateboardViaOmit.power // `'feet' | 'ramp'`

💡 If you find yourself using this a lot, you could also use a more powerful utility type that is not built-in, like this solution

Overriding Class Member or Method Typings

For overriding class property typings, instead of using Omit<>, you will usually want to use declare class to create a new class derived from the old one. This example should illustrate why (TS Playground):

Show / Hide Example
interface MachineConfig {
	batteryTypes: Array<'alkaline'|'lithium'|'zinc'>
}

declare abstract class AbstractMachine {
	private _numberOfUsers: number;
}

declare class Machine extends AbstractMachine {
	public id: string;
	public startUp(): void;
	public shutDown(): void;
	// Bad typing
	public getBatteryCount(): any;
}

function onlyTakeMachine(machine: Machine){}

// ===========================================
// We want to improve the typing on `getBatteryCount`, since we know method will return
// counts by specific battery types that the machine has, defined in the config
// =============================================


// Augmenting class through `type =` without omit fails to override the method type
type AugmentedMachineTypeNoOmit<T extends MachineConfig> = Machine & {
	getBatteryCount(): Record<T['batteryTypes'][number], number>;
}
(() => {
	const betterTypedMachine = 'fake' as unknown as AugmentedMachineTypeNoOmit<{
		batteryTypes: ['alkaline']
	}>;
	const alkalineCount = betterTypedMachine.getBatteryCount().alkaline;
	// ^ override did not work; inferred type is `any`
	onlyTakeMachine(betterTypedMachine);
	// ^ still works
})();

// Augmenting class through `type =` with omit works for overriding the method type, but makes our machine
// type now incompatible with things expecting `Machine`
type AugmentedMachineTypeWithOmit<T extends MachineConfig> = Omit<Machine, 'getBatteryCount'> & {
	getBatteryCount(): Record<T['batteryTypes'][number], number>;
}
(() => {
	const betterTypedMachine = 'fake' as unknown as AugmentedMachineTypeWithOmit<{
		batteryTypes: ['alkaline']
	}>;
	const alkalineCount = betterTypedMachine.getBatteryCount().alkaline;
	// ^ override worked; inferred type is `number`
	onlyTakeMachine(betterTypedMachine);
	// ^ FAIL:Argument of type 'AugmentedMachineTypeWithOmit<{ batteryTypes: ["alkaline"]; }>' is not assignable to parameter of type 'Machine'.
	// Property '_numberOfUsers' is missing in type 'AugmentedMachineTypeWithOmit<{ batteryTypes: ["alkaline"]; }>' but required in type 'Machine'.
})();

// Augmenting class through `declare class ... extends` gives us everything we need!!!
declare class AugmentedMachineClass<T extends MachineConfig> extends Machine {
	getBatteryCount(): Record<T['batteryTypes'][number], number>;
}
(() => {
	const betterTypedMachine = 'fake' as unknown as AugmentedMachineClass<{
		batteryTypes: ['alkaline']
	}>;
	const alkalineCount = betterTypedMachine.getBatteryCount().alkaline;
	// ^ override worked; inferred type is `number`
	onlyTakeMachine(betterTypedMachine);
	// ^ Works!
})();

Also relevant is class and interface merging.


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, or this one
        • A lot of these workarounds will also induce the file is not under 'rootDir'. 'rootDir' is expected to contain all source files error though.
    • 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
  • I can't stop TSC from checking node_modules!
    • First, try making sure you are using include and/or exclude to properly indicate to TSC which files you want checked. If you are still getting TSC checking inside node_modules, here are some more things to try:
      • Set compilerOptions.types to some value, at the very least an empty array ([])
      • Set compilerOptions.skipLibCheck to true (be warned, this skips checking of all your *.d.ts files!)
      • Make sure that the version of TSC you are running is the same as in your dependencies; you can use npx tsc to make sure you use the local version instead of a globally installed TSC version
      • If you are using a jsconfig.json as the project config (e.g. with --project jsconfig.json), try adding "maxNodeModuleJsDepth": 0 to the config file (or pass via CLI flag), since TSC will default to 2 for JSConfig based projects instead of zero (see related open issue)
    • You can use the --traceResolution and --listFiles flags to help troubleshoot why a file is getting included. Also, see the FAQ in the official Wiki.
    • The --showConfig option is also a helpful way to preview the effects of include, files, etc.
    • This can also occur if you are importing a type from a library, and that type depends on another 3rd party type, and that type is incorrect.
      • Unfortunately, in that scenario, the only fix is to either @ts-ignore that instance, or turn skipLibCheck on. However, to make matters worse, skipLibCheck will stop TSC from type-checking ALL ambient *.d.ts files, not just those in node_modules, so this can cause you to lose massive chunks of type-safety if you enable it.
      • There is a feature request (#30511) to allow granularity in skipLibCheck to only apply to node_modules
  • 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
  • VSCode only shows error for currently open files, instead of all files
  • 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
    • These approaches also work with things like .svg files, assuming you have a bundler that is transforming the files into importable strings
  • Getting the Element implicitly has an 'any' type because index expression is not of type 'number'.ts(7015) error on things like window['myKey'], even after following the steps here to add the specific property to the interface
    • Try accessing through dot-notation, instead of with brackets: window.myKey instead of window['myKey']
  • ts-node appears to be caching my code! I've made updates to the TS, but nothing changes!
    • Do you have any conflicting JS files? This can happen if you are emitting .js files in the same directory as your .ts source files, and with the same filenames. I'm not really sure if this would even be considered a bug, especially if you have allowJs set to true.
      • Easiest workaround is to adopt a /src and /dist build strategy - isolate your TS vs JS files.
      • Highly related: comments on issue #693
  • How to preview the files that TSC will create, without actually creating them?
  • My emitted declaration files contain massive strings! E.g. export const MyStr = "..." instead of just export const MyStr: string
    • Since TypeScript now has a string literal type, you have to be extra careful when declaring strings if you care about the emitted type. If you want the type to expressed as just string, you need to either avoid declaring as const, OR, type assert / cast as string
      • Example: const MyStr: string = `Hello World`;
      • Example: let MyStr = `Hello World`;
    • If you are using JavaScript + JSDoc, you can use something like this to type-assert string:
      const MyStr = /** @type {string} */ (`Hello World`);
  • TypeScript is trying to use NodeJS types (@types/node) within a project where you actually want web types, and they are overriding lib.dom.d.ts. Perhaps this is causing issues with Type 'Timeout' is not assignable to type 'number' for setInterval or setTimeout
    • What has likely happened is that either yourself, or a dependency has brought in @types/node inside node_modules, which causes them to be auto-included in TypeScript type resolution algorithm, regardless of whether or not you import them or have them as a direct dependency.
    • To fix, you can either:
      • Make sure @types/node is excluded by setting compilerOptions.types to an array that excludes node as a value (you can set it to an empty array if you are not using any other types)
        • or,
      • As a quick fix, you can refer to browser methods via their global object. E.g. use window.setInterval instead of just setInterval.
  • Error: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.
    • This often happen when accidentally using {[key: keyof MyObj]: MyType} instead of {[key in keyof MyObj]: MyType} as an index signature
    • Related error: An index signature parameter type must be either 'string' or 'number'
  • The IDE is showing TypeScript errors, but they don't match the tsconfig settings and tsc does not throw any errors when compiling.
    • Verify that the version of TypeScript used by the IDE's live type-checking feature matches the one installed locally for this project.
      • In VSCode, you can verify with TypeScript: Select TypeScript Version and make sure that Use Workspace Version is selected instead of Use VS Code's Version. Or, manually edit your settings.json to have "typescript.tsdk": "node_modules/typescript/lib" (you might need to alter slightly)
    • If there have been recent changes to your tsconfig.json file, you might need to reload the workspace / IDE

'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 Casting and Other Escape Hatches

I'm always reluctant using these, and especially recommending them, but sometimes... TS just doesn't want to play nice with an existing system and infers the completely wrong types or thinks our app is going to explode, when it is just fine. The best course of action would be to fix the problems that are buried in the codebase that are causing the incorrect types; usually this involves getting better *.d.ts files created for legacy JS, using newer TS features, etc.

However, if you really need them, there are ways to suppress errors and assert / cast types.

TypeScript Error Suppression

  • // @ts-ignore comment
  • Reducing "strictness" in compiler options, by...
    • Disabling any of:
      • alwaysStrict
      • strict
      • strict___ (example, strictNullChecks)
      • noImplicit___ (example, noImplicitAny)
    • Turning on any of:
      • suppressExcessPropertyErrors
      • suppressImplicitAnyIndexErrors
    • Reducing strictness across your entire codebase is highly recommended against. It is much better to use tiny escape hatches where necessary.

TypeScript Type Casting / Assertion

  • Asserting that something is is set (not undefined or null)
    • Use a ! after the thing
      • myObj.prop!
      • myObj!.prop
      • myThing!
    • The trailing ! is a post-fix expression operator assertion
  • Asserting a different type than what TS inferred
    • In many cases (especially if any is inferred) you can use the simple as type assertion:
      • const strLength = (myVar as string).length
      • const res = func(myVar as string);
    • In cases where TS thinks there is no overlap, you will get an error when trying to use a simple type assertion: "neither type sufficiently overlaps". In this instances, you first have to tell TS that it needs to treat the item as unknown:

Note: It might feel naturally to call these types of approaches casting (and I've included that word in my headline), but keep in mind that they don't actually change the run-time JS that is output, so assertion is more accurate.

Debugging

Resources

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

Building NodeJS Packages

For building NodeJS packages (e.g. something to be distributed through NPM), many developers actually don't use TSC directly - they use a bundler like Rollup to wrap the TS build process and produce the final distributable code. Here are some relevant starter templates:

There are also some libraries / CLI tools that specifically offer lots of TS support and reduce the amount of manual configuration that is necessary:

Although it is possible to create your final package code with only TSC, there are some pain points to be aware of, especially if you are targeting ESM as your distributed module type (which is somewhat new to NodeJS).

  • TypeScript does not appear to respect package.exports (yet)
    • Also known as an exports map, this is a new NodeJS package feature that makes it easier to explicitly define entry points, provide direct access to nested files with shorter paths, and even support multiple module patterns with different files
    • Although NodeJS might report no issues, TS will have issues and report Cannot find module... errors for import paths that rely exclusively on the package.exports feature
    • Tracked as issue #33079, and there are some comments on it with potential workarounds (although none worked well for me 🤷‍♂️)
    • Since VSCode's JS intellisense is basically powered by TypeScript, this also breaks the autocompletion of import paths, since TS doesn't take the export map into account
  • TypeScript does not auto-append file extensions onto import paths when generating JS, which breaks the NodeJS OR Web consumer side (for ESM) if you forget them
    • Both NodeJS and the browser make it MANDATORY that file imports include the .js extension.
      • This might be a way around this, to enable extensionless imports
      • Sometimes referred to as fully qualified or fully resolved import paths
    • If you have import {myFunc} from './utils' in TypeScript, TSC will not automatically transform the statement to use ./utils.js, even if you specify the output format as ESM, etc.
    • Considered a wont-fix type issue: See #16577 and #42151
    • Solution: Annoying, but you need to either use a bundler that will handle this for you, or else manually also use {file}.js when importing in TypeScript. You can actually use .js in TS imports in your src without intellisense or TSC complaining.

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.

  • If running tsc if your only build task
    • Just use the --watch flag, as in tsc --watch. This automatically watches and incrementally builds based on the file inputs passed via CLI, or as inferred from your tsconfig.json
  • 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

  • If you want to quickly check all your files for Type errors, without compiling it, use the --noEmit option with TSC

TypeScript Config Import Aliases

TypeScript path import aliases are pretty neat: 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.

{
	"compilerOptions": {
		// ...
		"baseUrl": ".",
		// ...
		"paths": {
			"@core/*": ["internal/core/*"],
			"@core": ["internal/core"],
			"@shared/*": ["../shared/*"],
			"@shared": ["../shared"]
		}
	}
}

These mostly affect build output; if you are trying to use these at runtime (e.g., with something like ts-node), you will likely need to use something to resolve the paths, like the tsconfig-paths module (see this S/O answer for example setup).

If you run into issues where path aliases are only working for exports from within a very specific file from within the aliased directory, check to make sure that the main field in the package.json controlling that directory points to the highest level possible. Consider how module resolution works.

Markdown Source Last Updated:
Sun Sep 15 2024 01:21:26 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 19 2019 17:06:24 GMT+0000 (Coordinated Universal Time)
© 2024 Joshua Tzucker, Built with Gatsby
Feedback