💡 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)
- TS-Node (easy way to execute TypeScript without build step, for local dev)
- TypeStrong/ts-node
- Highly recommend using it with
transpileOnly
option 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
- tsx
- Alternative to
ts-node
, which usesesbuild
under the hood - Does not type-check
- Strongly recommend as alternative to ts-node
- Alternative to
- esrun
- Alternative to
ts-node
, wrapsesbuild
- Alternative to
- swc-node
- Alternative to
ts-node
, usesswc
(Rust-based compiler) under the hood - Note:
ts-node
actually supportsswc
with 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
) - 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)
Global install
npm install typescript -g
Quick init (create tsconfig.json)
tsc --init
Getting around index issues
Unknown Index Signature
A common issue with TS is index methods, for example, with the global window
object. If an object has no declared index signature, TS can't know the return type when accessing by a key, so any
is inferred (details). In certain environments, trying the follow code will throw an error:
window['globalVar'] = true;
Element implicitly has an 'any' type because index expression is not of type 'number'.ts(7015)
If you are OK with less strict type checking, this is usually easily fixed by adding "suppressImplicitAnyIndexErrors": true
to the tsconfig.json
file.
For keeping strict type checking, you have a few options:
Option A) Merging Interfaces (safest)
Merging interfaces is the safest way to do this, because it preserves the original window
in its full form (as defined in lib.dom.d.ts
) and then augments it with your additions.
You can do this by using the following:
// If using an ambient declaration file only for types
interface Window {
myProperty: string;
myMethod: () => {};
}
// If your ambient declaration file uses `import` or `export`, you need to wrap with `global`:
declare global {
interface Window {
myProperty: string;
myMethod: () => {};
}
}
If you are trying to strongly-type process.env
values (environment variable values) in NodeJS, target ProcessEnv
:
namespace NodeJS {
interface ProcessEnv {
AUTH_TOKEN: string;
NODE_ENV: 'development' | 'production';
DEBUG_PORT?: string;
}
}
// Same rule as above applies - if this file contains an `export` or
// `import` keyword, then the above most be wrapped in `declare global {}`
Option B) Individual casting to any
In each place where you reference window
(or the object with an unknown index signature), you can cast to any
:
(window as any)['globalVar'] = true;
Option C) Explicit index by any-string signature on interface
Warning: This whole option (adding a generic index signature) is less safe than any of the above approaches, but lets you touch
window
in a very ad-hoc way without having to explicitly define each property.
As outlined in this SO answer, the usual solution is to simply tell TS about the index signature:
interface Window {
[index:string]: any;
}
Note
[key:string]
is the same as[index:string]
- TS actually doesn't care about the word used; the syntax is what matters.
Note: You might be wondering why we don't also declare
[index:number]
. This is because JS actually only uses a string index on objects! It will call.toString()
automatically on any index you pass!
However, in some environments, this still might not satisfy TS. Another thing you can try is explicitly merging the interfaces. Like so:
interface Window {
[index:string]: any;
}
declare var window:Window;
window['globalVar'] = true;
Although it might seem counter-intuitive at first, this is actually be a less sound solution than just casting window to any
before picking off a value. The reason that here, you are polluting the global Window interface, and suddenly every window property becomes any
, regardless if TS already knew a type beforehand.
One in-between solution is to merge the interface, but only on a new variable interface, thus leaving the global window
alone:
interface WindowAsAny extends Window {
[index:string]: any;
}
var windowAsAny:WindowAsAny = window;
windowAsAny['globalVar'] = true;
Or, if you wanted to take advantage of ambient declaration files to make this available in any TS file, you could do something like...
*.d.ts file:
declare interface WindowAsAnyType extends Window {
[index:string]: any;
}
declare var windowAsAny:WindowAsAnyType;
unknown
Type (vs any
)
Introduced with version 3.0 of TS, unknown
has been growing in popularity, and for good reason; it is a much safer alternative to using any
for a type that is unknown.
Short Summary
In the shortest explanation, both any
and unknown
can accept any value, but unknown
can only be assigned to other unknown
variables or any
variables - assigning it to an actual type requires a type guard to make it not unknown.
Practical Application
In practical use, whenever a value is unknown
, we need to do something to explicitly detect type or else TS will complain. For example:
let myUnknown: unknown;
myUnknown = 'Hello!';
// Throws error
myUnknown.toUpperCase();
if (typeof myUnknown === 'string') {
// Works, because we explicitly checked type
myUnknown.toUpperCase();
}
Another use for unknown
, which usually should be avoided, is as an escape hatch when TS has the wrong inferred or explicit type. Sometimes this is needed if a third-party library incorrectly cast a return type somewhere:
/**
* Pretend this is bad third party code,
* that returns a number, but,
* tells TS it returns a string
*/
function badFunction(): string {
const num = 2;
return num as any;
}
let wronglyTyped = badFunction();
// Works
(wronglyTyped as any)
// Fails: "neither type sufficiently overlaps"
(wronglyTyped as number)
// Works
(wronglyTyped as unknown as number)
Deeper Analysis
An easy way to start thinking about unknown
is first thinking about the danger of any
. The any
type says two very important things:
- Anything is assignable to
any
any
is assignable to anything
TS basically says that any
is a shape-shifter: it can convincingly become anything, on both the inside and outside, and therefore can be accepted anywhere. Maybe that is a good way to think of any
, as a creepy imitator that can sneak past checkpoints completely undetected; it should be avoided whenever possible.
Unknown, on the other hand, follows the first rule of any
, accepting any value, but breaks the second rule - it is only assignable to any
or unknown
typed variables.
Continuing our shape-shifter example, this is TS saying that unknown
is also a shape-shifter, but only on the inside; on the outside, it has a big question mark on its shirt, and our checkpoints need to stop it and ask for ID to see what it really is.
Blood Types Analogy
A nice fitting analogy is blood types, if you are familiar with those.
any
is like being both a universal recipient (type AB+) and a universal donor (type O-)unknown
is like being just a regular universal recipient (type AB+)- Can receive from all others, but can only donate to other
unknowns
(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
keyof
tips.
Issue with assignment when using keyof index on object
A problem with both of the above solutions is that they still don't like assignment. If you try to do:
myIceCream[fk] = value;
You'll get kind of a cryptic error like Type 'string' is not assignable to type 'never'.
.
This is where it starts to get really complicated, to the point of this not being a very clean solution. This might be a place to start if you are looking to use this.
Cast as any
For a lazy solution, we can just explicitly cast as any, which is basically just telling TS that we are aware we are doing something less safe, and the shape of myIceCream
could be anything, including something indexable by key
.
Object.keys(myIceCream).forEach((key, val) => {
(myIceCream as any)[key] = val;
});
Typing while destructuring
There are two main options for declaring types while destructuring. First, lets pretend we have an object that TS can't infer types from, maybe the result of JSON.parse():
let book = JSON.parse(`
{
"name": "Frankenstein",
"author": "Mary Shelley",
"release": 1818
}
`);
If we want to destructure, and grab name
, author
, etc., one option is with explicit types inline with the assignment:
const { author, name, release }: {
author: string,
name: string,
release: number
} = book;
Another option is to simply tell TS about the structure of the incoming object, so TS can infer types automatically:
interface Book {
name: string,
author: string,
release: number
}
let book:Book = JSON.parse(`
{
"name": "Frankenstein",
"author": "Mary Shelley",
"release": 1818
}
`);
// Since book is properly type annotated with full types, we can safely destructure without accidentally getting `any` as inferred type
const { author, name, release } = book;
Typedef and advanced types
If you are describing an object, you probably want to use interface
:
interface BasicPerson {
age: number;
}
If you are trying to set up a named alias for what a complex type looks like, you can use type
aliases (docs):
/* Bad - you really should use an interface for this... */
type BasicPerson = {
age: number;
}
/* This is more normal usage - things like unions, complex types, aliasing long types, etc. */
type Person = ThirdPartyLib.SubClass.SubClass.Enum.Value;
type NullablePerson = Person | null;
From the TypeScript docs: Because an ideal property of software is being open to extension, you should always use an interface over a type alias if possible.
Function as a type
Although you are free to use Function
as a type, such as:
let upperCaser: Function;
upperCaser = (t:string)=>t.toUpperCase();
... it is recommended to fill in the full function signature as the type (this can reduce implicit issues, as well as prevent errors related to mismatched signatures):
let upperCaser: (input:string)=>string;
upperCaser = (t:string)=>t.toUpperCase();
Not using
Function
as a type is also enforced by the@typescript-eslint/band-types
rule
You can also declare and use explicit function typedefs, like so:
type CustomFuncType = (input: string) => boolean;
const customFuncImplementation: CustomFuncType = (input) => !!input;
Function as Generic Type
For accepting a function type as a generic, you can use the extends
keyword to do something like this:
F extends (...args: any[]) => any
Here are some examples:
type Promisify<F extends (...args: any[]) => any> = (...args: Parameters<F>) => Promise<ReturnType<F>>;
function executeAnyFunctionTenTimes<F extends (...args: any[]) => any>(
functionToRunTenTimes: F,
...argsToRunWith: Parameters<F>
) {
for (let x = 0; x < 10; x++) {
functionToRunTenTimes.apply(functionToRunTenTimes, argsToRunWith);
}
}
Strict null checks and query selectors
Strict null checks (e.g. disallowing the assignment of something that could be null to a non-null typed var) can cause issues, and for me, that came up around query selectors, like document.getElementById()
. If I'm doing something like writing a SFC, I can be pretty dang sure that an element with the ID I want exists, since I control both the TS and the DOM, but TypeScript does not know that. And it is a lot of extra boilerplate to wrap all those query selector calls in IF()
statements and turning strictNullChecks
off globally might not be the best solution.
A quick workaround is to use !
as a post-fix expression operator assertion - a fancy way of saying that putting !
at the end of a variable asserts that the value coming before it is not null.
Here is it in action, with code that will not error with strictNullChecks enabled:
let myElement: HTMLElement = document.getElementById('my-element')!;
Including external type definition files (e.g. @types/...
)
For telling TSC about external type definitions, there are a few options.
- Easiest: Control automatic imports through your
tsconfig.json
filecompilerOptions
:compilerOptions.types
:- If
compilerOptions.types
is not defined, then TSC will auto-import any@types/___
defs from node_modules. - If it is defined, it will only pull what you specify. E.g.:
types: ["@types/googlemaps"]
- 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/bases
repo (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}.json
instead of{NAME}.tsconfig.json
, VSCode will automatically recognize it as a valid TS Config file and provide automatic schema intellisense
💡 If you create a
*.tsconfig.json
file, add"$schema": "https://json.schemastore.org/tsconfig"
as a top level property so you get intellisense 🤓
Multiple TS Config Files with VSCode
Unfortunately, as far as Intellisense and in-IDE type-checking goes, VSCode only allows for one tsconfig.json
file per directory (AFAIK) - this could be considered the "root level" config of that specific directory. I have seen multiple comments like this one, that seem to indicate that VSCode resolves config mapping by searching from the bottom up, so keep that in mind when designing a system that uses multiple configs. For example, a standard nested approach might be something like:
.
├── tsconfig.json
├── frontend/
│ └── tsconfig.json
└── backend/
└── tsconfig.json
💡 You can even combine this with a VSCode multi-root workspace for maximum effect
💡 This approach is really useful for projects that have areas of code that need different
lib
s available - e.g.DOM
for frontend, noDOM
for 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
--include
option 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
extends
feature - For example, this is an easy way to split up compile options for different sets of files (e.g.
src
vs__tests__
);files
,include
, andexclude
will always override when extending a config
Type annotating object literals
If you have a lot of objects with a repeated structure, obviously the best solution is to write a common interface (DRY 🧠). But what if you just want to quickly use an object literal, such as when returning an object from a function? Of course, you could just use the normal object literal without using types, and TS will infer them, but what if you want type annotations?
Most people probably prefer Option C for its more concise and readable format (and compatibility with
tsx
)
Option A) Multi-part declaration
let user: {
name: string,
openTabs: number,
likesCilantro: boolean
} = {
name: 'Joshua',
openTabs: 81,
likesCilantro: true
};
Option B) Cast prefix operator
let user = {
name: <string> 'Joshua',
openTabs: <number> 81,
likesCilantro: <boolean> true
}
Option C) Cast as
operator
This is required with TSX (JSX), since Option B will not work.
let user = {
name: 'Joshua' as string,
openTabs: 81 as number,
likesCilantro: true as boolean
}
Classes
📄 Main Doc: Handbook - Classes
👉 Given the overlap, you might also be interested in the cheatsheet on JS Classes
Key things to note about TS classes:
- Everything is public by default, making the modifier not required, unless you want something different
- You can use
private {name}
modifier, or JS's#{name}
syntax for private fields, but they don't function exactly the same protected
acts likeprivate
, but any deriving classes (viaextends
) can also access the field- Making the constructor
protected
means that the class cannot be instantiated directly (outside its containing class), but can be extended
- Making the constructor
readonly
is supported, but requires that the value be assigned where declared, or in constructor- Getters and setters are supported; use
get
andset
keywords- These require output > ES5
- TypeScript supports "Parameter Properties", which are an awesome way to initialize and assign class members directly via the constructor arguments (see section below)
TS Classes - Parameter Properties
As noted above, Parameter Properties are a special TypeScript feature that lets you initialize and assign class members directly in the function. This can replace a bunch of code that previously was 100% boilerplate. For example:
// Standard way of assignment
// Notice how much extra code we have to type out that is redundant! We are double typing!
class Person {
private id: number;
name: string;
age: number;
constructor(id: number, name: string, age: number) {
this.id = id;
this.name = name;
this.age = age;
}
}
With Parameter Properties:
// Lot less code!
class Person {
constructor (private id: number, public name: string, public age: number) {}
}
Shout-out to Steve Fenton for this blog post on the topic.
If you are interested in assigning to a class the destructuring results of an object, passed as a an argument to the constructor, See this S/O answer and this open issue.
TS Classes - Constructor and Instance Types
Getting Instance Type from Uninstantiated Class
Some might also be basically doing a "reverse typeof" operation, where you need to go backward from
type = typeof MyClass
to 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 = MyClass
Inferring Constructor Parameters from a Class
TypeScript has a utility type that makes this easy - ConstructorParameters<T>
.
For example:
const wrappedFormData = (...args: ConstructorParameters<typeof window.FormData>) => {
console.log('FormData constructor called', args);
return new FormData(...args);
}
TS Classes - Expression is Not Constructable
Occasionally, you might run into this type of error:
This expression is not constructable.
Type 'MyClass' has no construct signatures.
ts(2351)
Usually this stems from one of a few different options.
The first option, and most frequent mistake, is that you are accidentally referring to an instance of the class instead of the uninstantiated class (constructor) itself. In that case, you want to use typeof MyClass
.
Here is a complete example, showing the error inducing mistake, and then the corrected version (playground):
Show Example
// Pretend this is valid class, from somewhere else
const ImportedClass: any = null;
declare class MyClass {
sayHi(): void;
}
/**
* Error example - missing `typeof`
* This is referencing the instance type, not the raw Class
*/
const WronglyTypedClass: MyClass = ImportedClass;
const wronglyTypedClassInstance = new WronglyTypedClass();
// ^ - Error: This expression is not constructable
/**
* _Correct_ example - uses typeof
* References the Class _type_, not instance
*/
const CorrectlyTypedClass: typeof MyClass = ImportedClass;
const correctlyTypedClassInstance = new CorrectlyTypedClass();
// Success!
correctlyTypedClassInstance.sayHi();
The second option, although much less likely, is that it could be exactly what the error says; the actual type definition for the type of MyClass
is missing a constructor.
This could happen if you accidentally typed your class as an interface
, but otherwise shouldn't happen with declare class
code - TS will automatically inject a void constructor type, as that is what JavaScript does anyway
- In JavaScript, providing a manual constructor is not required, and if omitted, a default constructor is used.
- Example: This is completely valid, and has an auto-injected constructor signature
declare class MyClass {}
TS Classes - Type Definitions for Classes and Constructors
Crafting TypeScript for classes, especially as pure type-definitions and not through actual implementation code, can be tricky.
One particular catch is that if you try to create a class type with class
, you can't specify the return type - you will get a Type annotation cannot appear on a constructor declaration error. An alternative declaration syntax is to declare it as an interface with a new
property:
declare global {
const Tree: {
new(serializedTree: string): HydratedTree;
}
}
const tree = new Tree('A -> B');
Enums
A reminder about how enums work in TS (ignoring string enums for a second):
Enums are converted in TS to an object with both string and numerical keys (so value can be looked up by either value or key):
enum colors {
'red',
'green',
'blue'
}
// becomes, at run-time
colors = {
0: 'red',
1: 'green',
2: 'blue',
red: 0,
green: 1,
blue: 2
}
Casting Enum Option to String
When you reference an enum value by key, TS returns a number. To get the string, you actually feed the number back into the number, to lookup the string by number key.
enum colors {
'red',
'green',
'blue'
}
let colorGreenString:string = colors[colors.green];
console.log(colorGreenString);
// > green
Casting string to enum option
Assuming a string based enum, we might want to take an input string and check if matches an enum key, or cast to get the integer value that corresponds. How do we do this?
Since an enum is mapped by both string and numerical index, this can be pretty easy. Essentially we are looking up the integer based on the string key. The casting of the lookup string to "any" might not be necessary depending on your TS settings.
enum colors {
'red',
'green',
'blue'
}
let colorBlueEnumVal:number = colors['blue'];
console.log(colorBlueEnumVal);
// > 2
The above isn't always a working solution, especially when dealing with noImplicitAny
. This seems to be the most accepted solution at the moment:
enum colors {
'red',
'green',
'blue'
}
let colorBlueEnumVal:number = colors[('blue' as keyof typeof colors)];
console.log(colorBlueEnumVal);
// > 2
Comparing Enum Values
If we just want to check if a string is equal to an enum value or not, we can use the trick above to convert our enum to a string and then compare:
enum colors {
'red',
'green',
'blue'
}
let pick = 'green';
let match = pick === colors[colors.green];
console.log(match);
// > true
Of course, you could also convert on the key side - however, this tends to be a little trickier, especially with noImplicitAny
:
enum colors {
'red',
'green',
'blue'
}
let pick = 'green';
let match = colors[pick as keyof typeof colors] === colors.green;
console.log(match);
// > true
Get Typescript Enum as Array
Since enums are stored as an objet with both string and numerical keys (see warning above) you need to filter the object while converting it to an array, so you don't get mixed duplicates (ref). Two options for converting a TypeScript enum to an array:
Enum to Array - Filter on keys
function enumToArr(enumInput){
return Object.keys(enumInput).filter(key=>isNaN(Number(key))===true);
}
Enum to Array - Filter on Values
function enumToArr(enumInput){
return Object.values(enumInput).filter(val=>isNaN(Number(val))===true);
}
String Enums
A string enum looks like this:
enum Books {
WizardOfOz = 'wizard-of-oz',
AliceInWonderland = 'alice-in-wonderland'
}
One of the problems with string enums in TS is that they do not get an automatic reverse-mapping generated for them at runtime, unlike other enum types. So, if I have a string and want to look up the enum key, I'm going to run into some issues...
Here are some options open to us:
- Use a union type instead - this is my personal preference whenever possible, as these are often very readable as well - s/o
- Build the enum as a plain JS object instead, and skip TS - s/o
- Build our own reverse mapping, keep TS enum - s/o
String Enums - Comparing Values
For comparison, as opposed to a reverse lookup, things are a little easier. Basically, all you need to use is {string value corresponding to enum key} as EnumName
.
Here is a full example, using the Books
enum from above:
function getRandomBook() {
return Math.random() < 0.5 ? 'wizard-of-oz' : 'alice-in-wonderland';
}
const pick: Books = getRandomBook() as Books;
if (pick === Books.AliceInWonderland) {
console.log('Down the rabbit hole!');
}
else {
console.log("I don't think we're in Kansas anymore...");
}
TypeScript Enums - Further reading:
- 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
allowJs
is set totrue
in yourtsconfig.json
if you are importing JS!
* 📖 - This (post from Atomist) is a great summary of all the different options for addressing TS7016 / "Could not Find Declaration File" error
Legacy JS Ambient Declaration File - Example:
Let's pretend we have a legacy JS file we need to import into TS.
lib/legacy-javascript.js
:
class SimpleClass {
constructor(yourName) {
this.name = yourName;
}
greet() {
return "Hello " + this.name;
}
static sayGoodbye() {
return "Goodbye on " + new Date().toLocaleDateString();
}
}
export default SimpleClass;
export function multiple(inputA,inputB){
return inputA * inputB;
}
export var creator = 'Joshua';
We can create a *.d.ts
file with any name, but to make it easier to find later, we'll follow the same pattern:
legacy.d.ts
:
declare module "lib/legacy-javascript" {
export default class SimpleClass {
constructor(yourName:string);
greet(): string;
static sayGoodbye(): string;
}
export function multiple(a:number,b:number): number;
export var creator:string;
}
Note that you don't have to do it like this, wrapping all your declarations as exports within a declared module - you can use top-level, and use as many declares as you would like. The issue really has to do with scope and file resolution.
Essentially, if you don't use the named module pattern, you are likely to end up polluting the global type scope, which is usually a bad thing. Sometimes it is necessary or helpful though, especially when dealing with injected framework globals.
WARNING: If you add an (top-level)
import
or anexport
to your ambient declaration file, it changes from acting like a script (which adds to the global scope) to acting like a module (which keeps local scope). In that scenario, to make types truly global, you have to put wrap them indeclare global {}
. Thank you to this excellent S/O answer for clearing that up.
Do not use
declare global {}
wrapper unless you have animport
orexport
in the file.
Ambient Declarations In-Depth
Multiple Modules
Within an ambient declaration file it is possible to declare multiple modules, generic modules (covering infinity files matching the pattern), and even declare a module and then import that same module into another.
To write a module declaration that covers a generic set of files, you write the module path with a glob, like this:
declare module 'my-lib/shared-configs/*.js' {
const config: IConfig
export = config;
}
When declaring modules, the declarations will take effect even within the same file, can be imported into other declarations. Here is an example:
declare module 'my-lib/nested/delta' {
// ...
}
declare module 'my-lib' {
// This will pick up the types declared above
import Delta = require('my-lib/nested/delta');
// ...
}
Ambient Declarations - Ambient Module Declarations CommonJS Interop
If you are using ambient module declarations, you need to be particularly careful about import / export syntax, and how default
is used.
For example, if you are trying to provide types for a CommonJS library, without turning on the special interop setting, and the library uses a singular root default export, then you need to make sure you have:
declare module 'js-lib' {
export = class Main {
// ...
}
}
... and NOT export default class Main {}
. And the consuming files should use the import Main = require('js-lib');
syntax.
Ignoring these rules will likely result in static type issues, like getting typeof import
as the only type, or even runtime exceptions, such as not being able to find the module if you used the wrong import syntax.
Ambient Declarations - Declared Module Not Getting Picked Up Inside Global Module
An issue I've run into in the past is trying to mix import
, declare global
, and declare module
, all within the same ambient declaration file.
The root problem is that when you add an import
statement to a *.d.ts
declaration file, the following happens:
- The file stops acting like a top-level script (global), and starts acting like an external module (local). This in turn leads to the following:
- To make something globally available, without an explicit import, you now have to wrap the declaration in
declare global {}
- If you have ambient module declarations -
declare module "my-module" {}
- inside the file, they will all of a sudden stop working
- 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
skipLibCheck
acts as a binary flag; setting it totrue
will let you ignore some global conflict issues (but not all), but will also completely disable type-checking related to ambient declaration files.
The main options right now would be to:
- Hand-code the exact correct global type in your own global file, to override the conflicting third-party definitions
- Use something like
patch-package
to remove the troublesome definitions from your dependencies
Ambient Declarations - Common Issue: Not Getting Picked Up
If your ambient declaration file does not seem to be getting picked up, here are a few things to check
- Make sure
compilerOptions.skipLibCheck
is not set 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
any
in the consumers...
How to Include Ambient Declaration Files in Compiled Output
Ironically, the frequent answer to the question of how to get the TSC to copy *.d.ts
files to output is... to not write them as *.d.ts
ambient declarations!
To clarify, your options are:
- Rewrite
*.d.ts
ambient files as regular*.ts
files that useexport
to share types with other files- Only downside is that you have to write import statements for types you want to use
- This is actually how most libraries seem to handle it, often using
types.ts
or/types/my-type.ts
- You can use
import
/export
in*.d.ts
to turn it into a module, but this isn't really best practice
- Manually copy your
*.d.ts
file(s) fromsrc
todist
(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.ts
file (remember, imports & exports automatically convert an ambient file into amodule
file)
- 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
import
of the original module in the same file - Moving
import
statements 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
DefinitelyTyped
handle 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:
typeRoots
types
baseUrl
paths
include
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
double
satisfies 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
function
in 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
satisfies
keyword - #51556 - Ticket to support
implements
with 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
as
the 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/node
if you are using TypeScript with NodeJS
Extending NodeJS Globals with TypeScript
The answer to this is almost identical to what I outlined for extending the global window
object and getting around index issues.
To summarize, the best option (for keeping type-safety) is to merge interfaces. For NodeJS, this also involves merging the namespace, since that is how Global
is typed (NodeJS.Global
).
Here is how you can use that approach:
// If in a pure types file (not module):
declare namespace NodeJS {
interface Global {
myProperty: string;
myMethod: () => {};
}
}
// If in a file that uses `import` or `export`, that is a "module", and you need to wrap the namespace merge with `global` to signify that the change should affect *everywhere* and not just module
declare global {
declare namespace NodeJS {
interface Global {
myProperty: string;
myMethod: () => {};
}
}
}
Also, this S/O answer is a great summary.
Fun advanced types
Using a interface property (nested) as a type
This is so cool! You can re-use a property of an interface as a standalone type, simply by accessing via bracket notation. This is a lifesaver when a third-party library only exports interfaces, but you need to get to a nested type!
interface Person {
name: string;
age: number;
email: {
address: string;
vendor: string;
lastSent: number;
hasBounced: boolean;
}
}
// Easy!
type Email = Person['email'];
// You can even grab nested!
type EmailVendor = Person['email']['vendor'];
Thank you to StackOverflow, for the billionth time, for the solution
Using the type accessed by a generic index
This is similar to the goal of the above trick; given something like
interface MyInterface {
[k: string]: MyComplexType;
}
How do we extract the type that is allowed for generic indexes (MyComplexType
)?
You can use the keyof
(index type query operator):
interface MyInterface {
[k: string]: MyComplexType;
}
type ExtractedComplextType = MyInterface[keyof MyInterface];
It is important to note that this solution also applies to interfaces that do not explicitly have a generic index signature, but where you are trying to extract the type that is the union of all possible values represented by keyof T
lookups. For example:
interface Person {
name: string;
age: number;
email: {
address: string;
vendor: string;
lastSent: number;
hasBounced: boolean;
}
}
type PersonVal = Person[keyof Person];
/*
Extracted type contains *all* possible nested types that could be accessed through a key:
type PersonVal = string | number | {
address: string;
vendor: string;
lastSent: number;
hasBounced: boolean;
}
*/
Using an Array Element as a Type
This is similar to the above trick, as it also uses bracket notation to re-use a nested type.
interface Person {
name: string;
computingDevices: ComputingDevices;
}
type ComputingDevices = Array<{
model: string;
isActive: boolean;
lastPing: number;
}>;
// We can reuse array element easily
const compy386: ComputingDevices[number] = {
model: 'Compy 386',
isActive: false,
lastPing: 1100505600
};
// We can even combine with the nested type index trick
const lappy486: Person['computingDevices'][number] = {
model: 'Lappy 486',
isActive: false,
lastPing: 1246345200
};
// You can also index by 0
type ExtractedFirstElemType = ComputingDevices[0]
Any
, but exclude undefined
or null
If you are looking to exclude
undefined
from a union, you can do so withExclude<MyUnionType, undefined>
, without involving any of the below hacks. Same thing works for excludingnull
from 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
bigint
as part of the union type. Needing to update primitives is another downside to this approach.
Omit From a Union, Using Exclude
How do you specify a type that is equal to an existing union, but with certain elements removed? Or, put another way, omitting certain elements from a specific union?
With the useful Exclude
helper!
It looks like Exclude<Type, ExcludedUnion>
, and you can use it like so:
type vehicles = 'boat' | 'plane' | 'car' | 'bus' | 'truck';
type automobiles = Exclude<vehicles, 'boat' | 'plane'>;
Fun fact, the
Omit
helper, for interfaces, builds on this helper!
Exhaustive Array Checks
Exhaustive checks in TS tend to be notoriously tricky, but for enforcing an exhaustive array declaration of strings, there is a shortcut to avoid trickier approaches, which is to use Tuple types.
// This is a Tuple type
type RequiredMeals = ['Breakfast', 'Lunch', 'Dinner'];
// ERROR: not assignable to type 'RequiredMeals'. -- this is missing Dinner!
const exhaustiveArr: RequiredMeals = ['Breakfast', 'Lunch'];
💡 Tip: You can re-use Tuples as unions, by using a number index, with
type DerivedTuple = MyTuple[number]
. In the above example, that would 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
instanceof
to 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 (
UnhandledPromiseRejectionWarning
being one of them)
Method overloading
In general, method overloading (functions in classes) work very similarly to function overloading. However, if you extend
a base class, you might run into some interesting issues around overriding methods.
The biggest catch comes with arrow functions (common in React classes):
"Since they are not defined on the prototype, you can not override them when subclassing a component"
-@oleg008 - Arrow Functions in React
This is also why you can't call members that are defined as arrow function class properties with super
when you are subclassing / extending. Actually, in general there are a lot of caveats - see "arrow functions in class properties might not be as great as we think".
Troubleshooting / Assorted Issues
- Methods that return nullable unions (e.g.
string | null
) don't seem to be getting checked properly (e.g. always inferred asT
, when it should beT | null
)- Make sure the
strictNullChecks
compiler 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
push
to 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
strictNullChecks
tofalse
(😢)
- 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
never
type is inferred for something.- For example:
const emptyObj = {}; let propVal: keyof typeof emptyObj; propVal = 'hello'; // Type '"hello"' is not assignable to type 'never'
- 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
target
is set to an ES version that corresponds with how you wrote your source code. For example, if you usedasync / await
, then target needs to bees2017
or higher to avoid transpiling to__awaiter
polyfills.
- 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 allowsundefined
to 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
rootDir
setting withoutDir
, and producing an output withrootDir
nested instead of flat- Make sure you are not importing / requiring any files outside the rootDir.
- This is easy to do accidentally, if you do something like import
package.json
from{projectRoot}/src
. - There are some workarounds for this, if you can use a relative path that doesn't change once it outputs to
outDir
:- Easiest: Change ESM syntax to CommonJS -
import * as PackageRaw from '../package.json'
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 files
error 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
rootDir
for 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
include
and/orexclude
to 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.types
to some value, at the very least an empty array ([]
) - Set
compilerOptions.skipLibCheck
totrue
(be warned, this skips checking of all your*.d.ts
files!) - Make sure that the version of TSC you are running is the same as in your dependencies; you can use
npx tsc
to make sure you use the local version instead of a globally installed TSC version - If you are using a
jsconfig.json
as the project config (e.g. with--project jsconfig.json
), try adding"maxNodeModuleJsDepth": 0
to the config file (or pass via CLI flag), since TSC will default to2
for JSConfig based projects instead of zero (see related open issue)
- Set
- You can use the
--traceResolution
and--listFiles
flags to help troubleshoot why a file is getting included. Also, see the FAQ in the official Wiki. - The
--showConfig
option is also a helpful way to preview the effects 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-ignore
that instance, or turnskipLibCheck
on. However, to make matters worse,skipLibCheck
will stop TSC from type-checking ALL ambient*.d.ts
files, 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
skipLibCheck
to 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
sourceMap
is on. - Manually check the
*.js.map
files that TSC is emitting - Any chance you have any malformed function overloads?
- I personally ran into a bizarre issue with TSC, where it compiled fine, but threw
UnhandledPromiseRejectionWarning
errors and threw uncaught exceptions that VSCode could not trace back through source maps (it would break inside generators); the issue ended up being a malformed function signature override
- 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-node
likeTypeError: myfile_1.myFunction is not a function
that don't apply- Check for a circular dependency!
- A clue that this might be the case is if the error only throws for a subset of files, or even a single file with
ts-node
- 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
enableProjectDiagnostics
setting (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.ts
files), and you don't have any local declarations to fill in the gaps. With strict rules, this will get flagged, since TS has to default 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
import
statement the same, but add an ambient declaration for*.css
files, like so - Type-Safe CSS (!): Use a Webpack plugin that generates type definitions for your stylesheets, automatically! You can find some great guides on how to accomplish this here and here
- No type safety (but error goes away): Change
- These approaches also work with things like
.svg
files, assuming you have a bundler that is transforming the files into importable strings
- Getting the
Element implicitly has an 'any' type because index expression is not of type 'number'.ts(7015)
error on things 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.myKey
instead 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
.js
files in the same directory as your.ts
source files, and with the same filenames. I'm not really sure if this would even be considered a bug, especially if you haveallowJs
set to true.- Easiest workaround is to adopt a
/src
and/dist
build strategy - isolate your TS vs JS files. - Highly related: comments on issue #693
- 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 --dry
for 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'
forsetInterval
orsetTimeout
- What has likely happened is that either yourself, or a dependency has brought in
@types/node
insidenode_modules
, which causes them to be auto-included in TypeScript type resolution algorithm, regardless of whether or not you import them or have them as a direct dependency. - To fix, you can either:
- Make sure
@types/node
is excluded by settingcompilerOptions.types
to an array that excludesnode
as a value (you can set it to an empty array if you are not using any other types)- or,
- As a quick fix, you can refer to browser methods via their global object. E.g. use
window.setInterval
instead of 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
tsc
does not throw any errors when compiling.- Verify that the version of TypeScript used by the IDE's live type-checking feature matches the one installed locally for this project.
- In VSCode, you can verify with
TypeScript: Select TypeScript Version
and make sure that Use Workspace Version is selected instead of Use VS Code's Version. Or, manually edit yoursettings.json
to 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.json
file, 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-ignore
comment- Reducing "strictness" in compiler options, by...
- Disabling any of:
alwaysStrict
strict
strict___
(example,strictNullChecks
)noImplicit___
(example,noImplicitAny
)
- Turning on any of:
suppressExcessPropertyErrors
suppressImplicitAnyIndexErrors
- Reducing strictness across your entire codebase is highly recommended against. It is much better to use tiny escape hatches where necessary.
- Disabling any of:
TypeScript Type Casting / Assertion
- Asserting that something is is set (not undefined or null)
- Use a
!
after the thingmyObj.prop!
myObj!.prop
myThing!
- The trailing
!
is a post-fix expression operator assertion
- Use a
- Asserting a different type than what TS inferred
- In many cases (especially if
any
is inferred) you can use the simpleas
type assertion:const strLength = (myVar as string).length
const res = func(myVar as string);
- In cases where TS thinks there is no overlap, you will get an error when trying to use a simple type assertion: "neither type sufficiently overlaps". In this instances, you first have to tell TS that it needs to treat the item as
unknown
:(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-node
for simplified debugging; docs
Building NodeJS Packages
For building NodeJS packages (e.g. something to be distributed through NPM), many developers actually don't use TSC directly - they use a bundler like Rollup to wrap the TS build process and produce the final distributable code. Here are some relevant starter templates:
There are also some libraries / CLI tools that specifically offer lots of TS support and reduce the amount of manual configuration that is necessary:
Although it is possible to create your final package code with only TSC, there are some pain points to be aware of, especially if you are targeting ESM as your distributed module type (which is somewhat new to NodeJS).
- TypeScript does not appear to respect
package.exports
(yet)- Also known as an exports map, this is a new NodeJS package feature that makes it easier to explicitly define entry points, provide direct access to nested files with shorter paths, and even support multiple module patterns with different files
- Although NodeJS might report no issues, TS will have issues and report
Cannot find module...
errors for import paths that rely exclusively on thepackage.exports
feature - 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.3
release 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
.js
extension.- This might be a way around this, to enable extensionless imports
- Sometimes referred to as fully qualified or fully resolved import paths
- If you have
import {myFunc} from './utils'
in TypeScript, TSC will not automatically transform the statement to use./utils.js
, even if you specify the output format as ESM, etc. - Considered a wont-fix type issue: See #16577 and #42151
- Solution: Annoying, but you need to either use a bundler that will handle this for you, or else manually also use
{file}.js
when importing in TypeScript. You can actually use.js
in TS imports in your src without intellisense or TSC complaining.- Walk-through: @ddprrt: TypeScript and ECMAScript Modules
- Although this fixes NodeJS ESM imports, it (including extensions) currently breaks
ts-node
in 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
tsc
if your only build task- Just use the
--watch
flag, 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
tsc
in watch mode, but also lets you execute any command of your choice when it rebuilds - Easy to use, fast
- It starts
- More manual: Use
nodemon
as watcher- This is slightly complex; you have to be careful on how you arrange the commands
- You might be tempted to do something like
concurrently "tsc --watch" "nodemon --watch dist ...."
, but this is going to work poorly due to TSC touching the output files even when there are not changes - It is best to have Nodemon watch
*.ts
files instead of TSC, and have it trigger a compile and then whatever else you want:{"watch" : "nodemon -e js,ts --ignore dist --exec \"tsc && echo 'recompiled!'\""}
Assorted Tips and Tricks
- If you want to quickly check all your files for Type errors, without compiling it, use the
--noEmit
option with TSC
TypeScript Config Import Aliases
TypeScript path import aliases are pretty neat: you can use something like import {User} from '@models'
instead of import {User} from '../../../mvc/models
There are a bunch of guides out there on how to use this.
{
"compilerOptions": {
// ...
"baseUrl": ".",
// ...
"paths": {
"@core/*": ["internal/core/*"],
"@core": ["internal/core"],
"@shared/*": ["../shared/*"],
"@shared": ["../shared"]
}
}
}
These mostly affect build output; if you are trying to use these at runtime (e.g., with something like ts-node
), you will likely need to use something to resolve the paths, like the tsconfig-paths
module (see this S/O answer for example setup).
If you run into issues where path aliases are only working for exports from within a very specific file from within the aliased directory, check to make sure that the
main
field in thepackage.json
controlling that directory points to the highest level possible. Consider how module resolution works.