Joshua's Cheatsheets
Light
help

TypeScript - Cheatsheet

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) 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 B) Explicit index signature on interface

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 explictly merging the interfaces. Like so:

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

Although it might seem counter-intuative 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;

Object Index Types

TS Docs

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

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

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.


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.


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

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:

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

Comparison

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

Filter on keys

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

Filter on values

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

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!

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.

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

Common things I forget / How do I:

  • Declare type for a value returned from a promise?

    • Use generics

      • Promise<string>
Markdown Source Last Updated:
Sun Nov 03 2019 02:45:33 GMT+0000 (Coordinated Universal Time)
Markdown Source Created:
Mon Aug 19 2019 17:06:24 GMT+0000 (Coordinated Universal Time)