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

Resources

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

Other:

Global install

npm install typescript -g

Quick init (create tsconfig.json)

tsc --init

Getting around index issues

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

window['globalVar'] = true;

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

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

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

Option A) Merging Interfaces (safest)

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

You can do this by using the following:

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

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

Option B) Individual casting to any

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

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

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

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

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

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

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

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

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

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

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

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

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

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

*.d.ts file:

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

declare var windowAsAny:WindowAsAnyType;

unknown Type (vs any)

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

Short Summary

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

Practical Application

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

let myUnknown: unknown;

myUnknown = 'Hello!';

// Throws error
myUnknown.toUpperCase();

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

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

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

let wronglyTyped = badFunction();

// Works
(wronglyTyped as any)

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

// Works
(wronglyTyped as unknown as number)

Deeper Analysis

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

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

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

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

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

Blood Types Analogy

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

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

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

Further reading

Object Index Types

TS Docs

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

interface FuncHolder {
	[index: string]: Function
}

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

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

An index must be either a string or number.

You can also extend Object explicitly:

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

Index by any

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

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

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

Or, combine with Record:

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

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

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

What about a mixed index type of string or number?

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

Only allowing certain keys

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

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

Or, different enum type:

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

Only allowing indexing by an original key of the object

Let's say we created an object like so:

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

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

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

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

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

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

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

Add a generic index signature

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

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

Use keyof

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

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

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

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

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

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

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

Thanks to this S/O Answer.

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

Issue with assignment when using keyof index on object

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

myIceCream[fk] = value;

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

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

Cast as any

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

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

Typing while destructuring

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

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

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

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

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

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

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

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

Typedef and advanced types

Advanced types docs

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

interface BasicPerson {
	age: number;
}

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

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

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

Function as a type

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

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

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

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

Strict null checks and query selectors

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

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.

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

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

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

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

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

Details


Type annotating object literals

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

Option A) Multi-part declaration

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

Option B) Cast prefix operator

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

Option C) Cast as operator

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

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

Enums

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

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

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

Casting Enum Option to String

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

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

Casting string to enum option

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

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

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

The above isn't always a working solution, especially when dealing with noImplicityAny. 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!

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

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.

Where this gets tricky is in trying to extend or alias a type in an ambient declaration file. Generally, first you need to import the type, then augment it, and if you want it global, you need to make sure you specify with the global namespace. Like so:

globals.d.ts:

import * as fs from 'fs';
// Or...
// import {Stats} from 'fs';

declare global {
	interface FullFileStats extends fs.Stats {
		isExecutable: boolean;
	}
}

Further reading:

Reminder: Don't mix up export patterns

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

*.d.ts:

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

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

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

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

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

  • typeRoots
  • types
  • baseUrl
  • paths
  • include

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

CommonJS Interoperability Issues

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

CommonJS Default Export Interop Issue

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

  • Try 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)

Relevant links:


Common things I forget / How do I:

  • Declare type for a value returned from a promise?

    • Use generics

      • Promise<string>
  • Default argument for a function

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

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

Type Guarding

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

Answer:

  • Often, in the code that we write, we introduce areas where the type becomes intentially 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.

Primitive guarding

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

For example:

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

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

Class guarding

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

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

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

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

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

Custom guarding

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

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

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

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

	return false;
}

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

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

Here is a full example, expanding on the above:

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

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

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

	return false;
}

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

NodeJS Specifics with TypeScript

Extending NodeJS Globals with TypeScript

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

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

Here is how you can use that approach:

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

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

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


Fun advanced types

Using a interface property (nested) as a type

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

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

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

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

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

Any, but exclude undefined or null

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

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

-- Hackish Solution --

You can very easily just define a union type:

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

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


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

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

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

Sample Input:

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

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

Option A: Use Pick

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

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

Option B: Leave off type declaration

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

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

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


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

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


Overloading / Overriding functions and methods

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

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

Basic rules of function overloading

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

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

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

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

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

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

    • This means both arguments and return types

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

Method overloading

In general, method overloading (functions in classes) work very similarliy 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".

Markdown Source Last Updated:
Wed May 27 2020 00:05:10 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 19 2019 17:06:24 GMT+0000 (Coordinated Universal Time)
© 2020 Joshua Tzucker, Built with Gatsby