💡 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:
- The TypeScript Docs / Handbook
- TypeScript FAQ
- rmolinamir/typescript-cheatsheet
- f0lg0/ts-extended-cheatsheet
- TS Playground / Sandbox
- Marius Schulz
- The Object Type in TypeScript
- Blog
- TypeScript-Weekly (Weekly Email Newsletter)
- TypeStrong: learn-typescript workshop
- Type Definitions:
- lib.dom.d.ts
- sindresorhus/type-fest
- DefinitelyTyped
- QuickType (Quickly create types from JSON)
- github.com/labs42io/clean-code-typescript
- https://omakoleg.github.io/typescript-practices/
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)
- tsx
- Alternative to
ts-node, which usesesbuildunder the hood - Does not type-check
- Strongly recommend as alternative to ts-node
- Does not offer ability to customize esbuild config
- Alternative to
- esrun
- Warning: Now considered deprecated (by maintainers) in favor of
bunortsx - Alternative to
ts-node, wrapsesbuild
- Warning: Now considered deprecated (by maintainers) in favor of
- swc-node
- Alternative to
ts-node, usesswc(Rust-based compiler) under the hood - Note:
ts-nodeactually supportsswcwith minimal setup now - see docs
- Alternative to
- esbuild
- Go based bundler, supports TS
- esno / esmo: Like
ts-node, but esbuild powered. Supports both CJS (esno) and ESM (esmo)- Deprecated in favor of
tsx
- Deprecated in favor of
- folke/esbuild-runner: Wrapper around 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)
No longer recommended:
- TS-Node (easy way to execute TypeScript without build step, for local dev)
- TypeStrong/ts-node
- Highly recommend using it with
transpileOnlyoption set to true.
- Highly recommend using it with
- wclr/ts-node-dev
- Watches files and restarts NodeJS on changes. Faster alternative to running ts-node with a watcher program (like
nodemon), but still uses ts-node under the hood
- Watches files and restarts NodeJS on changes. Faster alternative to running ts-node with a watcher program (like
- TypeStrong/ts-node
Global install
npm install typescript -gQuick init (create tsconfig.json)
tsc --initGetting 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
windowin 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:
- Anything is assignable to
any anyis 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.
anyis like being both a universal recipient (type AB+) and a universal donor (type O-)unknownis like being just a regular universal recipient (type AB+)- Can receive from all others, but can only donate to other
unknowns(orany)
- Can receive from all others, but can only donate to other
Further reading
Object Index Types
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
keyoftips.
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
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
Functionas a type is also enforced by the@typescript-eslint/band-typesrule
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[]) => anyHere 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);
}
}Grouping Types via Namespace
Namespaces in TypeScript can serve as a convenient way to logically group types together:
export namespace CoffeeShopPurchase {
// If you don't export, the type will only be internal to the namespace
type Cookie = {chocolateChips: boolean}
type Biscuit = {glutenFree: boolean}
// This will be accessible outside this scope
export type Snack = Cookie | Biscuit
}
// The only type we can access is those exported
type MyTypeFromNamespace = CoffeeShopPurchase.Snack
// ERROR: `has no exported member Cookie`
type MyTypeFromNamespace = CoffeeShopPurchase.CookieHowever, you should be careful not to overuse namespaces for a couple reasons:
- While simplifying the organization of types, they can introduce type-complexity as far as interacting with other types.
- They are something that (can be) TypeScript only - meaning a namespace might have no corresponding real object in the JavaScript runtime - which can make things confusing
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.jsonfilecompilerOptions:compilerOptions.types:- If
compilerOptions.typesis 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"]
- If
- 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/basesrepo (recommended)- Or, for starting from scratch
tsc --init
TSConfig In-Depth: Compiler Options
Resources:
- https://indepth.dev/configuring-typescript-compiler/
- https://www.typescriptlang.org/docs/handbook/compiler-options.html
- https://github.com/tsconfig/bases
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}.jsoninstead 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.jsonfile, 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.DOMfor frontend, noDOMfor backend.
⚠ 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
--includeoption on the CLI to match the config file; you pass the file(s) as the last option totsc, 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...
- E.g.
- 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
extendsfeature - For example, this is an easy way to split up compile options for different sets of files (e.g.
srcvs__tests__);files,include, andexcludewill 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 protectedacts likeprivate, but any deriving classes (viaextends) can also access the field- Making the constructor
protectedmeans that the class cannot be instantiated directly (outside its containing class), but can be extended
- Making the constructor
readonlyis supported, but requires that the value be assigned where declared, or in constructor- Getters and setters are supported; use
getandsetkeywords- 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 Constructor Type via Class Implementation
use typeof MyClassImplementation.
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 MyClassto justMyClass
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 = MyClassInferring 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);
// > greenCasting 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);
// > 2The 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);
// > 2Comparing 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);
// > trueOf 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);
// > trueGet 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:
- Docs: Enums at compile time
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
allowJsis set totruein yourtsconfig.jsonif 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)
importor anexportto 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 indeclare global {}. Thank you to this excellent S/O answer for clearing that up.
Do not use
declare global {}wrapper unless you have animportorexportin 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
- To make something globally available, without an explicit import, you now have to wrap the declaration in
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
skipLibCheckacts as a binary flag; setting it totruewill 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-packageto 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.skipLibCheckis not set totrue - 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
anyin 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.tsambient files as regular*.tsfiles that useexportto 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.tsor/types/my-type.ts - You can use
import/exportin*.d.tsto turn it into a module, but this isn't really best practice
- Manually copy your
*.d.tsfile(s) fromsrctodist(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
- Basarat: ambient/d.ts
- Official Templates
- Related topics:
- TrueJS: YouTube Tutorial (Starts at 3:41)
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.tsfile (remember, imports & exports automatically convert an ambient file into amodulefile)
- This happens automatically if you have a top-level import / export inside your
- If you aren't seeing your module augmentation being merged with the original (instead it is just replacing it), make sure you have an
importof the original module in the same file - Moving
importstatements inside a nesteddeclare 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 {}
- Either don't place namespace in module, or wrap with
Further reading:
- TSLang - declaration merging
- Helpful S/O answers:
- alligator.io - module augmentation
- Review how packages in
DefinitelyTypedhandle this
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:
typeRootstypesbaseUrlpathsinclude
Another thing to remember: module names must be quoted:
declare module "widget" {}, notdeclare 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');toconst myImport = require('my-lib').default; - If you are the library author, try changing
export default ___toexport = ____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:
- TS Issue #2719 (ES6 default exports interop)
- Related issue with Axios (#1975)
- StackOverflow comment about how to create working exported ambient types
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:
- As an author of the package:
- Compile / transpile to ES6 / ESM and include in distribution (plus point package at entry point)
- See notes on NPM package development and module options here
- Compile / transpile to ES6 / ESM and include in distribution (plus point package at entry point)
- As a consumer of the package:
- Use the CJS interop style import:
import myThing = require('my-lib'); - Try turning on
esModuleInterop - Try removing any aliases on the import
- Change
import * as myThing from 'my-lib'toimport myThing from 'my-lib'
- Change
- Use the CJS interop style import:
- Helpful links:
Common things I forget / How do I:
- Declare type for a value returned from a promise?
- Use generics
Promise<string>
- Use generics
- 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}) { // }
- Don't use the optional flag - just set equal to:
- Using syntax for generics
- Calling a method
- Use angle brackets to enclose the type
- Example:
let result = myFunc<IMyInterface>({...})
- Example:
- If type can be inferred, you might not need to declare it - e.g. just use
let result = myFunc('myStr');
- Use angle brackets to enclose the type
- 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) {}
- Example:
- Arrow functions are special!!!
- Example:
const myFunc = <T extends unknown>(input: T) => {}; - TSX specific example:
const MyComponent = <T,>(arg: T) => {}
- Example:
- 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; }
- Example:
- Use angle brackets with generic placeholders, and then annotate arguments with same placeholders
- Declaring types:
type TakeGeneric<T> = ...interface TakeGeneric<T> { ... }
- For more uses of generics (such as with classes), refer to the handbook.
- Calling a method
- Functions as types?
- Generally,
(ARGS) => RETURN_TYPE. - If you need to type an async function, use
(ARGS) => Promise<RETURN_TYPE>(notasync () ...) - 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
doublesatisfies the signature oftype 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
functionin a one-shot operation; you usually separately type the args and return type- Workaround - wrap function in parenthesis and then use
satisfies:export default (function double(i) { return i * 2; }) satisfies NumberOperation; - Ticket to support
satisfieskeyword - #51556 - Ticket to support
implementswith functions - #34319
- Workaround - wrap function in parenthesis and then use
- Arrow function:
- See above for using generic slots
- Generally,
- Define a constructor type signature outside of its implementation (e.g. in ambient declaration)
- Define an interface, and add a property that has a key of
new(constructorArgs): ClassInstance;. - See Docs: Interfaces -> Class Types, and this S/O
- Define an interface, and add a property that has a key of
- Multiple extends?
- Comma separate:
interface MultiInheritance extends Alpha, Bravo {}
- Comma separate:
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;
- For example, we might write a function that returns either an error or a success type:
- 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
asthe type needed - e.g.(result as SuccessResult).mySuccessMethod()
- A common bypass around this, without using type guards, is to simply cast
- 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 asstring | 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!
- See section above - "Child Property Narrowing via Stored 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/nodeif 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 ExtractedComplexType = 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
undefinedfrom a union, you can do so withExclude<MyUnionType, undefined>, without involving any of the below hacks. Same thing works for excludingnullfrom a union. For both, useExclude<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
bigintas 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
Omithelper, 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 betype 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 thatkeyof 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>ortype 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
instanceofto guard- E.g.
if (error instanceof CustomError && error.code === CustomErrorCodes.AuthFailed) {}
- E.g.
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 beforefunction handler(input: any)
- E.g.
- 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 (
UnhandledPromiseRejectionWarningbeing 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".
Function Type Unions
// NOPE!
export type SetState<T> = ((updated: T) => void) | ((cb: (current: T) => T) => void);
// yep!
export type SetState<T> = ((updated: T) => void) & ((cb: (current: T) => T) => void);https://stackoverflow.com/a/43673223/11447682
Troubleshooting / Assorted Issues
- Methods that return nullable unions (e.g.
string | null) don't seem to be getting checked properly (e.g. always inferred asT, when it should beT | null)- Make sure the
strictNullCheckscompiler option is set totrue. This affects both TS and JS checking. - This affects JSDoc too, even if you explicitly document a return type as a null union
- Make sure the
argument of type ____ is not assignable to parameter of type 'never'- This most often comes up when trying to
pushto an array, where the array was created without an explicit type. In those instances, TS will default tonever[].- 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 ofmyArr = [] - Cast before accessing:
(myArr as string[]).push('new string!'); - Set
strictNullCheckstofalse(😢)
- Explicitly type the array:
- To fix for arrays, you can...:
- 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
nevertype is inferred for something.- For example:
const emptyObj = {}; let propVal: keyof typeof emptyObj; propVal = 'hello'; // Type '"hello"' is not assignable to type 'never'
- For example:
- This most often comes up when trying to
- How to get the TypeScript compiler (tsc) to stop transpiling (especially with JS input to JS output)
- Make sure
targetis set to an ES version that corresponds with how you wrote your source code. For example, if you usedasync / await, then target needs to bees2017or higher to avoid transpiling to__awaiterpolyfills.
- Make sure
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 allowsundefinedto pass through! change toif (myObj.myProp !== undefined)
- Example: Trying to get nested object -
- 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
- Make sure you are using the right kind of check:
- TSC is not respecting the
rootDirsetting withoutDir, and producing an output withrootDirnested 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.jsonfrom{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'becomesconst 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 fileserror though.
- Easiest: Change ESM syntax to CommonJS -
- This is easy to do accidentally, if you do something like import
- TSC does not automatically exclude files outside
rootDirfor consideration- you can manually do this with
exclude,include, orfiles- I like
include, since it lets you use glob patterns as opposed tofiles
- I like
- You can check which files are being included by using
tsc --listFiles --noEmit
- you can manually do this with
- Make sure you are not importing / requiring any files outside the rootDir.
- I can't stop TSC from checking node_modules!
- First, try making sure you are using
includeand/orexcludeto properly indicate to TSC which files you want checked. If you are still getting TSC checking insidenode_modules, here are some more things to try:- Set
compilerOptions.typesto some value, at the very least an empty array ([]) - Set
compilerOptions.skipLibChecktotrue(be warned, this skips checking of all your*.d.tsfiles!) - Make sure that the version of TSC you are running is the same as in your dependencies; you can use
npx tscto make sure you use the local version instead of a globally installed TSC version - If you are using a
jsconfig.jsonas the project config (e.g. with--project jsconfig.json), try adding"maxNodeModuleJsDepth": 0to the config file (or pass via CLI flag), since TSC will default to2for JSConfig based projects instead of zero (see related open issue)
- Set
- You can use the
--traceResolutionand--listFilesflags to help troubleshoot why a file is getting included. Also, see the FAQ in the official Wiki. - The
--showConfigoption is also a helpful way to preview the effects ofinclude,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-ignorethat instance, or turnskipLibCheckon. However, to make matters worse,skipLibCheckwill stop TSC from type-checking ALL ambient*.d.tsfiles, not just those innode_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
skipLibCheckto only apply tonode_modules
- Unfortunately, in that scenario, the only fix is to either
- First, try making sure you are using
- Source maps are not getting resolved with debuggers
- Are you definitely outputting source maps? Make sure
sourceMapis on. - Manually check the
*.js.mapfiles 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
UnhandledPromiseRejectionWarningerrors 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
- I personally ran into a bizarre issue with TSC, where it compiled fine, but threw
- Additional resources:
- Excellent guide by Carl Rippon: "Emitting TypeScript Source Maps"
- Sentry: "Your Source Maps are Broken"
- Are you definitely outputting source maps? Make sure
- 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-nodelikeTypeError: myfile_1.myFunction is not a functionthat 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
- 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
- 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
- Check for a circular dependency!
- VSCode only shows error for currently open files, instead of all files
- You can use VSCode tasks +
problemMatcher, and manually run- See this S/O and this open issue thread for VSCode
- You can try out the experimental
enableProjectDiagnosticssetting (see this S/O)
- You can use VSCode tasks +
- 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.tsfiles), 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 toany. - 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
- Often the issue is exactly what the error is stating; your library author did not export types (e.g.
- 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 arequire()statement (orimport style = require(...)) - Barely any type safety: Keep
importstatement the same, but add an ambient declaration for*.cssfiles, 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
- No type safety (but error goes away): Change
- These approaches also work with things like
.svgfiles, 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 likewindow['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.myKeyinstead ofwindow['myKey']
- Try accessing through dot-notation, instead of with brackets:
- 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
.jsfiles in the same directory as your.tssource files, and with the same filenames. I'm not really sure if this would even be considered a bug, especially if you haveallowJsset to true.- Easiest workaround is to adopt a
/srcand/distbuild strategy - isolate your TS vs JS files. - Highly related: comments on issue #693
- Easiest workaround is to adopt a
- Do you have any conflicting JS files? This can happen if you are emitting
- How to preview the files that TSC will create, without actually creating them?
- I haven't found an exactly right approach (yet), but you can kind of use
tsc --build --dryfor this. You could also just have TSC emit to a temp or null location.
- I haven't found an exactly right approach (yet), but you can kind of use
- My emitted declaration files contain massive strings! E.g.
export const MyStr = "..."instead of justexport 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 asconst, OR, type assert / cast asstring- Example:
const MyStr: string = `Hello World`; - Example:
let MyStr = `Hello World`;
- Example:
- If you are using JavaScript + JSDoc, you can use something like this to type-assert
string:const MyStr = /** @type {string} */ (`Hello World`);
- 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
- TypeScript is trying to use NodeJS types (
@types/node) within a project where you actually want web types, and they are overridinglib.dom.d.ts. Perhaps this is causing issues withType 'Timeout' is not assignable to type 'number'forsetIntervalorsetTimeout- What has likely happened is that either yourself, or a dependency has brought in
@types/nodeinsidenode_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/nodeis excluded by settingcompilerOptions.typesto an array that excludesnodeas 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.setIntervalinstead of justsetInterval.
- Make sure
- What has likely happened is that either yourself, or a dependency has brought in
- 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'
- This often happen when accidentally using
- The IDE is showing TypeScript errors, but they don't match the tsconfig settings and
tscdoes 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 Versionand make sure that Use Workspace Version is selected instead of Use VS Code's Version. Or, manually edit yoursettings.jsonto have"typescript.tsdk": "node_modules/typescript/lib"(you might need to alter slightly)
- In VSCode, you can verify with
- If there have been recent changes to your
tsconfig.jsonfile, you might need to reload the workspace / IDE
- Verify that the version of TypeScript used by the IDE's live type-checking feature matches the one installed locally for this project.
'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();
});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-ignorecomment- Reducing "strictness" in compiler options, by...
- Disabling any of:
alwaysStrictstrictstrict___(example,strictNullChecks)noImplicit___(example,noImplicitAny)
- Turning on any of:
suppressExcessPropertyErrorssuppressImplicitAnyIndexErrors
- Reducing strictness across your entire codebase is highly recommended against. It is much better to use tiny escape hatches where necessary.
- Disabling any of:
TypeScript Type Casting / Assertion
- Asserting that something is is set (not undefined or null)
- Use a
!after the thingmyObj.prop!myObj!.propmyThing!
- The trailing
!is a post-fix expression operator assertion
- Use a
- Asserting a different type than what TS inferred
- In many cases (especially if
anyis inferred) you can use the simpleastype assertion:const strLength = (myVar as string).lengthconst 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:(wronglyTyped as unknown as string)- For more info on why this works and is required, please review my section on the "unknown" type
- In many cases (especially if
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
- VSCode Docs: "TypeScript Debugging with Visual Studio Code"
You can also use
ts-nodefor 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 thepackage.exportsfeature - Tracked as issue #33079, and there are some comments on it with potential workarounds (although none worked well for me 🤷♂️)
- Also relevant #8305
- TS team is well aware, and support is planned on rolling out in the
v4.3release on May 25th.
- 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
.jsextension.- 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}.jswhen importing in TypeScript. You can actually use.jsin TS imports in your src without intellisense or TSC complaining.- Walk-through: @ddprrt: TypeScript and ECMAScript Modules
- Although this fixes NodeJS ESM imports, it (including extensions) currently breaks
ts-nodein certain situations, e.g. with dynamic imports, or if target = CJS, will throwCannot find module- Tracked as ts-node Issue #1007, workaround.
- Both NodeJS and the browser make it MANDATORY that file imports include the
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
tscif your only build task- Just use the
--watchflag, as intsc --watch. This automatically watches and incrementally builds based on the file inputs passed via CLI, or as inferred from yourtsconfig.json
- Just use the
- Best solution: Use tsc-watch
- It starts
tscin watch mode, but also lets you execute any command of your choice when it rebuilds - Easy to use, fast
- It starts
- More manual: Use
nodemonas 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
*.tsfiles 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
--noEmitoption 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
mainfield in thepackage.jsoncontrolling that directory points to the highest level possible. Consider how module resolution works.