effective typescript summary - wip

March 05, 2024

Todo

  • need to review item 28: Use Classes and Currying to Create New inference Sites

Understand the relationship between typescript and javascript

  • If you’re migrating an existing javascript codebase to typesript. It means that you don’t have to rewrite any of your code in another language to start using TypeScript and get the benefits it provides.

  • one of the goals of TypeScript’s type system is to detect code that will throw an exception at runtime, without having to run your code. The type checker cannot always spot code that will throw exceptions, but it will try.

  • type annotations tell TypeScript what your intent is, and this lets it spot places where your code’s behavior does not match your intent.

  • typescript is superset of javascript

  • Things to remember

    • TypeScript is a superset of JavaScript. In other words, all JavaScript programs are already TypeScript programs. TypeScript has some syntax of its own, so typeScript programs are not, in general, valid JavaScript programs.
    • TypeScript adds a type system that models JavaScript’s runtime behavior and tries to spot code which will throw exceptions at runtime. But you shouldn’t expect it to flag every exception. It is possible for code to pass the type checker but still throw at runtime.
    • While TypeScript’s type system largely models JavaScript behavior, there are some constructs that JavaScript allows but TypeScript chooses to bar, such as calling functions with the wrong number of arguments. This is largely a matter of taste.

know which TypeScript Options You’re Using

  • if you mean to allow null, you can fix the error by making your intent explicit:
const x: number | null = null;
  • Things to Remember
    • The TypeScript compiler includes several settings which affect core aspects of the language.
    • Configure TypeScript using tsconfig.json rather than command-line. options.
    • Turn on noImplicit Any unless you are transitioning a JavaScript project to TypeScript.
    • Use strictNullChecks to prevent “undefined is not an object”- style runtime errors.
    • Aim to enable strict to get the most thorough checking that TypeScript can offer.

understand that code generation is independent of types

  • At a high level, tsc (the TypeScript compiler) does two things:
    • It converts next-generation TypeScript/JavaScript to an older version of JavaScript that works in browsers (“transpiling”).
    • It checks your code for type errors.
  • The types in your code cannot affect the JavaScript that TypeScript emits. Since it’s this JavaScript that gets executed, this means that your types can’t affect the way your code runs.
  • Code with Type Errors Can Produce Output because Because code output is independent of type checking
$ cat test.ts
let x 'hello';
X = 1234;
$ tsc test.ts
test.ts:2:1- error TS2322: Type '1234' is not assignable to type 'string'
2 x = 1234;

$ cat test.js
var x = 'hello';
x = 1234;
  • You can think of all TypeScript errors as being similar to warnings in those languages: it’s likely that they indicate a problem and are worth investigating, but they won’t stop the build.

    • It’s better to say that your code has errors, or that it “doesn’t type check.”
  • You should aim for zero errors when you commit code, lest you fall into the trap of having to remember what is an expected or unexpected error. If you want to disable output on errors, you can use the noEmitOnError option in tsconfig.json, or the equivalent in your build tool.

  • You Cannot Check TypeScript Types at Runtime

  • Things to Remember

    • Code generation is independent of the type system. This means that TypeScript types cannot affect the runtime behavior or performance of your code.
      • the TypeScript compiler will introduce build time overhead. The TypeScript team takes compiler performance seriously and compilation is usually quite fast, especially for incremental builds. If the overhead becomes significant, your build tool may have a “transpile only” option to skip the type checking.
      • The code that TypeScript emits to support older runtimes may incur a performance overhead vs. native implementations. For example, if you use generator functions and target ES5, which predates generators, then tsc will emit some helper code to make things work. This may have some overhead vs. a native implementation of generators. In any case, this has to do with the emit target and language levels and is still independent of the types.
function as Number (val: number | string): number {
  return val as number;
}
// Looking at the generated JavaScript makes it clear what this function really does:
function as Number (val) {
  return val;
}

//There is no conversion going on whatsoever. The as number is a type operation, so it cannot affect the runtime behavior of your code. To normalize the value you'll need to check its runtime type and do the conversion using JavaScript constructs:
function asNumber (val: number | string): number {
  return typeof(val) === 'string' ? Number(val): val;
}
  • runtime types may not be the same as declared types
function setLightSwitch (value: boolean) { 
  switch (value) {
    case true:
      turnLighton();
      break;
    case false:
      turnLightOff();
      break;
    default:
    console.log("Im afraid I cant do that.");
  }
}
// The key is to remember that boolean is the declared type. Because it is a TypeScript type, it goes away at runtime. In JavaScript code, a user might inadvertently call setLightSwitch with a value like "ON"
  • You Cannot Overload a Function Based on TypeScript Types
    • Languages like C++ allow you to define multiple versions of a function that differ only in the types of their parameters. This is called “function overloading” Because the runtime behavior of your code is independent of its TypeScript types, this construct isn’t possible in TypeScript:
function add(a: number, b: number) { return a + b; } // Duplicate function implementation 
function add(a: string, b: string) { return a + b; } // Duplicate function implementation

// TypeScript does provide a facility for overloading functions, but it operates entirely at the type level. You can provide multiple declarations for a function, but only a single implementation:
function add (a: number, b: number): number;
function add (a: string, b: string): string;
function add (a, b) {
  return a + b;
}
const three add(1, 2); // Type is number
const twelve = add('1', '2'); // Type is string
//The first two declarations of add only provide type information. When TypeScript produces JavaScript output, they are removed, and only the implementation remains. 
  • It is possible for a program with type errors to produce code (“compile”).
  • TypeScript types are not available at runtime. To query a type at runtime, you need some way to reconstruct it. Tagged unions and property checking are common ways to do this. Some constructs, such as class, introduce both a TypeScript type and a value that is available at runtime.
//check for property example
function calculate Area (shape: Shape) {
if ('height' in shape) {
shape; // Type is Rectangle
return shape.width * shape.height;
} else {
shape; // Type is Square

// tagged unions example
interface Square {
  kind: 'square';
  width: number;
}
interface Rectangle {
  kind: 'rectangle';
  height: number;
  width: number;
}
type Shape = Square | Rectangle;

// class constructs introduce both a type and a value
class Square {
  constructor (public width: number) {}
}
class Rectangle extends Square {
  constructor (public width: number, public height: number) { super(width); }
}
type Shape = Square | Rectangle;
function calculateArea (shape: Shape) {
  if (shape instanceof Rectangle) {
    shape; // Type is Rectangle 
    return shape.width * shape.height;
  } else {
    shape;// Type is Square return shape.width
    return shape.width*shape.width; // OK
  }
}
//this works because class Rectangle introduces both a type and a value, whereas interface only introduced a type
//The Rectangle in type Shape = Square | Rectangle refers to the type, but the Rectangle in shape instanceof Rectangle refers to the value.

get comfortable with structural typing javascript

  • javascript is inherently duck typed: if you pass a function a value with all the right properties, it won’t care how you made the value. It will just use it. (“If it walks like a duck and talks like a duck…“)

  • Understand that JavaScript is duck typed and TypeScript uses structural typing to model this: values assignable to your interfaces might have properties beyond those explicitly listed in your type declarations. Types are open and are not “sealed.”

// Say you're working on a physics library and have a 2D vector type:
interface Vector2D {
  x: number;
  y: number;
}

//You write a function to calculate its length:
function calculateLength(v: Vector2D) { return Math.sqrt(v.x * v.x + v.y * v.y); }

// now you introduce the notion of a named vector
interface NamedVector { name: string;
  x: number;
  y: number;
}

//The calculateLength function will work with NamedVectors because they have x and y properties, which are numbers. TypeScript is smart enough to figure this out
const v NamedVector = {x: 3, y: 4, name: 'Zee' }; 
calculateLength (v); // OK, result is 5

//What is interesting is that you never declared the relationship between Vector2D and NamedVector. 
//And you did not have to write an alternative implementation of calculateLength for NamedVectors.
//It allowed calculateLength to be called with a NamedVector because its structure was compatible with Vector2D

// this can also lead to trouble with 3D vector type as  the calculateLength only use x, y (and ignore z) to calculate the length
interface Vector3D {
  x: number;
  y: number;
  z: number;
}


function calculateLengthL1 (v: Vector3D) { 
  let length = 0;
  for (const axis of Object.keys(v)) { 
    const coord = v[axis]; // Element implicitly has an 'any' type because
                           // 'string' can't be used to index type 'Vector3D'
    length + Math.abs (coord);
  }
  return length;
}
// this logic assumes that Vector3D is sealed and does not have other properties, but it could
const vec3D = {x: 3, y: 4, z: 1, address: '123 Broadway');
calculateLengthL1 (vec3D); // OK, returns NaN

//but in this case an implementation without loops would be better:
function calculate LengthL1 (v: Vector3D) { return Math.abs (v.x) + Math.abs (v. y) + Math.abs (v.z); }

// Structural typing can also lead to surprises with classes, which are compared structurally for assignability:
// Be aware that classes also follow structural typing rules. You may not have an instance of the class you expect!
class C { 
  foo: string; 
  constructor (foo: string) { this.foo = foo; }
}
const c = new C('instance of C'); 
const d: C = { foo: 'object literal' }; // OK!

// Structural typing is beneficial when you're writing tests.
// Say you have a function that runs a query on a database and processes the results:
interface Author {
  first: string;
  last: string;
}
function getAuthors (database: PostgresDB): Author[] {
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

// To test this, you could create a mock PostgresDB. But a better approach is to use structural typing and define a narrower interface:
interface DB {
  runQuery: (sql: string) => any [];
}

function getAuthors (database: DB): Author[] { 
  const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
  return authorRows.map(row => ({first: row[0], last: row[1]}));
}

//You can still pass getAuthors a PostgresDB in production since it has a runQuery method.
//Because of structural typing, the PostgresDB doesn't need to say that it implements DB.
test('getAuthors', () => {
  const authors = getAuthors ({ 
    runQuery(sql: string) {
      return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
    }
  });

  expect (authors).toEqual([
    {first: 'Toni', last: 'Morrison'},
    {first: 'Maya', last: 'Angelou'}
  ]);
});

// TypeScript will verify that our test DB conforms to the interface. 
// And your tests don't need to know anything about your production database: no mocking libraries necessary!
// By introducing an abstraction (DB), we've freed our logic (and tests) from the details of a specific implementation (PostgresDB).
// Another advantage of structural typing is that it can cleanly sever dependencies between libraries.

limit use of the any Type

  • The “any type” effectively silences the type checker and TypeScript language services. It can mask real problems, harm developer experience, and undermine confidence in the type system. Avoid using it when you can!

  • TypeScript’s type system is gradual and optional: gradual because you can add types to your code bit by bit and optional because you can disable the type checker whenever you like. The key to these features is the any type:

let age: number;
age = '12'; // Type "12" is not assignable to type 'number'
age = '12' as any; // OK
//The type checker is right to complain here, but you can silence it just by typing as any.
  • There’s No Type Safety with any Types
    • In the preceding example, the type declaration says that age is a number. But any lets you assign a string to it. The type checker will believe that it’s a number (that’s what you said, after all), and the chaos will go uncaught:
age += 1; // OK; at runtime, age is now "121"
  • any Lets You Break Contracts

    • When you write a function, you are specifying a contract: if the caller gives you a certain type of input, you’ll produce a certain type of output. But with an any type you can break these contracts:
function calculateAge (birthDate: Date): number { // ... 
}
let birthDate: any = '1990-01-19'; calculateAge (birthDate); // OK
  • There Are No Language Services for any Types
    • When a symbol has a type, the TypeScript language services are able to provide intelligent autocomplete and contextual documentation but for symbols with an any type, you’re on your own

    • Renaming is another such service. If you have a Person type and functions to format a person’s name:

interface Person { first: string; last: string; }
const formatName = (p: Person) => `${p.first} ${p.last}`; const formatNameAny = (p: any) => `${p.first} ${p.last}`;
  • TypeScript’s motto is “JavaScript that scales.” A key part of “scales” is the language services, which are a core part of the TypeScript experience. Losing them will lead to a loss in productivity, not just for you but for everyone else working with your code.

  • any Types Mask Bugs When You Refactor Code

interface Component Props {
  onSelectItem: (item: any) => void;
}

// Here's code that manages that component:
function render Selector(props: ComponentProps) { /*...*/ }
let selectedId: number = 0;
function handleSelectItem(item: any) {
  selectedId= item. id;
}
renderSelector({onSelectItem: handleSelectItem});

//Later you rework the selector in a way that makes it harder to pass the whole item object through to onSelectItem. 
//But that's no big deal since you just need the ID. You change the signature in Component Props. It passed type checker but it produces runtime exception
interface Component Props { }
onSelectItem: (id: number) => void;
  • any Hides Your Type Design

  • any Undermines Confidence in the Type System

    • Every time you make a mistake and the type checker catches it, it boosts your confidence in the type system. But when you see a type error at runtime, that confidence takes a hit. If you’re introducing TypeScript on a larger team, this might make your coworkers question whether TypeScript is worth the effort. any types are often the source of these uncaught errors.

    • TypeScript aims to make your life easier, but TypeScript with lots of any types can be harder to work with than untyped JavaScript because you have to fix type errors and still keep track of the real types in your head. When your types match reality, it frees you from the burden of having to keep type information in your head. TypeScript will keep track of it for you.

Use Your Editor to Interrogate and Explore the Type System

  • Take advantage of the TypeScript language services by using an editor that can use them.

Use your editor to build an intuition for how the type system works and how TypeScript infers types.

  • Know how to jump into type declaration files to see how they model behavior.

  • When you install TypeScript, you get two executables:

    • tsc, the TypeScript compiler
    • tsserver, the TypeScript standalone server
  • configure the language services with your editor for services like autocomplete, inspection, navigation, and refactoring.

  • if the inferred type does not match your expectation, you should add a type declaration and track down the discrepancy.

Seeing TypeScript’s understanding of a variable’s type at any given point is essential for building an intuition around widening (Item 21) and narrowing (Item 22). Seeing the type of a variable change in the branch of a conditional is a tremendous way to build confidence in the type system (see Figure 2-3).

function logMessage(message: string | null) { 
  if (message) {
    // type of message is string | null outside the branch but string inside
    message
  }
}
const foo = {
  //If your intention was for x to be a tuple type ([number, number, number]), then a type annotation will be required.
  //(property) x: number []
  x: [1, 2, 3],
  bar: {
  }
};
  • Seeing type errors in your editor can also be a great way to learn the nuances of the type system. For example, this function tries to get an HTMLElement by its ID, or return a default one. TypeScript flags two errors:
function getElement (el0rId: string| HTMLElement | null): HTMLElement { 
  if (typeof el0rId === 'object') { 
    // 'HTMLElement | null' is not assignable to 'HTMLElement'
    // The intent in the first branch of the if statement was to filter down
    // to just the objects, namely, the HTMLElements.
    // But oddly enough, in JavaScript typeof null is "object", 
    // so el0rId could still be null in that branch. 
    // You can fix this by putting the null check first. 
    return el0rId;
  } else if (el0rId === null) { 
    return document.body;
  } else {
    const el = document.getElementById(el0rId);
    // ~~~~~~~'HTMLElement | null' is not assignable to 'HTMLElement'
    // The second error is because document.getElementById can return null,
    // so you need to handle that case as well, perhaps by throwing an exception.
    return el;
  }
}

think of types as sets of values

  • Think of types as sets of values (the type’s domain). These sets can either be finite (e.g., boolean or literal types) or infinite (e.g., number or string).

    • The smallest set is the empty set, which contains no values. It corresponds to the never type in TypeScript. Because its domain is empty, no values are assignable to a variable with a never type:
const x: never = 12;
// Type '12' is not assignable to type 'never'
  • The next smallest sets are those which contain single values. These correspond to literal types in TypeScript, also known as unit types:
type A = 'A';
type B = 'B';
type Twelve = 12;
// To form types with two or three values, you can union unit types:
type AB = 'A' | 'B'; 
type AB12 = 'A' | 'B' | 12;
// and so on. Union types correspond to unions of sets of values.
  • The word “assignable” appears in many TypeScript errors. In the context of sets of values, it means either “member of” (for a relationship between a value and a type) or “subset of” (for a relationship between two types):

  • At the end of the day, almost all the type checker is doing is testing whether one set is a subset of another

  • Think of Identified interface as a description of the values in the domain of its type. Does the value have an id property whose value is assignable to (a member of) string? Then it’s an Identified.

  • Two ways of thinking of type relationships: as a hierarchy or as overlapping sets

  • With the Venn diagram, it’s clear that the subset/subtype/assignability relationships are unchanged if you rewrite the interfaces without extends:

  • Typescript types form intersecting sets (a Venn diagram) rather than a strict hierarchy. Two types can overlap without either being a subtype of the other.

    • Remember that an object can still belong to a type even if it has additional properties that were not mentioned in the type declaration.
interface Person { 
  name: string;
}
interface Lifespan {
  birth: Date;
  death? Date;
}
type PersonSpan = Person & Lifespan;
const ps: PersonSpan = {
  name: 'Alan Turing',
  birth: new Date('1912/06/23'),
  death: new Date('1954/06/07'), // OK
};
  • Type operations apply to a set’s domain. The intersection of A and B is the intersection of A’s domain and B’s domain. For object types, this means that values in A & B have the properties of both A and B.
    • Think of “extends,” “assignable to,” and “subtype of” as synonyms for “subset of.”
interface Point {
  x: number;
  y: number;
}
type PointKeys = keyof Point; // Type is "x" | "y"
function sortBy<K extends keyof T, T>(vals: T[], key: K): T[] {}
const pts: Point[] = [{x: 1, y: 1}, {x: 2, y: 0}];
sortBy (pts, 'x'); // OK, 'x' extends'x'|'y' (aka keyof T)
sortBy (pts, 'y'); // OK, 'y' extends 'x'|'y'
sortBy(pts, Math.random() < 0.5 ? 'x': 'y'); // OK, 'x', 'y' extends 'x'|'y'
sortBy (pts, 'z'); //Type "z" is not assignable to parameter of type "x"|"y"
  • What’s the relationship between string|number and string|Date, for instance? Their intersection is non-empty (it’s string), but neither is a subset of the other. The relationship between their domains is clear, even though these types don’t fit into a strict hierarchy.

    • Union types may not fit into a hierarchy but can be thought of in terms of sets of values
  • Thinking of types as sets can also clarify the relationships between arrays and tuples.

const list = [1, 2]; // Type is number[] 

// Type 'number[]' is missing the following properties from type
// [number, number]': 0, 1
const tuple: [number, number] = list; 
//The empty list and the list [1] are list of numbers which are not pairs of numbers. It therefore makes sense that number [] is not assignable to [number, number] since it's not a subset of it. (The reverse assignment does work.)

//'[number, number, number]' is not assignable to '[number, number]' 
//Types of property 'length are incompatible
//Type '3' is not assignable to type '2'
const triple: [number, number, number] [1, 2, 3];
const double: [number, number] = triple; 
//TypeScript models a pairs of numbers as {0: number, 1: number, length: 2}.
  • Finally, it’s worth noting that not all sets of values correspond to TypeScript types. There is no TypeScript type for all the integers, or for all the objects that have x and y properties but no others. You can sometimes subtract types using Exclude, but only when it would result in a proper TypeScript type:
type T = Exclude <string | Date, string| number>; // Type is Date
type NonZeroNums = Exclude <number, 0> ; // Type is still just number

prefer type declarations to type assertions

  • Prefer type declarations (: Type) to type assertions (as Type).
const alice: Person = {}; 
// type '{}' Property 'name' is missing in but required in type 'Person'
const bob = {} as Person; // No error
interface Person { name: string };
const alice: Person = { name: 'Alice' }; // Type is Person
const bob = { name: 'Bob' as Person; } // Type is Person
// The first (alice: Person) adds a type declaration to the variable 
// and ensures that the value conforms to the type. 
// The latter (as Person) performs a type assertion. 
// This tells TypeScript that, despite the type it inferred,
// you know better and would like the type to be Person.

// The same thing happens if you specify an additional property:
const alice: Person = { 
  name: 'Alice',
  occupation: 'TypeScript developer' 
  // Object literal may only specify known properties
  // and 'occupation' does not exist in type 'Person'
};

const bob = {
  name: 'Bob', 
  occupation: 'JavaScript developer'
} as Person; // No error
  • Know how to annotate the return type of an arrow function.
//It's tempting to use a type assertion here, and it seems to solve the problem:
const people = ['alice', 'bob', 'jan']
.map(name => ({name} as Person)); // Type is Person[]


//But this suffers from all the same issues as a more direct use of type assertions.
//For example:
const people = ['alice', 'jan'].map(name => ({} as Person)); // No error

//A more concise way is to declare the return type of the arrow function:
const people = ['alice', 'bob', 'jan'].map((name): Person => ({name})); 
// Type is Person []
  • Use type assertions and non-null assertions when you know something about types that TypeScript does not.
//For instance, you may know the type of a DOM element more precisely than TypeScript does:
document.querySelector('#myButton')
.addEventListener('click', e => {
  e.currentTarget // Type is Event Target 
  const button = e.currentTarget as HTMLButtonElement;
  button // Type is HTMLButtonElement 
//Because TypeScript doesn't have access to the DOM of your page,
//it has no way of knowing that #myButton is a button element.
//And it doesn't know that the currentTarget of the event should be that same button.
//Since you have information that TypeScript does not,
//a type assertion makes sense here
});

//You may also run into the non-null assertion,
//which is so common that it gets a special syntax:
const elNull = document.getElementById('foo'); // Type is HTMLElement | null 
const el = document.getElementById('foo')!; // Type is HTMLElement
//Used as a prefix, ! is boolean negation.
//But as a suffix, is interpreted as an assertion that the value is non-null.
//You should treat ! just like any other assertion: 
//it is erased during compilation, 
//so you should only use it if you have information that the type checker lacks
//and can ensure that the value is non-null.
//If you can't, you should use a conditional to check for the null case.


interface Person { name: string; } 
const body = document.body; 
const el = body as Person;
//Conversion of type 'HTMLElement' to type 'Person' may be a mistake
//because neither type sufficiently overlaps with the other. If this was intentional,
//convert the expression to 'unknown' first
const el = document.body as unknown as Person; // OK

Avoid Object Wrapper Types (String, Number, Boolean, Symbol, BigInt)

  • Understand how object wrapper types are used to provide methods on primitive values. Avoid instantiating them or using them directly.

  • While a string primitive does not have methods, JavaScript also defines a String object type that does. JavaScript freely converts between these types. When you access a method like charAt on a string primitive, JavaScript wraps it in a String object, calls the method, and then throws the object away.

  • Avoid TypeScript object wrapper types. Use the primitive types instead: string instead of String, number instead of Number, boolean instead of Boolean, symbol instead of Symbol, and bigint instead of BigInt.

// Don't do this!
const originalCharAt = String.prototype.charAt; 
String.prototype.charAt = function(pos) { 
  console.log(this, typeof this, pos);
  return originalCharAt.call(this, pos);
};
console.log('primitive'.charAt(3));

// This produces the following output:
[String: 'primitive'] object 3
m

// The this value in the method is a String object wrapper, not a string primitive. 
// You can instantiate a String object directly and it will sometimes behave like a string primitive. But not always. For example, a String object is only ever equal to itself:
"hello" === new String("hello") -> false
new String("hello") === new String("hello") -> false

// The implicit conversion to object wrapper types explains an odd phenomenon in JavaScript-if you assign a property to a primitive, it disappears:
> x = "hello"
> x.language = 'English' 'English'
> x.language undefined
Now you know the explanation: x is converted to a String instance, the language property is set on that, and then the object (with its language property) is thrown away.
There are object wrapper types for the other primitives as well: Number for numbers, Boolean for booleans, Symbol for symbols, and BigInt for bigints (there are no object wrappers for null and undefined).
  • As a final note, it’s OK to call BigInt and Symbol without new, since these create primitives:

Using Either/Result in TypeScript for Error Handling

import * as P from 'fp-ts/lib/pipeable';
import * as E from 'fp-ts/lib/Either';
import axios, { AxiosResponse, AxiosError } from 'axios';

// Custom error type
type ResponseError = string;

async function makeApiCall(): Promise<E.Either<ResponseError, AxiosResponse>> {
  try {
    const response = await axios.get('https://jsonplaceholder.typicode.com/posts/1');
    return E.right(response);
  } catch (error) {
    const axiosError = error as AxiosError;
    return E.left(`API Error: ${axiosError.response?.statusText || 'Unknown'}`);
  }
}

const post = makeApiCall()
  .then((apiResponse) => {
    P.pipe(
      apiResponse,
      E.match(
        (error: ResponseError) => {
            console.error(error);
            return null
        },
        (response: AxiosResponse) => response.data
      )
    );
  });

Recognize the Limits of Excess Property Checking

  • When you assign an object literal to a variable or pass it as an argument to a function, it undergoes excess property checking.
  • Excess property checking is an effective way to find errors, but it is distinct from the usual structural assignability checks done by the TypeScript type checker. Conflating these processes will make it harder for you to build a mental model of assignability.
  • Be aware of the limits of excess property checking: introducing an intermediate variable will remove these checks.
//When you assign an object literal to a variable with a declared type, TypeScript makes sure it has the properties of that type and no others:
//you've triggered a process known as "excess property checking," which helps catch an important class of errors that the structural type system would otherwise miss
interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}
const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
  //~`Object literal may only specify known properties, and 'elephant' does not exist in type 'Room'
};

//The elephant constant is assignable to the Room type, which you can see by introducing an intermediate variable:
//The type of obj is inferred as { numDoors: number; ceilingHeightFt: number; elephant: string}. Because this type includes a subset of the values in the
//Room type, it is assignable to Room, and the code passes the type checker
const obj = {
  numDoors: 1, 
  ceilingHeightFt: 10,
  elephant: 'present',
};
const r:
Room = obj; // OK, as the right hand side is not object literal

//TypeScript goes beyond trying to flag code that will throw exceptions at runtime. It also tries to find code that doesn't do what you intend. Here's an example of the latter:
interface Options {
  title: string;
  darkMode?: boolean;
}

function createWindow(options: Options) {
  if (options.darkMode) {
    setDarkMode();
  }
  // ...
}

createWindow({
  title: 'Spider Solitaire',
  darkmode: true
  // Object literal may only specify known properties, but 'darkmode' does not exist in type 'Options'.
  // Did you mean to write 'darkMode'?
});


// Excess property checking does not happen when you use a type assertion:
const o = { darkmode: true, title: 'Ski Free' } as Options; // OK
// This is a good reason to prefer declarations to assertions

// If you don't want this sort of check, you can tell TypeScript to expect additional properties using an index signature:
interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown;
}
const o: Options = { darkmode: true }; // OK


//A related check happens for "weak" types, which have only optional properties:
interface LineChartOptions (
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}
const opts = { logScale: true };
const o: LineChartOptions = opts;
// ~ Type '{ logScale: boolean; }' has no properties in common // with type 'LineChartOptions'
//For weak types like this, TypeScript adds another check to make sure that the value type and declared type have at least one property in common.

Apply Types to Entire Function Expressions When Possible

Things to Remember

  • Consider applying type annotations to entire function expressions, rather than to their parameters and return type.
  • If you’re writing the same type signature repeatedly, factor out a function type or look for an existing one.
  • If you’re a library author, provide types for common callbacks.
  • Use typeof fn to match the signature of another function, or Parameters and a rest parameter if you need to change the return type.

Code Samples

function rollDice1(sides: number): number { /* ... */ }  // Statement
const rollDice2 = function(sides: number): number { /* ... */ };  // Expression
const rollDice3 = (sides: number): number => { /* ... */ };  // Also expression

💻 playground


type DiceRollFn = (sides: number) => number;
const rollDice: DiceRollFn = sides => { /* ... */ };

💻 playground


function add(a: number, b: number) { return a + b; }
function sub(a: number, b: number) { return a - b; }
function mul(a: number, b: number) { return a * b; }
function div(a: number, b: number) { return a / b; }

💻 playground


type BinaryFn = (a: number, b: number) => number;
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
const mul: BinaryFn = (a, b) => a * b;
const div: BinaryFn = (a, b) => a / b;
//reduced type annotations and makes the logic more apparent

💻 playground


declare function fetch(
  input: RequestInfo, init?: RequestInit,
): Promise<Response>;

💻 playground


async function checkedFetch(input: RequestInfo, init?: RequestInit) {
  const response = await fetch(input, init);
  if (!response.ok) {
    // An exception becomes a rejected Promise in an async function.
    throw new Error(`Request failed: ${response.status}`);
  }
  return response;
}

💻 playground


// we've changed from a function statement to a function expression and applied a type (typeof fetch) to the entire function. This allows TypeScript to infer the types of the input and init parameters.
const checkedFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init);
  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }
  return response;
}

💻 playground


const checkedFetch: typeof fetch = async (input, init) => {
  //  ~~~~~~~~~~~~
  //  'Promise<Response | HTTPError>' is not assignable to 'Promise<Response>'
  //    Type 'Response | HTTPError' is not assignable to type 'Response'
  // The type annotation also guarantees that the return type of checkedFetch will be the same as that of fetch.
  // by changing the throw to return, typescript caught the mistake as above
  const response = await fetch(input, init);
  if (!response.ok) {
    return new Error('Request failed: ' + response.status);
  }
  return response;
}

💻 playground


//what if you want to match the parameter types of another function but change the return type? This is possible using a rest parameter and the built-in Parameters utility type
//you also benefit from this technique whenever you pass a callback to another function. When you use the map of filter method of an Array, for example, TypeScript is able to infer a type for the callback parameter, and it applies that type to your function expression.
async function fetchANumber(
    ...args: Parameters<typeof fetch>
): Promise<number> {
  const response = await checkedFetch(...args);
  const num = Number(await response.text());
  if (isNaN(num)) {
    throw new Error(`Response was not a number.`);
  }
  return num;
}

💻 playground


fetchANumber
// ^? function fetchANumber(
//      input: RequestInfo | URL, init?: RequestInit | undefined
//    ): Promise<number>

💻 playground

Know the Differences Between type and interface

Things to Remember

  • Understand the differences and similarities between type and interface.
    • for new code where you need to pick a style, the general rule of thumb is to use interface where possible, using either where it’s required, (e.g, union types) or has a clearer syntax (e.g. function types)
  • Know how to write the same types using either syntax.
  • Be aware of declaration merging for interface and type inlining for type.
  • For projects without an established style, prefer interface to type for object types until you need to use features from type. Otherwise, stick with established styles
  • for complex types, you need to use type alias. For function types, tuple types, and array types, the type syntax is more concise and natural than the interface syntax.
  • you can enforce consistent use of type or interface using typescript-eslint’s consistent type-definitions rule.

Code Samples


// this type alias form (TFn) looks more natural and is more concise for function types. This is the preferred form and is the one you're most likely to encounter in type declarations.
type TFn = (x: number) => string;


// the latter 2 forms reflect the fact that functions in javascript are callable objects.
interface IFn {
  (x: number): string;
}
type TFnAlt = {
  (x: number): string;
};

const toStrT: TFn = x => '' + x;  // OK
const toStrI: IFn = x => '' + x;  // OK
const toStrTAlt: TFnAlt = x => '' + x;  // OK

💻 playground


// an interface can extend a type, and a type can extend an interface
interface IStateWithPop extends TState {
  population: number;
}

// you can't extend union type, if you want to do that, you'll need to use type and &
type TStateWithPop = IState & { population: number; };

💻 playground


// class can implement either an interface or a simple type
class StateT implements TState {
  name: string = '';
  capital: string = '';
}
class StateI implements IState {
  name: string = '';
  capital: string = '';
}

💻 playground


// there's union type but no interface
type AorB = 'a' | 'b';

💻 playground


type Input = { /* ... */ };
type Output = { /* ... */ };
interface VariableMap {
  [name: string]: Input | Output;
}

💻 playground


type NamedVariable = (Input | Output) & { name: string };

💻 playground


interface Person {
  name: string;
  age: string;
}

type TPerson = Person & { age: number; };  // no error, unusable type

interface IPerson extends Person {
  //      ~~~~~~~ Interface 'IPerson' incorrectly extends interface 'Person'.
  //                Types of property 'age' are incompatible.
  //                  Type 'number' is not assignable to type 'string'.
  // generally you want more safety check, so this is a good reason to use extends with interfaces
  age: number;
}

💻 playground


// type aliases are the natural way to express tuple and array types
type Pair = [a: number, b: number];
type StringList = string[];
type NamedNums = [string, ...number[]];

💻 playground


// this is known as "declaration merging"
// typescript itself uses declaration merging to model the different versions of javascript's standard library
// declaration makes most sense in declaration files. It can happen in user code, but only if the two interfaces are defined in the same module (i.e: the same .ts file). This prevents accidental collisions with global interfaces with generic-sounding names like Location and FormData
interface IState {
  name: string;
  capital: string;
}
interface IState {
  population: number;
}
const wyoming: IState = {
  name: 'Wyoming',
  capital: 'Cheyenne',
  population: 578_000
};  // OK

💻 playground


//TypeScript will always try to refer to an interface by its name, whereas it takes more liberties replacing a type alias with its underlying definition.
export function getHummer() {
  type Hummingbird = { name: string; weightGrams: number; };
  const ruby: Hummingbird = { name: 'Ruby-throated', weightGrams: 3.4 };
  return ruby;
};

const rubyThroat = getHummer();
//    ^? const rubyThroat: Hummingbird

💻 playground


// get-hummer.d.ts
export declare function getHummer(): {
  name: string;
  weightGrams: number;
};

💻 playground


export function getHummer() {
  //            ~~~~~~~~~
  // Return type of exported function has or is using private name 'Hummingbird'.
  interface Hummingbird { name: string; weightGrams: number; };
  const bee: Hummingbird = { name: 'Bee Hummingbird', weightGrams: 2.3 };
  return bee;
};

💻 playground

Use readonly to Avoid Errors Associated with Mutation

Things to Remember

  • If your function does not modify its parameters, declare them readonly (arrays) or Readonly (object types). This makes the function’s contract clearer and prevents inadvertent mutations in its implementation.
  • Understand that readonly and Readonly are shallow, and that Readonly only affects properties, not methods.
  • Use readonly to prevent errors with mutation and to find the places in your code where mutations occur.
  • Understand the difference between const and readonly: the former prevents reassignment, the latter prevents mutation.

Code Samples

interface PartlyMutableName {
  readonly first: string;
  last: string;
}

const jackie: PartlyMutableName = { first: 'Jacqueline', last: 'Kennedy' };
jackie.last = 'Onassis';  // OK
jackie.first = 'Jacky';
//     ~~~~~ Cannot assign to 'first' because it is a read-only property.

💻 playground


interface FullyMutableName {
  first: string;
  last: string;
}
type FullyImmutableName = Readonly<FullyMutableName>;
//   ^? type FullyImmutableName = {
//        readonly first: string;
//        readonly last: string;
//      }

💻 playground


interface Outer {
  inner: {
    x: number;
  }
}
const obj: Readonly<Outer> = { inner: { x: 0 }};
obj.inner = { x: 1 };
//  ~~~~~ Cannot assign to 'inner' because it is a read-only property
obj.inner.x = 1;  // OK

💻 playground


type T = Readonly<Outer>;
//   ^? type T = {
//        readonly inner: {
//          x: number;
//        };
//      }
// there's no built-in support for deep readonly types, may need to use DeepReadonly generic in ts-essentials

💻 playground


const date: Readonly<Date> = new Date();
date.setFullYear(2037);  // OK, but mutates date!
//Readonly  only affects properties. If you apply it to a type with methods that mutate the underlying object, it won't remove them

💻 playground


interface Array<T> {
  length: number;
  // (non-mutating methods)
  toString(): string;
  join(separator?: string): string;
  // ...
  // (mutating methods)
  pop(): T | undefined;
  shift(): T | undefined;
  // ...
  [n: number]: T;
}

💻 playground


interface ReadonlyArray<T> {
  readonly length: number;
  // (non-mutating methods)
  toString(): string;
  join(separator?: string): string;
  // ...
  readonly [n: number]: T;
}
//the key diffdrences are that the mutating methods (such as pop and shift) aren't defined on ReadonlyArray, and the two properties, length and index type ([n: number]: T), have readonly modifiers. This prevents resizing the array and assigning to elements in the array

💻 playground


const a: number[] = [1, 2, 3];
const b: readonly number[] = a;
const c: number[] = b;
//    ~ Type 'readonly number[]' is 'readonly' and cannot be
//      assigned to the mutable type 'number[]'

💻 playground


function printTriangles(n: number) {
  const nums = [];
  for (let i = 0; i < n; i++) {
    nums.push(i);
    console.log(arraySum(nums as readonly number[]));
    //                   ~~~~~~~~~~~~~~~~~~~~~~~~~
    // The type 'readonly number[]' is 'readonly' and cannot be
    // assigned to the mutable type 'number[]'.
  }
}

💻 playground


function arraySum(arr: readonly number[]) {
  let sum = 0, num;
  while ((num = arr.pop()) !== undefined) {
    //              ~~~ 'pop' does not exist on type 'readonly number[]'
    sum += num;
  }
  return sum;
}

💻 playground


function arraySum(arr: readonly number[]) {
  let sum = 0;
  for (const num of arr) {
    sum += num;
  }
  return sum;
}
//when you give a parameter a read-only type (either readonly for an array or Readyonly for an object type), a few things happen
// - Typescript checks that the parameter isn't mutated in the function body
// - you advertise to callers that your function doesn't mutate the parameter
// - callers may pass your function a readonly array or Readonly object.

💻 playground

Use Type Operations and Generic Types to Avoid Repeating Yourself

Things to Remember

  • The DRY (don’t repeat yourself) principle applies to types as much as it applies to logic.
  • Name types rather than repeating them. Use extends to avoid repeating fields in interfaces.
  • Build an understanding of the tools provided by TypeScript to map between types. These include keyof, typeof, indexing, and mapped types.
  • Generic types are the equivalent of functions for types. Use them to map between types instead of repeating type-level operations.
  • Familiarize yourself with generic types defined in the standard library, such as Pick, Partial, and ReturnType.
  • Avoid over-application of DRY: make sure the properties and types you’re sharing are really the same thing.

Code Samples

// the simplest way to reduce repetition is by naming your types
function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
  return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}

💻 playground


interface Point2D {
  x: number;
  y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }

💻 playground


// if several functions share the same type signature, you can factor out a named type for this signature
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

💻 playground


interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}

💻 playground


interface Vertebrate {
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
}
interface Bird extends Vertebrate {
  wingspanCm: number;
}
interface Mammal extends Vertebrate {
  eatsGardenPlants: boolean;
}

💻 playground


type PersonWithBirthDate = Person & { birth: Date };

💻 playground


interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}
interface TopNavState {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  // omits pageContents
}

💻 playground


interface TopNavState {
  userId: State['userId'];
  pageTitle: State['pageTitle'];
  recentFiles: State['recentFiles'];
};

💻 playground


type TopNavState = {
  [K in 'userId' | 'pageTitle' | 'recentFiles']: State[K]
};

💻 playground


type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

💻 playground


interface SaveAction {
  type: 'save';
  // ...
}
interface LoadAction {
  type: 'load';
  // ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load';  // Repeated types!

💻 playground


type ActionType = Action['type'];
//   ^? type ActionType = "save" | "load"

💻 playground


type ActionRecord = Pick<Action, 'type'>;
//   ^? type ActionRecord = { type: "save" | "load"; }

💻 playground


interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
interface OptionsUpdate {
  width?: number;
  height?: number;
  color?: string;
  label?: string;
}
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: OptionsUpdate) { /* ... */ }
}

💻 playground


type OptionsUpdate = {[k in keyof Options]?: Options[k]};

💻 playground


type OptionsKeys = keyof Options;
//   ^? type OptionsKeys = keyof Options
//      (equivalent to "width" | "height" | "color" | "label")

💻 playground


// same as OptionsKeys = keyof Options, this pattern is very common and is included in the standard library as Partial
class UIWidget {
  constructor(init: Options) { /* ... */ }
  update(options: Partial<Options>) { /* ... */ }
}

💻 playground


interface ShortToLong {
  q: 'search';
  n: 'numberOfResults';
}
type LongToShort = { [k in keyof ShortToLong as ShortToLong[k]]: k };
//   ^? type LongToShort = { search: "q"; numberOfResults: "n"; }

💻 playground


interface Customer {
  /** How the customer would like to be addressed. */
  title?: string;
  /** Complete name as entered in the system. */
  readonly name: string;
}

type PickTitle = Pick<Customer, 'title'>;
//   ^? type PickTitle = { title?: string; }
type PickName = Pick<Customer, 'name'>;
//   ^? type PickName = { readonly name: string; }
type ManualName = { [K in 'name']: Customer[K]; };
//   ^? type ManualName = { name: string; }

💻 playground


type PartialNumber = Partial<number>;
//   ^? type PartialNumber = number

💻 playground


const INIT_OPTIONS = {
  width: 640,
  height: 480,
  color: '#00FF00',
  label: 'VGA',
};
interface Options {
  width: number;
  height: number;
  color: string;
  label: string;
}
// or
type Options = typeof INIT_OPTIONS;

💻 playground


function getUserInfo(userId: string) {
  // ...
  return {
    userId,
    name,
    age,
    height,
    weight,
    favoriteColor,
  };
}
// Return type inferred as { userId: string; name: string; age: number, ... }

type UserInfo = ReturnType<typeof getUserInfo>;

💻 playground


interface Product {
  id: number;
  name: string;
  priceDollars: number;
}
interface Customer {
  id: number;
  name: string;
  address: string;
}

💻 playground


// Don't do this!
// this is because while the id and name properties happen to have the same name and type, they're not referring to the same thing. 
// In this case, factoring out the common base interface is a premature abstraction which may make it harder for the 2 types to evolve independently in the future. (as Product and Customer is likely evolve differently)
// a good rule of thumb is that if it's hard to name a type ( or a function, then it may not be a useful abstraction. In this case, NamedAndIdentified) just describes the structure of the type, not what it is. The Vertebrate type from before, on the other hand, is meaningful on its own. Remember, "duplication is far cheaper than wrong abstraction"
interface NamedAndIdentified {
  id: number;
  name: string;
}
interface Product extends NamedAndIdentified {
  priceDollars: number;
}
interface Customer extends NamedAndIdentified {
  address: string;
}

💻 playground

Prefer More Precise Alternatives to Index Signatures

Things to Remember

  • Understand the drawbacks of index signatures: much like any, they erode type safety and reduce the value of language services.
  • Prefer more precise types to index signatures when possible: ++interface++s, Map, ++Record++s, mapped types, or index signatures with a constrained key space.

Code Samples

type Rocket = {[property: string]: string};
// this is index signatures, it specifies 3 things:
// 1) the name for the keys
// 2) a type for the key
// 3) a type for the values

// while the code does type check, it has few downsides:
// 1) it allows any keys, including incorrect ones. Had you written Name instead of name, it would have still been a valid Rocket type
// 2) it doesn't require any specific keys to be present, {} is also a valid Rocket
// 3) it cannot have distinct types for different keys, we might want thrust to be a number rather than a string.
// 4) as you're typing name: there's no autocomplete because the key could be anything
const rocket: Rocket = {
  name: 'Falcon 9',
  variant: 'v1.0',
  thrust: '4,940 kN',
};  // OK

💻 playground


interface Rocket {
  name: string;
  variant: string;
  thrust_kN: number;
}
const falconHeavy: Rocket = {
  name: 'Falcon Heavy',
  variant: 'v1',
  thrust_kN: 15200,
};

💻 playground


// historically, index signature were the best way to model truly dynamic data, such as parsing CSV or JSON
function parseCSV(input: string): {[columnName: string]: string}[] {
  const lines = input.split('\n');
  const [headerLine, ...rows] = lines;
  const headers = headerLine.split(',');
  return rows.map(rowStr => {
    const row: {[columnName: string]: string} = {};
    rowStr.split(',').forEach((cell, i) => {
      row[headers[i]] = cell;
    });
    return row;
  });
}

💻 playground


interface ProductRow {
  productId: string;
  name: string;
  price: string;
}

declare let csvData: string;
const products = parseCSV(csvData) as unknown[] as ProductRow[];

💻 playground


const rockets = parseCSVMap(csvData);
const superHeavy = rockets[2];
const thrust_kN = superHeavy.get('thrust_kN');  // 74,500
//    ^? const thrust_kN: string | undefined

💻 playground


// if you want to get an object type out of a Map, you'll need to write some parsing code
function parseRocket(map: Map<string, string>): Rocket {
  const name = map.get('name');
  const variant = map.get('variant');
  const thrust_kN = Number(map.get('thrust_kN'));
  if (!name || !variant || isNaN(thrust_kN)) {
    throw new Error(`Invalid rocket: ${map}`);
  }
  return {name, variant, thrust_kN};
}
const rockets = parseCSVMap(csvData).map(parseRocket);
//    ^? const rockets: Rocket[]

💻 playground


// if your type has a limited set of possible fields, don't model this with an index signature, if you know your data will have keys like A, B, C, D, but you don't know how many of them there will be, you could model the type either with optional fields or a union.
interface Row1 { [column: string]: number }  // Too broad
interface Row2 { a: number; b?: number; c?: number; d?: number }  // Better
type Row3 =
    | { a: number; }
    | { a: number; b: number; }
    | { a: number; b: number; c: number;  }
    | { a: number; b: number; c: number; d: number };  // Also better

💻 playground


// Record is a built-in wrapper around a mapped type
type Vec3D = Record<'x' | 'y' | 'z', number>;
//   ^? type Vec3D = {
//        x: number;
//        y: number;
//        z: number;
//      }

💻 playground


declare function renderAButton(props: ButtonProps): void;
interface ButtonProps {
  title: string;
  onClick: () => void;
}

renderAButton({
  title: 'Roll the dice',
  onClick: () => alert(1 + Math.floor(6 * Math.random())),
  theme: 'Solarized',
// ~~~~ Object literal may only specify known properties…
});

💻 playground


// you can use an index type of disable excess property checking, for example, you might define a few known properties on a ButtonProps type but still want to allow it to have any others.
// you can constraint these additional properties to match a certain pattern. For example, some web components allow arbitrary properties but only if they start with "data-", you can model this using an index signature and a template literal type
interface ButtonProps {
  title: string;
  onClick: () => void;
  [otherProps: string]: unknown;
}

renderAButton({
  title: 'Roll the dice',
  onClick: () => alert(1 + Math.floor(20 * Math.random())),
  theme: 'Solarized',  // ok
});

💻 playground

Avoid Numeric Index Signatures

Things to Remember

  • Understand that arrays are objects, so their keys are strings, not numbers. number as an index signature is a purely TypeScript construct designed to help catch bugs.
  • Prefer Array, tuple, ArrayLike, or Iterable types to using number in an index signature yourself.

Code Samples

interface Array<T> {
  // ...
  [n: number]: T;
}

💻 playground


const xs = [1, 2, 3];
const x0 = xs[0];  // OK
const x1 = xs['1'];  // stringified numeric constants are also OK

const inputEl = document.getElementsByTagName('input')[0];
const xN = xs[inputEl.value];
//            ~~~~~~~~~~~~~ Index expression is not of type 'number'.
// in particular, numbers cannot be used as keys. If you try to use a number as a property name, the javascript runtime will convert it to a string

💻 playground


x = [1, 2, 3]
const keys = Object.keys(xs);
//    ^? const keys: string[]

💻 playground


function checkedAccess<T>(xs: ArrayLike<T>, i: number): T {
  if (i >= 0 && i < xs.length) {
    return xs[i];
  }
  throw new Error(`Attempt to access ${i} which is past end of array.`)
}

💻 playground


// a general rule, there's not much reason to use number as the index signature of a type. if you want to specify something that will be indexed using numbers, you probably want to use an ArrayLike or tuple type instead.
const tupleLike: ArrayLike<string> = {
  '0': 'A',
  '1': 'B',
  length: 2,
};  // OK

💻 playground

Avoid Cluttering Your Code with Inferable Types

  • example 1:

Don’t write:

let x: number = 12;

Instead, just write:

let x = 12;
  • example 2:

Instead of:

const person: {
  name: string;
  born: {
    where: string;
    when: string;
  };
  died: {
    where: string;
    when: string;
  }
} = {
  name: 'Sojourner Truth',
  born: {
    where: 'Swartekill, NY',
    when: 'c.1797',
  },
  died: {
    where: 'Battle Creek, MI',
    when: 'Nov. 26, 1883'
  }
};

you can just write:

const person = {
  name: 'Sojourner Truth',
  born: {
    where: 'Swartekill, NY',
    when: 'c.1797',
  },
  died: {
    where: 'Battle Creek, MI',
    when: 'Nov. 26, 1883'
  }
};
  • example 3:

instead of:

interface Product {
  id: number;
  name: string;
  price: number;
}

function logProduct(product: Product) {
  const id: number = product.id;
  const name: string = product.name;
  const price: number = product.price;
  console.log(id, name, price);
}

you can just write:

interface Product {
  id: number;
  name: string;
  price: number;
}

function logProduct(product: Product) {
  const {id, name, price} = product;
  console.log(id, name, price);
}
  • example 4:
// Don't do this:
app.get('/health', (request: express.Request, response: express.Response) => {
  response.send('OK');
});
// Do this:
app.get('/health', (request, response) => {
  response.send('OK');
});
  • There are a few situations where you may still want to specify a type even where it can be inferred.

    • when you define an object literal
const elmo: Product = {
  name: 'Tickle Me Elmo',
  id: '048188 627152',
  price: 28.99,
};
- When you specify a type on a definition like this, you enable excess property checking. This can help catch errors, particularly for types with optional fields.
- You also increase the odds that an error will be reported in the right place (type error where it's defined vs type error where it's used)
  • You may still want to annotate return type even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function.

    • Writing out the return type may also help you think more clearly about your function: you should know what its input and output types are before you implement it. While the implementation may shift around a bit, the function’s contract (its type signature) generally should not. This is similar in spirit to test-driven development (TDD), in which you write the tests that exercise a function before you implement it. Writing the full type signature first helps get you the function you want, rather than the one the implementation makes expedient.
  • If you are using a linter, the eslint rule no-inferrable-types (note the variant spelling) can help ensure that all your type annotations are really necessary.

Use Different Variables for Different Types

Things to Remember

  • While a variable’s value can change, its type generally does not.
    • the one common way a type can change is to narrow
  • To avoid confusion, both for human readers and for the type checker, avoid reusing variables for differently typed values.

Code Samples

let productId = "12-34-56";
fetchProduct(productId);

productId = 123456;
// ~~~~~~ Type 'number' is not assignable to type 'string'
fetchProductBySerialNumber(productId);
//                         ~~~~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'number'

💻 playground


let productId: string | number = "12-34-56";
fetchProduct(productId);

productId = 123456;  // OK
fetchProductBySerialNumber(productId);  // OK

💻 playground


// better solution to union type is to introduce different variables
// - it disentangles 2 unrelated concepts (ID and serial number)
// - it allows you to use more specific variable names
// - it improves type inferences, no type annotation needed
// - it results in simpler types (string and number literals, rather than string | number)
// - it let you declare variable const rather than let. This make it easier for hte type checker and people to reason about.
const productId = "12-34-56";
fetchProduct(productId);

const serial = 123456;  // OK
fetchProductBySerialNumber(serial);  // OK

💻 playground


const productId = "12-34-56";
fetchProduct(productId);

{
  const productId = 123456;  // OK
  fetchProductBySerialNumber(productId);  // OK
}

💻 playground

Understand How a Variable Gets Its Type

Things to Remember

  • Understand how TypeScript infers a type from a literal by widening it.
  • widening process is when typescript needs to decide on a set of possible values, from a single value that you specified.
  • Familiarize yourself with the ways you can affect this behavior: const, type annotations, context, helper functions, as const, and satisfies.

Code Samples

interface Vector3 { x: number; y: number; z: number; }
function getComponent(vector: Vector3, axis: 'x' | 'y' | 'z') {
  return vector[axis];
}

💻 playground


// the general fule for primitive value assigned with let is that they expand to their "base type"; x expands to string, 39 expands to number, true expands to boolean, and so on (null and undefined are handled differently)
let x = 'x';
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x);
//                ~ Argument of type 'string' is not assignable
//                  to parameter of type '"x" | "y" | "z"'

💻 playground


// if you declare a variable with const instead of let, it gets a narrower type.
const x = 'x';
//    ^? const x: "x"
let vec = {x: 10, y: 20, z: 30};
getComponent(vec, x);  // OK

💻 playground


// in the case of objects, TypeScript infers what it calls the "best common types", It determines this by treating each property as through it was assigned with let. so the type of obj.x comes out as number, this lets you assign obj.x to different number but not to a string. And it prevents you from adding other properties via direct assignment.
const obj = {
  x: 1,
};
obj.x = 3;  // OK
obj.x = '3';
//  ~ Type 'string' is not assignable to type 'number'
obj.y = 4;
//  ~ Property 'y' does not exist on type '{ x: number; }'
obj.z = 5;
//  ~ Property 'z' does not exist on type '{ x: number; }'
obj.name = 'Pythagoras';
//  ~~~~ Property 'name' does not exist on type '{ x: number; }'

💻 playground


// override default typescript behaviors
const obj: { x: string | number } = { x: 1 };
//    ^? const obj: { x: string | number; }

💻 playground


const obj1 = { x: 1, y: 2 };
//    ^? const obj1: { x: number; y: number; }

const obj2 = { x: 1 as const, y: 2 };
//    ^? const obj2: { x: 1; y: number; }

const obj3 = { x: 1, y: 2 } as const;
//    ^? const obj3: { readonly x: 1; readonly y: 2; }

💻 playground


// while type assertion are best avoided, a const assertion doesn't compromise type safety and is always OK
const arr1 = [1, 2, 3];
//    ^? const arr1: number[]
const arr2 = [1, 2, 3] as const;
//    ^? const arr2: readonly [1, 2, 3]

💻 playground


function tuple<T extends unknown[]>(...elements: T) { return elements; }

const arr3 = tuple(1, 2, 3);
//    ^? const arr3: [number, number, number]
const mix = tuple(4, 'five', true);
//    ^? const mix: [number, string, boolean]

💻 playground


// like const, Object.freeze has introduced some readonly modifiers into the the inferred types 
// unlike the const assertion, the "freeze" will be enforced by your javascript runtime, but it's shallow freeze/readonly, whereas const assertion is deep.
const frozenArray = Object.freeze([1, 2, 3]);
//    ^? const frozenArray: readonly number[]
const frozenObj = Object.freeze({x: 1, y: 2});
//    ^? const frozenObj: Readonly<{ x: 1; y: 2; }>

💻 playground


type Point = [number, number];
const capitals1 = { ny: [-73.7562, 42.6526], ca: [-121.4944, 38.5816] };
//    ^? const capitals1: { ny: number[]; ca: number[]; }

const capitals2 = {
  ny: [-73.7562, 42.6526], ca: [-121.4944, 38.5816]
} satisfies Record<string, Point>;
capitals2
// ^? const capitals2: { ny: [number, number]; ca: [number, number]; }
// satisfies prevent the values from being widened beyond the Point type
// this is an improvement over a const assertion because it will report the error where you define the object, rather than where you use it.

💻 playground


const capitals3: Record<string, Point> = capitals2;
capitals3.pr;  // undefined at runtime
//        ^? Point
capitals2.pr;
//        ~~ Property 'pr' does not exist on type '{ ny: ...; ca: ...; }'

💻 playground


const capitalsBad = {
    ny: [-73.7562, 42.6526, 148],
//  ~~ Type '[number, number, number]' is not assignable to type 'Point'.
    ca: [-121.4944, 38.5816, 26],
//  ~~ Type '[number, number, number]' is not assignable to type 'Point'.
} satisfies Record<string, Point>;

💻 playground

Create Objects All at Once

Things to Remember

  • Prefer to build objects all at once rather than piecemeal.
  • Use multiple objects and object spread syntax ({...a, ...b}) to add properties in a type-safe way.
  • Know how to conditionally add properties to an object.

Code Samples

const pt: Point = {
  x: 3,
  y: 4,
};

💻 playground


// if you need to build larger object from smaller one, avoid doing it in smaller steps
const pt = {x: 3, y: 4};
const id = {name: 'Pythagoras'};
const namedPoint = {};
Object.assign(namedPoint, pt, id);
namedPoint.name;
// ~~~~ Property 'name' does not exist on type '{}'

💻 playground


// you can build the larger object all at once using object spread syntax
const namedPoint = {...pt, ...id};
//    ^? const namedPoint: { name: string; x: number; y: number; }
namedPoint.name;  // OK
//         ^? (property) name: string

💻 playground


// you can also use object spread syntax to build up objects field by field in a type-safe way. The key is to use a new variable on every update, so that each gets new type
const pt0 = {};
const pt1 = {...pt0, x: 3};
const pt: Point = {...pt1, y: 4};  // OK

💻 playground


declare let hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};
//    ^? const president: {
//         middle?: string;
//         first: string;
//         last: string;
//       }
// or: const president = {...firstLast, ...(hasMiddle && {middle: 'S'})};

💻 playground


// you can use spread syntax to add multiple fields conditionally
declare let hasDates: boolean;
const nameTitle = {name: 'Khufu', title: 'Pharaoh'};
const pharaoh = { ...nameTitle, ...(hasDates && {start: -2589, end: -2566})};
//    ^? const pharaoh: {
//         start?: number;
//         end?: number;
//         name: string;
//         title: string;
//       }

💻 playground

Understand Type Narrowing

Things to Remember

  • Understand how TypeScript narrows types based on conditionals and other types of control flow.
  • Use tagged/discriminated unions and user-defined type guards to help the process of narrowing.
  • Think about whether code can be refactored to let TypeScript follow along more easily.

Code Samples

const elem = document.getElementById('what-time-is-it');
//in TypeScript, a symbol has a type at a location
//    ^? const elem: HTMLElement | null
if (elem) {
  elem.innerHTML = 'Party Time'.blink();
  // ^? const elem: HTMLElement
} else {
  elem
  // ^? const elem: null
  alert('No element #what-time-is-it');
}

💻 playground


const elem = document.getElementById('what-time-is-it');
//    ^? const elem: HTMLElement | null
if (!elem) throw new Error('Unable to find #what-time-is-it');
elem.innerHTML = 'Party Time'.blink();
// ^? const elem: HTMLElement

💻 playground


function contains(text: string, search: string | RegExp) {
  if (search instanceof RegExp) {
    return !!search.exec(text);
    //       ^? (parameter) search: RegExp
  }
  return text.includes(search);
  //                   ^? (parameter) search: string
}

💻 playground


interface Apple { isGoodForBaking: boolean; }
interface Orange { numSlices: number; }
function pickFruit(fruit: Apple | Orange) {
  if ('isGoodForBaking' in fruit) {
    fruit
    // ^? (parameter) fruit: Apple
  } else {
    fruit
    // ^? (parameter) fruit: Orange
  }
  fruit
  // ^? (parameter) fruit: Apple | Orange
}

💻 playground


function contains(text: string, terms: string | string[]) {
  const termList = Array.isArray(terms) ? terms : [terms];
  //    ^? const termList: string[]
  // ...
}

💻 playground


//because typeof null is "object" in javascript, you have not, in fact, excluded null with this check.
const elem = document.getElementById('what-time-is-it');
//    ^? const elem: HTMLElement | null
if (typeof elem === 'object') {
  elem;
  // ^? const elem: HTMLElement | null
}

💻 playground


function maybeLogX(x?: number | string | null) {
  if (!x) {
    console.log(x);
    //because empty string and 0 are both falsy, x could still be a string, or number
    //          ^? (parameter) x: string | number | null | undefined
  }
}

💻 playground


// this is known as "tagged union" or "dicriminated union"
interface UploadEvent { type: 'upload'; filename: string; contents: string }
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;

function handleEvent(e: AppEvent) {
  switch (e.type) {
    case 'download':
      console.log('Download', e.filename);
      //                      ^? (parameter) e: DownloadEvent
      break;
    case 'upload':
      console.log('Upload', e.filename, e.contents.length, 'bytes');
      //                    ^? (parameter) e: UploadEvent
      break;
  }
}

💻 playground


function isInputElement(el: Element): el is HTMLInputElement {
  return 'value' in el;
}

function getElementContent(el: HTMLElement) {
  if (isInputElement(el)) {
    return el.value;
    //     ^? (parameter) el: HTMLInputElement
  }
  return el.textContent;
  //     ^? (parameter) el: HTMLElement
}

💻 playground


const formEls = document.querySelectorAll('.my-form *');
const formInputEls = [...formEls].filter(isInputElement);
//    ^? const formInputEls: HTMLInputElement[]

💻 playground


const nameToNickname = new Map<string, string>();
declare let yourName: string;
let nameToUse: string;
if (nameToNickname.has(yourName)) {
  nameToUse = nameToNickname.get(yourName);
  // ~~~~~~ Type 'string | undefined' is not assignable to type 'string'.
} else {
  nameToUse = yourName;
}

const nickname = nameToNickname.get(yourName);
let nameToUse: string;
if (nickname !== undefined) {
  nameToUse = nickname;
} else {
  nameToUse = yourName;
}

// this pattern is common and can be written more concisely using the "nullish coalescing" operator (??)
const nameToUse = nameToNickname.get(yourName) ?? yourName;

💻 playground


function logLaterIfNumber(obj: { value: string | number }) {
  if (typeof obj.value === "number") {
    setTimeout(() => console.log(obj.value.toFixed()));
    //                                     ~~~~~~~
    // Property 'toFixed' does not exist on type 'string | number'.
  }
}

💻 playground


const obj: { value: string | number } = { value: 123 };
logLaterIfNumber(obj);
obj.value = 'Cookie Monster';

// by the time the callback runs, the type of obj value has changed, invalidating the refinement. This code throws an exception at runtime, and TypeScript is right to warn you about it.

💻 playground

Be Consistent in Your Use of Aliases

Things to Remember

  • Aliasing can prevent TypeScript from narrowing types. If you create an alias for a variable, use it consistently.
  • Be aware of how function calls can invalidate type refinements on properties. Trust refinements on local variables more than on properties.

Code Samples

const place = {name: 'New York', latLng: [41.6868, -74.2692]};
const loc = place.latLng;
// you have created an alas, changes to the properties on the alias will be visible on the original value as well

💻 playground


interface Coordinate {
  x: number;
  y: number;
}

interface BoundingBox {
  x: [number, number];
  y: [number, number];
}

interface Polygon {
  exterior: Coordinate[];
  holes: Coordinate[][];
  bbox?: BoundingBox;
}

💻 playground


function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  if (polygon.bbox) {
    if (pt.x < polygon.bbox.x[0] || pt.x > polygon.bbox.x[1] ||
        pt.y < polygon.bbox.y[0] || pt.y > polygon.bbox.y[1]) {
      return false;
    }
  }

  // ... more complex check
}

💻 playground


function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  polygon.bbox
  //      ^? (property) Polygon.bbox?: BoundingBox | undefined
  const box = polygon.bbox;
  //    ^? const box: BoundingBox | undefined
  if (polygon.bbox) {
    console.log(polygon.bbox);
    //                  ^? (property) Polygon.bbox?: BoundingBox
    console.log(box);
    //          ^? const box: BoundingBox | undefined
  }
}

💻 playground


// if you introduce an alias, use it consistently
function isPointInPolygon(polygon: Polygon, pt: Coordinate) {
  const box = polygon.bbox;
  if (box) {
    if (pt.x < box.x[0] || pt.x > box.x[1] ||
        pt.y < box.y[0] || pt.y > box.y[1]) {  // OK
      return false;
    }
  }
  // ...
}

💻 playground


function expandABit(p: Polygon) { /* ... */ }

polygon.bbox
//      ^? (property) Polygon.bbox?: BoundingBox | undefined
if (polygon.bbox) {
  polygon.bbox
  //      ^? (property) Polygon.bbox?: BoundingBox
  expandABit(polygon);
  // the call to expandABit could very well unset polygon.bbox, so it would be safer for the type to revert to BoundingBox | undefined. BUt this means that you would need to repeat your property checks every time you called a function. So Typescript, makes a pragmatic choice to assume the function does not invalidate its type refinements.
  // you could pass read only version of polygon to the function. By preventing mutation, we also improve type safety.
  polygon.bbox
  //      ^? (property) Polygon.bbox?: BoundingBox
}

💻 playground

Understand How Context Is Used in Type Inference

Things to Remember

  • Be aware of how context is used in type inference.
  • If factoring out a variable introduces a type error, maybe add a type annotation.
  • If the variable is truly a constant, use a const assertion (as const). But be aware that this may result in errors surfacing at use, rather than definition.
  • Prefer inlining values where it’s practical to reduce the need for type annotations.

Code Samples

function setLanguage(language: string) { /* ... */ }

setLanguage('JavaScript');  // OK

let language = 'JavaScript';
setLanguage(language);  // OK

💻 playground


type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) { /* ... */ }

setLanguage('JavaScript');  // OK

let language = 'JavaScript';
setLanguage(language);
//          ~~~~~~~~ Argument of type 'string' is not assignable
//                   to parameter of type 'Language'
// Typescript determines the type of a variable when it is first introduced (unlike some languages are able to infer types for variables based on their eventual usage.)

💻 playground


let language: Language = 'JavaScript';
setLanguage(language);  // OK

💻 playground


const language = 'JavaScript';
//    ^? const language: "JavaScript"
setLanguage(language);  // OK
// By using const, we've told the type checker that this variable cannot change. So TypeScript can infer a more precise type for language.

💻 playground


// Parameter is a (latitude, longitude) pair.
function panTo(where: [number, number]) { /* ... */ }

panTo([10, 20]);  // OK

const loc = [10, 20];
//    ^? const loc: number[]
panTo(loc);
//    ~~~ Argument of type 'number[]' is not assignable to
//        parameter of type '[number, number]'

💻 playground


const loc: [number, number] = [10, 20];
panTo(loc);  // OK

💻 playground


const loc = [10, 20] as const;
//    ^? const loc: readonly [10, 20]
panTo(loc);
//    ~~~ The type 'readonly [10, 20]' is 'readonly'
//        and cannot be assigned to the mutable type '[number, number]'

💻 playground


function panTo(where: readonly [number, number]) { /* ... */ }
const loc = [10, 20] as const;
panTo(loc);  // OK

💻 playground


const loc = [10, 20, 30] as const;  // error is really here.
panTo(loc);
//    ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to
//        parameter of type 'readonly [number, number]'
//          Source has 3 element(s) but target allows only 2.
// const contexts can neatly solve issues around losing context in inference, but they do have an unfortunate downside, if you make a mistake in the definition (say you add a third element to the tuple), then the error will be flagged at the call site, not at the definition. This may be confusing, especially if the error in a deeply nested object that's used for from where it's defined. For this reason, it's preferable to use the inline form or apply a type declaration.

💻 playground


type Language = 'JavaScript' | 'TypeScript' | 'Python';
interface GovernedLanguage {
  language: Language;
  organization: string;
}

function complain(language: GovernedLanguage) { /* ... */ }

complain({ language: 'TypeScript', organization: 'Microsoft' });  // OK

const ts = {
  language: 'TypeScript',
  organization: 'Microsoft',
};
complain(ts);
//       ~~ Argument of type '{ language: string; organization: string; }'
//            is not assignable to parameter of type 'GovernedLanguage'
//          Types of property 'language' are incompatible
//            Type 'string' is not assignable to type 'Language'
//as before, the solution is to add a type annotation (const ts: )

💻 playground


function callWithRandomNumbers(fn: (n1: number, n2: number) => void) {
  fn(Math.random(), Math.random());
}

callWithRandomNumbers((a, b) => {
  //                   ^? (parameter) a: number
  console.log(a + b);
  //              ^? (parameter) b: number
  // when you pass a callback to another function, TypeScript uses context to infer the parameter types of the callback
  // if you factor the callback out into a constant, you lose that context and get noImplicitAny errors
  // if the function is only used in one place, prefer the inline form since it reduces the need for annotations
});

💻 playground


const fn = (a, b) => {
  //        ~    Parameter 'a' implicitly has an 'any' type
  //           ~ Parameter 'b' implicitly has an 'any' type
  console.log(a + b);
}
callWithRandomNumbers(fn);

💻 playground


const fn = (a: number, b: number) => {
  console.log(a + b);
}
callWithRandomNumbers(fn);

💻 playground

Understand Evolving types

  • example 1:
const result = [];  // Type is any[]
result.push('a');
result  // Type is string[]
result.push(1);
result  // Type is (string | number)[]
  • Implicit any types do not evolve through function calls. The arrow function here trips up inference:
function makeSquares(start: number, limit: number) {
  const out = [];
     // ~~~ Variable 'out' implicitly has type 'any[]' in some locations
  range(start, limit).forEach(i => {
    out.push(i * i);
  });
  return out;
      // ~~~ Variable 'out' implicitly has an 'any[]' type
}
  • In cases like this, you may want to consider using an array’s map and filter methods to build arrays in a single statement and avoid iteration and evolving any entirely

  • Things to Remember

    • While TypeScript types typically only refine, the types of values initialized to null, undefined, [], any and any[] types are allowed to evolve.
    • You should be able to recognize and understand this construct where it occurs. and use it to reduce the need for type annotations in your own code.
    • For better error checking, consider providing an explicit type annotation instead of using evolving any.

Use Functional Constructs and Libraries to Help Types Flow

Things to Remember

  • Use built-in functional constructs and those in utility libraries like Lodash instead of hand-rolled constructs to improve type flow, increase legibility, and reduce the need for explicit type annotations.
  • JavaScript has never included the sort of standard library you find in Python, C, or Java. Over the years, many libraries have tried to fill the game. JQuery provided helpers not just for interacting with the DOM but also for iterating and mapping over objects and arrays. Underscore focused more on providing general utility functions, and Lodash built on this effort. Today libraries like Ramda continue bring ideas from functional programming into JavaScript world.

Code Samples

const csvData = "...";
const rawRows = csvData.split('\n');
const headers = rawRows[0].split(',');

const rows = rawRows.slice(1).map((rowStr) => {
  const row = {};
  rowStr.split(",").forEach((val, j) => {
    row[headers[j]] = val;
  });
  return row;
});

💻 playground


import _ from 'lodash';
const rows = rawRows.slice(1)
    .map(rowStr => _.zipObject(headers, rowStr.split(',')));

💻 playground


const rowsImperative = rawRows.slice(1).map(rowStr => {
  const row = {};
  rowStr.split(',').forEach((val, j) => {
    row[headers[j]] = val;
    // ~~~~~~~~~~~~ No index signature with a parameter of
    //              type 'string' was found on type '{}'
  });
  return row;
});
const rowsFunctional = rawRows.slice(1)
  .map((rowStr) =>
    rowStr
      .split(",")
      .reduce(
        (row, val, i) => ((row[headers[i]] = val), row),
        //                 ~~~~~~~~~~~~~~~ No index signature with a parameter of
        //                                 type 'string' was found on type '{}'
        {}
      )
  );
  // the solution in each case is to provide a type annotation for {}, either {[column: string]: string} or Record<string, string>

💻 playground


const rowsLodash =
  rawRows.slice(1).map(rowStr => _.zipObject(headers, rowStr.split(',')));
rowsLodash
// ^? const rowsLodash: _.Dictionary<string>[]

💻 playground


interface BasketballPlayer {
  name: string;
  team: string;
  salary: number;
}
declare const rosters: {[team: string]: BasketballPlayer[]};

💻 playground


let allPlayers = [];
//  ~~~~~~~~~~ Variable 'allPlayers' implicitly has type 'any[]'
//             in some locations where its type cannot be determined
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players);
  //           ~~~~~~~~~~ Variable 'allPlayers' implicitly has an 'any[]' type
}

💻 playground


let allPlayers: BasketballPlayer[] = [];
for (const players of Object.values(rosters)) {
  allPlayers = allPlayers.concat(players);  // OK
}

💻 playground


const allPlayers = Object.values(rosters).flat(); // OK
//    ^? const allPlayers: BasketballPlayer[]

💻 playground


const teamToPlayers: {[team: string]: BasketballPlayer[]} = {};
for (const player of allPlayers) {
  const {team} = player;
  teamToPlayers[team] = teamToPlayers[team] || [];
  teamToPlayers[team].push(player);
}

for (const players of Object.values(teamToPlayers)) {
  players.sort((a, b) => b.salary - a.salary);
}

const bestPaid = Object.values(teamToPlayers).map(players => players[0]);
bestPaid.sort((playerA, playerB) => playerB.salary - playerA.salary);
console.log(bestPaid);

// better option,

const bestPaid = _(allPlayers)
  .groupBy(player => player.team)
  .mapValues(players => _.maxBy(players, p => p.salary)!)
  .values()
  .sortBy(p => -p.salary)
  .value();
console.log(bestPaid.slice(0, 10));
//          ^? const bestPaid: BasketballPlayer[]

💻 playground

Use async Functions Instead of Callbacks to Improve Type Flow

Things to Remember

  • Prefer Promises to callbacks for better composability and type flow.
  • Prefer async and await to raw Promises when possible. They produce more concise, straightforward code and eliminate whole classes of errors.
  • If a function returns a Promise, declare it async.

Code Samples

// classic javascript modeled asynchronous behavior using callbacks. This led to the infamous "pyramid of doom"
declare function fetchURL(
  url: string, callback: (response: string) => void
): void;

fetchURL(url1, function(response1) {
  fetchURL(url2, function(response2) {
    fetchURL(url3, function(response3) {
      // ...
      console.log(1);
    });
    console.log(2);
  });
  console.log(3);
});
console.log(4);

// Logs:
// 4
// 3
// 2
// 1

💻 playground


const page1Promise = fetch(url1);
page1Promise.then(response1 => {
  return fetch(url2);
}).then(response2 => {
  return fetch(url3);
}).then(response3 => {
  // ...
}).catch(error => {
  // ...
});

💻 playground


// the await keyword pauses execution of the fetchPages. Within an async function, awaiting a Promise that rejects will throw an exception. This lets you use the usual try/catch machinery
async function fetchPages() {
  try {
    const response1 = await fetch(url1);
    const response2 = await fetch(url2);
    const response3 = await fetch(url3);
    // ...
  } catch (e) {
    // ...
  }
}

💻 playground


//there are few good reasons to prefer Promises or async/await callbacks:
// - Promises are easier to compose than callbacks
// - Types are able to flow through Promises more easily than callbacks
// - if you want to fetch the pages concurrently, for example, you can compose Promises with Promise all
async function fetchPages() {
  const [response1, response2, response3] = await Promise.all([
    fetch(url1), fetch(url2), fetch(url3)
  ]);
  // ...
}

💻 playground


function fetchPagesWithCallbacks() {
  let numDone = 0;
  const responses: string[] = [];
  const done = () => {
    const [response1, response2, response3] = responses;
    // ...
  };
  const urls = [url1, url2, url3];
  urls.forEach((url, i) => {
    fetchURL(url, r => {
      responses[i] = url;
      numDone++;
      if (numDone === urls.length) done();
    });
  });
}

💻 playground


function timeout(timeoutMs: number): Promise<never> {
  return new Promise((resolve, reject) => {
     setTimeout(() => reject('timeout'), timeoutMs);
  });
}

//Type inference also works well with Promise.race, which resolves when the first of its input Promises resolves.
async function fetchWithTimeout(url: string, timeoutMs: number) {
  return Promise.race([fetch(url), timeout(timeoutMs)]);
}

💻 playground


// 
async function getNumber() { return 42; }
//             ^? function getNumber(): Promise<number>

const getNumber = async () => 42;
//    ^? const getNumber: () => Promise<number>

// you may occasionally need to use raw Promises, notably when you're wrapping a callback API like setTimeout, but if you have a choice, you should generally prefer async/await to raw Promises for 2 reasons
// - it typically produces more concise and straightforward code
// - it enforces that async function always return Promises
const getNumber = () => Promise.resolve(42);
//    ^? const getNumber: () => Promise<number>

💻 playground


// a function should either always be run synchronously or always be run asynchronously. It should never mix the two
// Don't do this!
const _cache: {[url: string]: string} = {};
function fetchWithCache(url: string, callback: (text: string) => void) {
  if (url in _cache) {
    callback(_cache[url]);
  } else {
    fetchURL(url, text => {
      _cache[url] = text;
      callback(text);
    });
  }
}

💻 playground


let requestStatus: 'loading' | 'success' | 'error';
function getUser(userId: string) {
  fetchWithCache(`/user/${userId}`, profile => {
    requestStatus = 'success';
  });
  requestStatus = 'loading';
}

💻 playground


const _cache: {[url: string]: string} = {};
async function fetchWithCache(url: string) {
  if (url in _cache) {
    return _cache[url];
  }
  const response = await fetch(url);
  const text = await response.text();
  _cache[url] = text;
  return text;
}

let requestStatus: 'loading' | 'success' | 'error';
async function getUser(userId: string) {
  requestStatus = 'loading';
  const profile = await fetchWithCache(`/user/${userId}`);
  requestStatus = 'success';
}

💻 playground


// if you return a Promise from an async function, it will not get wrapped in another Promise, the return type will be Promise<T> rather than Promise<Promise<T>>
async function getJSON(url: string) {
  const response = await fetch(url);
  const jsonPromise = response.json();
  return jsonPromise;
  //     ^? const jsonPromise: Promise<any>
}
getJSON
// ^? function getJSON(url: string): Promise<any>

💻 playground

Use Classes and Currying to Create New Inference Sites

Things to Remember

  • For functions with multiple type parameters, inference is all or nothing: either all type parameters are inferred or all must be specified explicitly.
  • To get partial inference, use either classes or currying to create a new inference site.
  • Prefer the currying approach if you’d like to create a local type alias.

//// // verifier:reset## Code Samples

export interface SeedAPI {
  '/seeds': Seed[];
  '/seed/apple': Seed;
  '/seed/strawberry': Seed;
  // ...
}

💻 playground


declare function fetchAPI<
  API, Path extends keyof API
>(path: Path): Promise<API[Path]>;

💻 playground


fetchAPI<SeedAPI>('/seed/strawberry');
//       ~~~~~~~ Expected 2 type arguments, but got 1.

💻 playground


const berry = fetchAPI<SeedAPI, '/seed/strawberry'>('/seed/strawberry');  // ok
//    ^? const berry: Promise<Seed>

💻 playground


declare class ApiFetcher<API> {
  fetch<Path extends keyof API>(path: Path): Promise<API[Path]>;
}

💻 playground


const fetcher = new ApiFetcher<SeedAPI>();
const berry = await fetcher.fetch('/seed/strawberry'); // OK
//    ^? const berry: Seed

fetcher.fetch('/seed/chicken');
//            ~~~~~~~~~~~~~~~
// Argument of type '"/seed/chicken"' is not assignable to type 'keyof SeedAPI'

const seed: Seed = await fetcher.fetch('/seeds');
//    ~~~~ Seed[] is not assignable to Seed

💻 playground


declare function getDate(mon: string, day: number): Date;
getDate('dec', 25);

💻 playground


declare function getDate(mon: string): (day: number) => Date;
getDate('dec')(25);

💻 playground


declare function fetchAPI<API>():
  <Path extends keyof API>(path: Path) => Promise<API[Path]>;

💻 playground


const berry = await fetchAPI<SeedAPI>()('/seed/strawberry'); // OK
//    ^? const berry: Seed

fetchAPI<SeedAPI>()('/seed/chicken');
//                  ~~~~~~~~~~~~~~~
// Argument of type '"/seed/chicken"' is not assignable to type 'keyof SeedAPI'
//
const seed: Seed = await fetchAPI<SeedAPI>()('/seeds');
//    ~~~~ Seed[] is not assignable to Seed

💻 playground


const fetchSeedAPI = fetchAPI<SeedAPI>();
const berry = await fetchSeedAPI('/seed/strawberry');
//    ^? const berry: Seed

💻 playground


declare function apiFetcher<API>(): {
  fetch<Path extends keyof API>(path: Path): Promise<API[Path]>;
}

const fetcher = apiFetcher<SeedAPI>();
fetcher.fetch('/seed/strawberry');  // ok

💻 playground


function fetchAPI<API>() {
  type Routes = keyof API & string;  // local type alias

  return <Path extends Routes>(
    path: Path
  ): Promise<API[Path]> => fetch(path).then(r => r.json());
}

💻 playground

Item 29: Prefer Types That Always Represent Valid States

Things to Remember

  • Types that represent both valid and invalid states are likely to lead to confusing and error-prone code.
  • Prefer types that only represent valid states. Even if they are longer or harder to express, they will save you time and pain in the end!

Code Samples

interface State {
  pageText: string;
  isLoading: boolean;
  error?: string;
}

💻 playground


function renderPage(state: State) {
  if (state.error) {
    return `Error! Unable to load ${currentPage}: ${state.error}`;
  } else if (state.isLoading) {
    return `Loading ${currentPage}...`;
  }
  return `<h1>${currentPage}</h1>\n${state.pageText}`;
}

💻 playground


async function changePage(state: State, newPage: string) {
  state.isLoading = true;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const text = await response.text();
    state.isLoading = false;
    state.pageText = text;
  } catch (e) {
    state.error = '' + e;
  }
}

💻 playground


interface RequestPending {
  state: 'pending';
}
interface RequestError {
  state: 'error';
  error: string;
}
interface RequestSuccess {
  state: 'ok';
  pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;

interface State {
  currentPage: string;
  requests: {[page: string]: RequestState};
}

💻 playground


function renderPage(state: State) {
  const {currentPage} = state;
  const requestState = state.requests[currentPage];
  switch (requestState.state) {
    case 'pending':
      return `Loading ${currentPage}...`;
    case 'error':
      return `Error! Unable to load ${currentPage}: ${requestState.error}`;
    case 'ok':
      return `<h1>${currentPage}</h1>\n${requestState.pageText}`;
  }
}

async function changePage(state: State, newPage: string) {
  state.requests[newPage] = {state: 'pending'};
  state.currentPage = newPage;
  try {
    const response = await fetch(getUrlForPage(newPage));
    if (!response.ok) {
      throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
    }
    const pageText = await response.text();
    state.requests[newPage] = {state: 'ok', pageText};
  } catch (e) {
    state.requests[newPage] = {state: 'error', error: '' + e};
  }
}

💻 playground


interface CockpitControls {
  /** Angle of the left side stick in degrees, 0 = neutral, + = forward */
  leftSideStick: number;
  /** Angle of the right side stick in degrees, 0 = neutral, + = forward */
  rightSideStick: number;
}

💻 playground


function getStickSetting(controls: CockpitControls) {
  return controls.leftSideStick;
}

💻 playground


function getStickSetting(controls: CockpitControls) {
  const {leftSideStick, rightSideStick} = controls;
  if (leftSideStick === 0) {
    return rightSideStick;
  }
  return leftSideStick;
}

💻 playground


function getStickSetting(controls: CockpitControls) {
  const {leftSideStick, rightSideStick} = controls;
  if (leftSideStick === 0) {
    return rightSideStick;
  } else if (rightSideStick === 0) {
    return leftSideStick;
  }
  // ???
}

💻 playground


function getStickSetting(controls: CockpitControls) {
  const {leftSideStick, rightSideStick} = controls;
  if (leftSideStick === 0) {
    return rightSideStick;
  } else if (rightSideStick === 0) {
    return leftSideStick;
  }
  if (Math.abs(leftSideStick - rightSideStick) < 5) {
    return (leftSideStick + rightSideStick) / 2;
  }
  // ???
}

💻 playground


function getStickSetting(controls: CockpitControls) {
  return (controls.leftSideStick + controls.rightSideStick) / 2;
}

💻 playground


interface CockpitControls {
  /** Angle of the stick in degrees, 0 = neutral, + = forward */
  stickAngle: number;
}

💻 playground

Item 30: Be Liberal in What You Accept and Strict in What You Produce

Things to Remember

  • Input types tend to be broader than output types. Optional properties and union types are more common in parameter types than return types.
  • Avoid broad return types since these will be awkward for clients to use.
  • To reuse types between parameters and return types, introduce a canonical form (for return types) and a looser form (for parameters).
  • Use Iterable<T> instead of T[] if you only need to iterate over your function parameter.

Code Samples

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

💻 playground


interface CameraOptions {
  center?: LngLat;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}
type LngLat =
  { lng: number; lat: number; } |
  { lon: number; lat: number; } |
  [number, number];

💻 playground


type LngLatBounds =
  {northeast: LngLat, southwest: LngLat} |
  [LngLat, LngLat] |
  [number, number, number, number];

💻 playground


function focusOnFeature(f: Feature) {
  const bounds = calculateBoundingBox(f); // helper function
  const camera = viewportForBounds(bounds);
  setCamera(camera);
  const {center: {lat, lng}, zoom} = camera;
               // ~~~      Property 'lat' does not exist on type ...
               //      ~~~ Property 'lng' does not exist on type ...
  zoom;
  // ^? const zoom: number | undefined
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

💻 playground


interface LngLat { lng: number; lat: number; };
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number];

interface Camera {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
  center?: LngLatLike;
}
type LngLatBounds =
  {northeast: LngLatLike, southwest: LngLatLike} |
  [LngLatLike, LngLatLike] |
  [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;

💻 playground


interface CameraOptions {
  center?: LngLatLike;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

💻 playground


function focusOnFeature(f: Feature) {
  const bounds = calculateBoundingBox(f);
  const camera = viewportForBounds(bounds);
  setCamera(camera);
  const {center: {lat, lng}, zoom} = camera;  // OK
  //                         ^? const zoom: number
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

💻 playground


function sum(xs: number[]): number {
  let sum = 0;
  for (const x of xs) {
    sum += x;
  }
  return sum;
}

💻 playground


function sum(xs: Iterable<number>): number {
  let sum = 0;
  for (const x of xs) {
    sum += x;
  }
  return sum;
}

💻 playground


const six = sum([1, 2, 3]);
//    ^? const six: number

💻 playground


function* range(limit: number) {
  for (let i = 0; i < limit; i++) {
    yield i;
  }
}
const zeroToNine = range(10);
//    ^? const zeroToNine: Generator<number, void, unknown>
const fortyFive = sum(zeroToNine);  // ok, result is 45

💻 playground

Don’t Repeat Type Information in Documentation

  • example 1:
/**
 * Returns a string with the foreground color.
 * Takes zero or one arguments. With no arguments, returns the
 * standard foreground color. With one argument, returns the foreground color
 * for a particular page.
 */
function getForegroundColor(page?: string) {
  return page === 'login' ? {r: 127, g: 127, b: 127} : {r: 0, g: 0, b: 0};
}

better declaration and comment might look like this:

interface Color {
  r: number;
  g: number;
  b: number;
}

/** Get the foreground color for the application or a specific page. */
function getForegroundColor(page?: string): Color {
  // ...
}
  • Example 2:
/** Does not modify nums */
function sort(nums: number[]) { /* ... */ }

declare it readonly and let TypeScript enforce the contract

function sort(nums: readonly number[]) { /* ... */ }
  • Avoid repeating type information in comments and variable names. In the best case it is duplicative of type declarations, and in the worst it will lead to conflicting information.

  • Consider including units in variable names if they aren’t clear from the type (e.g., timeMs or temperatureC).

Item 32: Avoid Including null or undefined in Type Aliases

Things to Remember

  • Avoid defining type aliases that include null or undefined.

Code Samples

function getCommentsForUser(comments: readonly Comment[], user: User) {
  return comments.filter(comment => comment.userId === user?.id);
}

💻 playground


type User = { id: string; name: string; } | null;

💻 playground


interface User {
  id: string;
  name: string;
}

💻 playground


type NullableUser = { id: string; name: string; } | null;

💻 playground


function getCommentsForUser(comments: readonly Comment[], user: User | null) {
  return comments.filter(comment => comment.userId === user?.id);
}

💻 playground


type BirthdayMap = {
  [name: string]: Date | undefined;
};

💻 playground


type BirthdayMap = {
  [name: string]: Date | undefined;
} | null;

💻 playground

push null values to the perimeter of your types

  • example 1:
    • avoid
let min, max;
...
return [min, max];
  • better approach would be:
let result: [number, number] | null = null;
...
return result;
  • mix of null and non-null values can also lead to problems in classes. For example
class UserPosts {
  user: UserInfo | null;
  posts: Post [] | null;

  constructor () {
    this.user = null;
    this.posts = null;
  }

  async init(userId: string) {
    return Promise.all([
      async () => this.user = await fetchUser(userId),
      async () => this.posts = await fetchPostsForUser(userId)
    ]);
  }
}
  • better design would wait until all the data used by the class is available
class UserPosts {
  user: UserInfo;
  posts: Post [];

  constructor () {
    this.user = null;
    this.posts = null;
  }

  async init(userId: string) {
    const [user, posts] = Promise.all([
      async () => this.user = await fetchUser(userId),
      async () => this.posts = await fetchPostsForUser(userId)
    ]);
    return new UserPosts(user, posts);
  }
}
  • don’t be tempted to replace nullable properties with promises. This tends to lead to even more confusing code and forces all your methods to be async. Promises clarify the code that loads data but tend to have the opposite effect on the class that uses that data.

  • Things to Remember

    • Avoid designs in which one value being null or not null is implicitly related to another value being null or not null.
    • Push null values to the perimeter of your API by making larger objects either null or fully non-null. This will make code clearer both for human readers and for the type checker.
    • Consider creating a fully non-null class and constructing it when all values are available.
    • While strictNullChecks may flag many issues in your code, it’s indispensable for surfacing the behavior of functions with respect to null values.

Item 34: Prefer Unions of Interfaces to Interfaces with Unions

Things to Remember

  • Interfaces with multiple properties that are union types are often a mistake because they obscure the relationships between these properties.
  • Unions of interfaces are more precise and can be understood by TypeScript.
  • Use tagged unions to facilitate control flow analysis. Because they are so well supported, this pattern is ubiquitous in TypeScript code.
  • Consider whether multiple optional properties could be grouped to more accurately model your data.

Code Samples

interface Layer {
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

💻 playground


interface FillLayer {
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

💻 playground


interface Layer {
  type: 'fill' | 'line' | 'point';
  layout: FillLayout | LineLayout | PointLayout;
  paint: FillPaint | LinePaint | PointPaint;
}

💻 playground


interface FillLayer {
  type: 'fill';
  layout: FillLayout;
  paint: FillPaint;
}
interface LineLayer {
  type: 'line';
  layout: LineLayout;
  paint: LinePaint;
}
interface PointLayer {
  type: 'paint';
  layout: PointLayout;
  paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;

💻 playground


function drawLayer(layer: Layer) {
  if (layer.type === 'fill') {
    const {paint} = layer;
    //     ^? const paint: FillPaint
    const {layout} = layer;
    //     ^? const layout: FillLayout
  } else if (layer.type === 'line') {
    const {paint} = layer;
    //     ^? const paint: LinePaint
    const {layout} = layer;
    //     ^? const layout: LineLayout
  } else {
    const {paint} = layer;
    //     ^? const paint: PointPaint
    const {layout} = layer;
    //     ^? const layout: PointLayout
  }
}

💻 playground


interface Person {
  name: string;
  // These will either both be present or not be present
  placeOfBirth?: string;
  dateOfBirth?: Date;
}

💻 playground


interface Person {
  name: string;
  birth?: {
    place: string;
    date: Date;
  }
}

💻 playground


const alanT: Person = {
  name: 'Alan Turing',
  birth: {
// ~~~~ Property 'date' is missing in type
//      '{ place: string; }' but required in type
//      '{ place: string; date: Date; }'
    place: 'London'
  }
}

💻 playground


function eulogize(person: Person) {
  console.log(person.name);
  const {birth} = person;
  if (birth) {
    console.log(`was born on ${birth.date} in ${birth.place}.`);
  }
}

💻 playground


interface Name {
  name: string;
}

interface PersonWithBirth extends Name {
  placeOfBirth: string;
  dateOfBirth: Date;
}

type Person = Name | PersonWithBirth;

💻 playground


function eulogize(person: Person) {
  if ('placeOfBirth' in person) {
    person
    // ^? (parameter) person: PersonWithBirth
    const {dateOfBirth} = person;  // OK
    //     ^? const dateOfBirth: Date
  }
}

💻 playground

Item 35: Prefer More Precise Alternatives to String Types

Things to Remember

  • Avoid “stringly typed” code. Prefer more appropriate types where not every string is a possibility.
  • Prefer a union of string literal types to string if that more accurately describes the domain of a variable. You’ll get stricter type checking and improve the development experience.
  • Prefer keyof T to string for function parameters that are expected to be properties of an object.

Code Samples

interface Album {
  artist: string;
  title: string;
  releaseDate: string;  // YYYY-MM-DD
  recordingType: string;  // E.g., "live" or "studio"
}

💻 playground


const kindOfBlue: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: 'August 17th, 1959',  // Oops!
  recordingType: 'Studio',  // Oops!
};  // OK

💻 playground


function recordRelease(title: string, date: string) { /* ... */ }
recordRelease(kindOfBlue.releaseDate, kindOfBlue.title);  // OK, should be error

💻 playground


type RecordingType = 'studio' | 'live';

interface Album {
  artist: string;
  title: string;
  releaseDate: Date;
  recordingType: RecordingType;
}

💻 playground


const kindOfBlue: Album = {
  artist: 'Miles Davis',
  title: 'Kind of Blue',
  releaseDate: new Date('1959-08-17'),
  recordingType: 'Studio'
// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'
};

💻 playground


function getAlbumsOfType(recordingType: string): Album[] {
  // ...
}

💻 playground


/** What type of environment was this recording made in? */
type RecordingType = 'live' | 'studio';

💻 playground


function pluck(records, key) {
  return records.map(r => r[key]);
}

💻 playground


function pluck(records: any[], key: string): any[] {
  return records.map(r => r[key]);
}

💻 playground


function pluck<T>(records: T[], key: string): any[] {
  return records.map(r => r[key]);
  //                      ~~~~~~ Element implicitly has an 'any' type
  //                             because type '{}' has no index signature
}

💻 playground


type K = keyof Album;
//   ^? type K = keyof Album
//      (equivalent to "artist" | "title" | "releaseDate" | "recordingType")

💻 playground


function pluck<T>(records: T[], key: keyof T) {
  return records.map(r => r[key]);
}

💻 playground


const releaseDates = pluck(albums, 'releaseDate');
//    ^? const releaseDates: (string | Date)[]

💻 playground


function pluck<T, K extends keyof T>(records: T[], key: K): T[K][] {
  return records.map(r => r[key]);
}

💻 playground


const dates = pluck(albums, 'releaseDate');
//    ^? const dates: Date[]
const artists = pluck(albums, 'artist');
//    ^? const artists: string[]
const types = pluck(albums, 'recordingType');
//    ^? const types: RecordingType[]
const mix = pluck(albums, Math.random() < 0.5 ? 'releaseDate' : 'artist');
//    ^? const mix: (string | Date)[]
const badDates = pluck(albums, 'recordingDate');
//                             ~~~~~~~~~~~~~~~
// Argument of type '"recordingDate"' is not assignable to parameter of type ...

💻 playground

Item 36: Use a Distinct Type for Special Values

Things to Remember

  • Avoid special values that are assignable to regular values in a type. They will reduce TypeScript’s ability to find bugs in your code.
  • Prefer null or undefined as a special value instead of 0, -1, or "".
  • Consider using a tagged union rather than null or undefined if the meaning of those values isn’t clear.

Code Samples

function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
  const index = vals.indexOf(val);
  return [vals.slice(0, index), vals.slice(index+1)];
}

💻 playground


function safeIndexOf<T>(vals: readonly T[], val: T): number | null {
  const index = vals.indexOf(val);
  return index === -1 ? null : index;
}

💻 playground


function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
  const index = safeIndexOf(vals, val);
  return [vals.slice(0, index), vals.slice(index+1)];
  //                    ~~~~~              ~~~~~ 'index' is possibly 'null'
}

💻 playground


function splitAround<T>(vals: readonly T[], val: T): [T[], T[]] {
  const index = safeIndexOf(vals, val);
  if (index === null) {
    return [[...vals], []];
  }
  return [vals.slice(0, index), vals.slice(index+1)];  // ok
}

💻 playground


interface Product {
  title: string;
  priceDollars: number;
}

💻 playground


interface Product {
  title: string;
  /** Price of the product in dollars, or -1 if price is unknown */
  priceDollars: number;
}

💻 playground


// @strictNullChecks: false
const truck: Product = {
  title: 'Tesla Cybertruck',
  priceDollars: null,  // ok
};

💻 playground

Item 37: Limit the Use of Optional Properties

Things to Remember

  • Optional properties can prevent the type checker from finding bugs and can lead to repeated and possibly inconsistent code for filling in default values.
  • Think twice before adding an optional property to an interface. Consider whether you could make it required instead.
  • Consider creating distinct types for un-normalized input data and normalized data for use in your code.
  • Avoid a combinatorial explosion of options.

Code Samples

interface FormattedValue {
  value: number;
  units: string;
}
function formatValue(value: FormattedValue) { /* ... */ }

💻 playground


interface Hike {
  miles: number;
  hours: number;
}
function formatHike({miles, hours}: Hike) {
  const distanceDisplay = formatValue({value: miles, units: 'miles'});
  const paceDisplay = formatValue({value: miles / hours, units: 'mph'});
  return `${distanceDisplay} at ${paceDisplay}`;
}

💻 playground


type UnitSystem = 'metric' | 'imperial';
interface FormattedValue {
  value: number;
  units: string;
  /** default is imperial */
  unitSystem?: UnitSystem;
}

💻 playground


interface AppConfig {
  darkMode: boolean;
  // ... other settings ...
  /** default is imperial */
  unitSystem?: UnitSystem;
}

💻 playground


function formatHike({miles, hours}: Hike, config: AppConfig) {
  const { unitSystem } = config;
  const distanceDisplay = formatValue({
    value: miles, units: 'miles', unitSystem
  });
  const paceDisplay = formatValue({
    value: miles / hours, units: 'mph'  // forgot unitSystem, oops!
  });
  return `${distanceDisplay} at ${paceDisplay}`;
}

💻 playground


declare let config: AppConfig;
const unitSystem = config.unitSystem ?? 'imperial';

💻 playground


const unitSystem = config.unitSystem ?? 'metric';

💻 playground


interface InputAppConfig {
  darkMode: boolean;
  // ... other settings ...
  /** default is imperial */
  unitSystem?: UnitSystem;
}
interface AppConfig extends InputAppConfig {
  unitSystem: UnitSystem;  // required
}

💻 playground


function normalizeAppConfig(inputConfig: InputAppConfig): AppConfig {
  return {
    ...inputConfig,
    unitSystem: inputConfig.unitSystem ?? 'imperial',
  };
}

💻 playground

Item 38: Avoid Repeated Parameters of the Same Type

Things to Remember

  • Avoid writing functions that take consecutive parameters with the same TypeScript type.
  • Refactor functions that take many parameters to take fewer parameters with distinct types, or a single object parameter.

Code Samples

drawRect(25, 50, 75, 100, 1);

💻 playground


function drawRect(x: number, y: number, w: number, h: number, opacity: number) {
  // ...
}

💻 playground


interface Point {
  x: number;
  y: number;
}
interface Dimension {
  width: number;
  height: number;
}
function drawRect(topLeft: Point, size: Dimension, opacity: number) {
  // ...
}

💻 playground


drawRect({x: 25, y: 50}, {x: 75, y: 100}, 1.0);
//                        ~
// Argument ... is not assignable to parameter of type 'Dimension'.

💻 playground


interface DrawRectParams extends Point, Dimension {
  opacity: number;
}
function drawRect(params: DrawRectParams) { /* ... */ }

drawRect({x: 25, y: 50, width: 75, height: 100, opacity: 1.0});

💻 playground

Item 39: Prefer Unifying Types to Modeling Differences

Things to Remember

  • Having distinct variants of the same type creates cognitive overhead and requires lots of conversion code.
  • Rather than modeling slight variations on a type in your code, try to eliminate the variation so that you can unify to a single type.
  • Unifying types may require some adjustments to runtime code.
  • If the types aren’t in your control, you may need to model the variations.
  • Don’t unify types that aren’t representing the same thing.

////## Code Samples

interface StudentTable {
  first_name: string;
  last_name: string;
  birth_date: string;
}

💻 playground


interface Student {
  firstName: string;
  lastName: string;
  birthDate: string;
}

💻 playground


type Student = ObjectToCamel<StudentTable>;
//   ^? type Student = {
//        firstName: string;
//        lastName: string;
//        birthDate: string;
//      }

💻 playground


async function writeStudentToDb(student: Student) {
  await writeRowToDb(db, 'students', student);
  //                                 ~~~~~~~
  // Type 'Student' is not assignable to parameter of type 'StudentTable'.
}

💻 playground


async function writeStudentToDb(student: Student) {
  await writeRowToDb(db, 'students', objectToSnake(student));  // ok
}

💻 playground

Item 40: Prefer Imprecise Types to Inaccurate Types

Things to Remember

  • Avoid the uncanny valley of type safety: complex but inaccurate types are often worse than simpler, less precise types. If you cannot model a type accurately, do not model it inaccurately! Acknowledge the gaps using any or unknown.
  • Pay attention to error messages and autocomplete as you make typings increasingly precise. It’s not just about correctness: developer experience matters, too.
  • As your types grow more complex, your test suite for them should expand.

Code Samples

interface Point {
  type: 'Point';
  coordinates: number[];
}
interface LineString {
  type: 'LineString';
  coordinates: number[][];
}
interface Polygon {
  type: 'Polygon';
  coordinates: number[][][];
}
type Geometry = Point | LineString | Polygon;  // Also several others

💻 playground


type GeoPosition = [number, number];
interface Point {
  type: 'Point';
  coordinates: GeoPosition;
}
// Etc.

💻 playground


type Expression1 = any;
type Expression2 = number | string | any[];

💻 playground


const okExpressions: Expression2[] = [
  10,
  "red",
  ["+", 10, 5],
  ["rgb", 255, 128, 64],
  ["case", [">", 20, 10], "red", "blue"],
];
const invalidExpressions: Expression2[] = [
  true,
// ~~~ Type 'boolean' is not assignable to type 'Expression2'
  ["**", 2, 31],  // Should be an error: no "**" function
  ["rgb", 255, 0, 127, 0],  // Should be an error: too many values
  ["case", [">", 20, 10], "red", "blue", "green"],  // (Too many values)
];

💻 playground


type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb';
type CallExpression = [FnName, ...any[]];
type Expression3 = number | string | CallExpression;

const okExpressions: Expression3[] = [
  10,
  "red",
  ["+", 10, 5],
  ["rgb", 255, 128, 64],
  ["case", [">", 20, 10], "red", "blue"],
];
const invalidExpressions: Expression3[] = [
  true,
  // Error: Type 'boolean' is not assignable to type 'Expression3'
  ["**", 2, 31],
  // ~~ Type '"**"' is not assignable to type 'FnName'
  ["rgb", 255, 0, 127, 0],  // Should be an error: too many values
  ["case", [">", 20, 10], "red", "blue", "green"],  // (Too many values)
];

💻 playground


type Expression4 = number | string | CallExpression;

type CallExpression = MathCall | CaseCall | RGBCall;

type MathCall = [
  '+' | '-' | '/' | '*' | '>' | '<',
  Expression4,
  Expression4,
];

interface CaseCall {
  0: 'case';
  [n: number]: Expression4;
  length: 4 | 6 | 8 | 10 | 12 | 14 | 16; // etc.
}

type RGBCall = ['rgb', Expression4, Expression4, Expression4];

💻 playground


const okExpressions: Expression4[] = [
  10,
  "red",
  ["+", 10, 5],
  ["rgb", 255, 128, 64],
  ["case", [">", 20, 10], "red", "blue"],
];
const invalidExpressions: Expression4[] = [
  true,
// ~~~ Type 'boolean' is not assignable to type 'Expression4'
  ["**", 2, 31],
// ~~~~ Type '"**"' is not assignable to type '"+" | "-" | "/" | ...
  ["rgb", 255, 0, 127, 0],
  //                   ~ Type 'number' is not assignable to type 'undefined'.
  ["case", [">", 20, 10], "red", "blue", "green"],
  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  // Types of property 'length' are incompatible.
  //    Type '5' is not assignable to type '4 | 6 | 8 | 10 | 12 | 14 | 16'.
];

💻 playground


const moreOkExpressions: Expression4[] = [
  ['-', 12],
  // ~~~~~~ Type '["-", number]' is not assignable to type 'MathCall'.
  //          Source has 2 element(s) but target requires 3.
  ['+', 1, 2, 3],
  //          ~ Type 'number' is not assignable to type 'undefined'.
  ['*', 2, 3, 4],
  //          ~ Type 'number' is not assignable to type 'undefined'.
];

💻 playground

Item 41: Name Types Using the Language of Your Problem Domain

Things to Remember

  • Reuse names from the domain of your problem where possible to increase the readability and level of abstraction of your code. Make sure you use domain terms accurately.
  • Avoid using different names for the same thing: make distinctions in names meaningful.
  • Avoid vague names like “Info” or “Entity.” Name types for what they are, rather than for their shape.

Code Samples

interface Animal {
  name: string;
  endangered: boolean;
  habitat: string;
}

const leopard: Animal = {
  name: 'Snow Leopard',
  endangered: false,
  habitat: 'tundra',
};

💻 playground


interface Animal {
  commonName: string;
  genus: string;
  species: string;
  status: ConservationStatus;
  climates: KoppenClimate[];
}
type ConservationStatus = 'EX' | 'EW' | 'CR' | 'EN' | 'VU' | 'NT' | 'LC';
type KoppenClimate = |
  'Af' | 'Am' | 'As' | 'Aw' |
  'BSh' | 'BSk' | 'BWh' | 'BWk' |
  'Cfa' | 'Cfb' | 'Cfc' | 'Csa' | 'Csb' | 'Csc' | 'Cwa' | 'Cwb' | 'Cwc' |
  'Dfa' | 'Dfb' | 'Dfc' | 'Dfd' |
  'Dsa' | 'Dsb' | 'Dsc' | 'Dwa' | 'Dwb' | 'Dwc' | 'Dwd' |
  'EF' | 'ET';
const snowLeopard: Animal = {
  commonName: 'Snow Leopard',
  genus: 'Panthera',
  species: 'Uncia',
  status: 'VU',  // vulnerable
  climates: ['ET', 'EF', 'Dfd'],  // alpine or subalpine
};

💻 playground

Item 42: Avoid Types Based on Anecdotal Data

Things to Remember

  • Avoid writing types by hand based on data that you’ve seen. It’s easy to misunderstand a schema or get nullability wrong.
  • Prefer types sourced from official clients or the community. If these don’t exist, generate TypeScript types from schemas.## Code Samples
function calculateBoundingBox(f: GeoJSONFeature): BoundingBox | null {
  let box: BoundingBox | null = null;

  const helper = (coords: any[]) => {
    // ...
  };

  const {geometry} = f;
  if (geometry) {
    helper(geometry.coordinates);
  }

  return box;
}

💻 playground


interface GeoJSONFeature {
  type: 'Feature';
  geometry: GeoJSONGeometry | null;
  properties: unknown;
}
interface GeoJSONGeometry {
  type: 'Point' | 'LineString' | 'Polygon' | 'MultiPolygon';
  coordinates: number[] | number[][] | number[][][] | number[][][][];
}

💻 playground


import {Feature} from 'geojson';

function calculateBoundingBox(f: Feature): BoundingBox | null {
  let box: BoundingBox | null = null;

  const helper = (coords: any[]) => {
    // ...
  };

  const {geometry} = f;
  if (geometry) {
    helper(geometry.coordinates);
    //              ~~~~~~~~~~~
    //   Property 'coordinates' does not exist on type 'Geometry'
    //     Property 'coordinates' does not exist on type 'GeometryCollection'
  }

  return box;
}

💻 playground


const {geometry} = f;
if (geometry) {
  if (geometry.type === 'GeometryCollection') {
    throw new Error('GeometryCollections are not supported.');
  }
  helper(geometry.coordinates);  // OK
}

💻 playground


const geometryHelper = (g: Geometry) => {
  if (g.type === 'GeometryCollection') {
    g.geometries.forEach(geometryHelper);
  } else {
    helper(g.coordinates);  // OK
  }
}

const {geometry} = f;
if (geometry) {
  geometryHelper(geometry);
}

💻 playground

Item 43: Use the Narrowest Possible Scope for any Types

Things to Remember

  • Make your uses of any as narrowly scoped as possible to avoid undesired loss of type safety elsewhere in your code.
  • Never return an any type from a function. This will silently lead to the loss of type safety for code that calls the function.
  • Use as any on individual properties of a larger object instead of the whole object.

Code Samples

declare function getPizza(): Pizza;
function eatSalad(salad: Salad) { /* ... */ }

function eatDinner() {
  const pizza = getPizza();
  eatSalad(pizza);
  //       ~~~~~
  // Argument of type 'Pizza' is not assignable to parameter of type 'Salad'
  pizza.slice();
}

💻 playground


function eatDinner1() {
  const pizza: any = getPizza();  // Don't do this
  eatSalad(pizza);  // ok
  pizza.slice();  // This call is unchecked!
}

function eatDinner2() {
  const pizza = getPizza();
  eatSalad(pizza as any);  // This is preferable
  pizza.slice();  // this is safe
}

💻 playground


function eatDinner1() {
  const pizza: any = getPizza();
  eatSalad(pizza);
  pizza.slice();
  return pizza;  // unsafe pizza!
}

function spiceItUp() {
  const pizza = eatDinner1();
  //    ^? const pizza: any
  pizza.addRedPepperFlakes();  // This call is also unchecked!
}

💻 playground


function eatDinner1() {
  const pizza = getPizza();
  // @ts-ignore
  eatSalad(pizza);
  pizza.slice();
}

function eatDinner2() {
  const pizza = getPizza();
  // @ts-expect-error
  eatSalad(pizza);
  pizza.slice();
}

💻 playground


const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
 // ~~~ Property ... missing in type 'Bar' but required in type 'Foo'
  }
};

💻 playground


const config: Config = {
  a: 1,
  b: 2,
  c: {
    key: value
  }
} as any;  // Don't do this!

💻 playground


const config: Config = {
  a: 1,
  b: 2,  // These properties are still checked
  c: {
    key: value as any
  }
};

💻 playground

Item 44: Prefer More Precise Variants of any to Plain any

Things to Remember

  • When you use any, think about whether any JavaScript value is truly permissible.
  • Prefer more precise forms of any such as any[] or {[id: string]: any} or () => any if they more accurately model your data.

// TODO: I don’t love these examples since they could all be replaced with unknown[].

Code Samples

function getLengthBad(array: any) {  // Don't do this!
  return array.length;
}

function getLength(array: any[]) {  // This is better
  return array.length;
}

💻 playground


getLengthBad(/123/);  // No error, returns undefined
getLength(/123/);
//        ~~~~~
// Argument of type 'RegExp' is not assignable to parameter of type 'any[]'.

getLengthBad(null);  // No error, throws at runtime
getLength(null);
//        ~~~~
// Argument of type 'null' is not assignable to parameter of type 'any[]'.

💻 playground


function hasAKeyThatEndsWithZ(o: Record<string, any>) {
  for (const key in o) {
    if (key.endsWith('z')) {
      console.log(key, o[key]);
      return true;
    }
  }
  return false;
}

💻 playground


function hasAKeyThatEndsWithZ(o: object) {
  for (const key in o) {
    if (key.endsWith('z')) {
      console.log(key, o[key]);
      //               ~~~~~~ Element implicitly has an 'any' type
      //                      because type '{}' has no index signature
      return true;
    }
  }
  return false;
}

💻 playground


type Fn0 = () => any;  // any function callable with no params
type Fn1 = (arg: any) => any;  // With one param
type FnN = (...args: any[]) => any;  // With any number of params
                                     // same as "Function" type

💻 playground


const numArgsBad = (...args: any) => args.length;
//    ^? const numArgsBad: (...args: any) => any
const numArgsBetter = (...args: any[]) => args.length;
//    ^? const numArgsBetter: (...args: any[]) => number

💻 playground

Item 45: Hide Unsafe Type Assertions in Well-Typed Functions

Things to Remember

  • Sometimes unsafe type assertions and any types are necessary or expedient. When you need to use one, hide it inside a function with a correct signature.
  • Don’t compromise a function’s type signature to fix type errors in the implementation.
  • Make sure you explain why your type assertions are valid, and unit test your code thoroughly.

Code Samples

interface MountainPeak {
  name: string;
  continent: string;
  elevationMeters: number;
  firstAscentYear: number;
}

async function checkedFetchJSON(url: string): Promise<unknown> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`Unable to fetch! ${response.statusText}`);
  }
  return response.json();
}

export async function fetchPeak(peakId: string): Promise<MountainPeak> {
  return checkedFetchJSON(`/api/mountain-peaks/${peakId}`);
// ~~~~~ Type 'unknown' is not assignable to type 'MountainPeak'.
}

💻 playground


export async function fetchPeak(peakId: string): Promise<unknown> {
  return checkedFetchJSON(`/api/mountain-peaks/${peakId}`);  // ok
}

💻 playground


const sevenPeaks = [
  'aconcagua', 'denali', 'elbrus', 'everest', 'kilimanjaro', 'vinson', 'wilhelm'
];
async function getPeaksByHeight(): Promise<MountainPeak[]> {
  const peaks = await Promise.all(sevenPeaks.map(fetchPeak));
  return peaks.toSorted(
  // ~~~ Type 'unknown' is not assignable to type 'MountainPeak'.
    (a, b) => b.elevationMeters - a.elevationMeters
    //        ~                   ~ 'b' and 'a' are of type 'unknown'
  );
}

💻 playground


async function getPeaksByDate(): Promise<MountainPeak[]> {
  const peaks = await Promise.all(sevenPeaks.map(fetchPeak)) as MountainPeak[];
  return peaks.toSorted((a, b) => b.firstAscentYear - a.firstAscentYear);
}

💻 playground


export async function fetchPeak(peakId: string): Promise<MountainPeak> {
  return checkedFetchJSON(
    `/api/mountain-peaks/${peakId}`,
  ) as Promise<MountainPeak>;
}

💻 playground


async function getPeaksByContinent(): Promise<MountainPeak[]> {
  const peaks = await Promise.all(sevenPeaks.map(fetchPeak));  // no assertion!
  return peaks.toSorted((a, b) => a.continent.localeCompare(b.continent));
}

💻 playground


export async function fetchPeak(peakId: string): Promise<MountainPeak> {
  const maybePeak = checkedFetchJSON(`/api/mountain-peaks/${peakId}`);
  if (
    !maybePeak ||
    typeof maybePeak !== 'object' ||
    !('firstAscentYear' in maybePeak)
  ) {
    throw new Error(`Invalid mountain peak: ${JSON.stringify(maybePeak)}`);
  }
  return checkedFetchJSON(
    `/api/mountain-peaks/${peakId}`,
  ) as Promise<MountainPeak>;
}

💻 playground


export async function fetchPeak(peakId: string): Promise<MountainPeak>;
export async function fetchPeak(peakId: string): Promise<unknown> {
  return checkedFetchJSON(`/api/mountain-peaks/${peakId}`);  // OK
}

const denali = fetchPeak('denali');
//    ^? const denali: Promise<MountainPeak>

💻 playground


function shallowObjectEqual(a: object, b: object): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {
      //                      ~~~~ Element implicitly has an 'any' type
      //                           because type '{}' has no index signature
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

💻 playground


function shallowObjectEqualBad(a: object, b: any): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== b[k]) {  // ok
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

💻 playground


shallowObjectEqual({x: 1}, null)
//                         ~~~~ Type 'null' is not assignable to type 'object'.
shallowObjectEqualBad({x: 1}, null);  // ok, throws at runtime

💻 playground


function shallowObjectEqualGood(a: object, b: object): boolean {
  for (const [k, aVal] of Object.entries(a)) {
    if (!(k in b) || aVal !== (b as any)[k]) {
      // `(b as any)[k]` is OK because we've just checked `k in b`
      return false;
    }
  }
  return Object.keys(a).length === Object.keys(b).length;
}

💻 playground

Item 46: Use unknown Instead of any for Values with an Unknown Type

Things to Remember

  • The unknown type is a type-safe alternative to any. Use it when you know you have a value but do not know or do not care what its type is.
  • Use unknown to force your users to use a type assertion or other form of narrowing.
  • Avoid return-only type parameters, which can create a false sense of security.
  • Understand the difference between {}, object, and unknown.

////## Code Samples

function parseYAML(yaml: string): any {
  // ...
}

💻 playground


interface Book {
  name: string;
  author: string;
}
const book: Book = parseYAML(`
  name: Wuthering Heights
  author: Emily Brontë
`);

💻 playground


const book = parseYAML(`
  name: Jane Eyre
  author: Charlotte Brontë
`);
console.log(book.title);  // No error, logs "undefined" at runtime
book('read');  // No error, throws "book is not a function" at runtime

💻 playground


function safeParseYAML(yaml: string): unknown {
  return parseYAML(yaml);
}
const book = safeParseYAML(`
  name: The Tenant of Wildfell Hall
  author: Anne Brontë
`);
console.log(book.title);
//          ~~~~ 'book' is of type 'unknown'
book("read");
// Error: 'book' is of type 'unknown'

💻 playground


const book = safeParseYAML(`
  name: Villette
  author: Charlotte Brontë
`) as Book;
console.log(book.title);
//               ~~~~~ Property 'title' does not exist on type 'Book'
book('read');
// Error: This expression is not callable

💻 playground


interface Feature {
  id?: string | number;
  geometry: Geometry;
  properties: unknown;
}

💻 playground


function isSmallArray(arr: readonly unknown[]): boolean {
  return arr.length < 10;
}

💻 playground


function processValue(value: unknown) {
  if (value instanceof Date) {
    value
    // ^? (parameter) value: Date
  }
}

💻 playground


function isBook(value: unknown): value is Book {
  return (
      typeof(value) === 'object' && value !== null &&
      'name' in value && 'author' in value
  );
}
function processValue(value: unknown) {
  if (isBook(value)) {
    value;
    // ^? (parameter) value: Book
  }
}

💻 playground


function safeParseYAML<T>(yaml: string): T {
  return parseYAML(yaml);
}

💻 playground


declare const foo: Foo;
let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;

💻 playground

Item 47: Prefer Type-Safe Approaches to Monkey Patching

Things to Remember

  • Prefer structured code to storing data in globals or on the DOM.
  • If you must store data on built-in types, use one of the type-safe approaches (augmentation or asserting a custom interface).
  • Understand the scoping issues of augmentations. Include undefined if that’s a possibility at runtime.

Code Samples

document.monkey = 'Tamarin';
//       ~~~~~~ Property 'monkey' does not exist on type 'Document'

💻 playground


(document as any).monkey = 'Tamarin';  // OK

💻 playground


(document as any).monky = 'Tamarin';  // Also OK, misspelled
(document as any).monkey = /Tamarin/;  // Also OK, wrong type

💻 playground


interface User {
  name: string;
}

document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  window.user = user;
  //     ~~~~ Property 'user' does not exist
  //          on type 'Window & typeof globalThis'.
});

// ... elsewhere ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);
  //                    ~~~~ Property 'user' does not exist on type Window...
}

💻 playground


declare global {
  interface Window {
    /** The currently logged-in user */
    user: User;
  }
}

💻 playground


document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  window.user = user;  // OK
});

// ... elsewhere ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);  // OK
}

💻 playground


declare global {
  interface Window {
    /** The currently logged-in user */
    user: User | undefined;
  }
}

// ...
export function greetUser() {
  alert(`Hello ${window.user.name}!`);
  //             ~~~~~~~~~~~ 'window.user' is possibly 'undefined'.
}

💻 playground


type MyWindow = (typeof window) & {
  /** The currently logged-in user */
  user: User | undefined;
}

document.addEventListener("DOMContentLoaded", async () => {
  const response = await fetch('/api/users/current-user');
  const user = (await response.json()) as User;
  (window as MyWindow).user = user;  // OK
});

// ...
export function greetUser() {
  alert(`Hello ${(window as MyWindow).user.name}!`);
  //             ~~~~~~~~~~~~~~~~~~~~~~~~~ Object is possibly 'undefined'.
}

💻 playground

Quotes

References


Profile picture

Written by Tony Vo father, husband, son and software developer Twitter