Skip to content

Instantly share code, notes, and snippets.

@hookdump
Created September 5, 2024 05:01
Show Gist options
  • Save hookdump/77cc074ee315c0d3def2ec4ce001722e to your computer and use it in GitHub Desktop.
Save hookdump/77cc074ee315c0d3def2ec4ce001722e to your computer and use it in GitHub Desktop.
Effective Typescript Summary: 83 tips as code examples

1. Getting to Know TypeScript

Item 1: Understand the Relationship Between TypeScript and JavaScript

function add(a: number, b: number): number {
  return a + b;
}
// The above TypeScript function is translated directly to JavaScript.

Item 2: Know Which TypeScript Options You’re Using

// In tsconfig.json, set "strict" to enforce strict type checking
{
  "compilerOptions": {
    "strict": true
  }
}

Item 3: Understand That Code Generation Is Independent of Types

// TypeScript will erase types during JavaScript compilation
function greet(name: string): string {
  return `Hello, ${name}`;
}
// Transpiles to: function greet(name) { return "Hello, " + name; }

Item 4: Get Comfortable with Structural Typing

interface Point { x: number; y: number; }
const p = { x: 10, y: 20 };
function logPoint(pt: Point) {
  console.log(pt.x, pt.y);
}
logPoint(p); // Works because of structural typing

Item 5: Limit Use of the any Type

function logValue(val: any) {
  console.log(val);
}
// Avoid `any`. Use specific types like:
function logValue(val: string | number) {
  console.log(val);
}

2. TypeScript's Type System

Item 6: Use Your Editor to Interrogate and Explore the Type System

// Hovering over variables in TypeScript-enabled editors like VSCode shows inferred types
const message = "Hello"; // Hover shows: const message: string

Item 7: Think of Types as Sets of Values

type Direction = "left" | "right";
function move(dir: Direction) {
  console.log(dir);
}
move("left"); // Valid
move("up");   // Error: "up" is not assignable to Direction

Item 8: Know How to Tell Whether a Symbol Is in the Type Space or Value Space

interface Dog { bark(): void; }
const Dog = { species: 'Canine' };
// 'Dog' in type space is different from 'Dog' in value space

Item 9: Prefer Type Annotations to Type Assertions

let myVar = "string" as any; // Type assertion, try to avoid
let myVar: string = "string"; // Prefer type annotation

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

let s: string = new String("hello"); // Avoid
let s: string = "hello";             // Prefer primitive types

Item 11: Distinguish Excess Property Checking from Type Checking

interface Animal { name: string; }
const cat: Animal = { name: 'Lince', age: 5 }; // Error: 'age' is excess property
// Avoid by using a variable first
const lince = { name: 'Lince', age: 5 };
const cat: Animal = lince; // OK

Item 12: Apply Types to Entire Function Expressions When Possible

type Greet = (name: string) => string;
const greet: Greet = (name) => `Hello, ${name}`; // Applies type to the whole function

Item 13: Know the Differences Between type and interface

interface Dog { breed: string; }
type Cat = { breed: string; }
// Use `interface` for extending
interface Husky extends Dog { color: string; }

Item 14: Use readonly to Avoid Errors Associated with Mutation

interface Point { readonly x: number; readonly y: number; }
const p: Point = { x: 10, y: 20 };
p.x = 30; // Error: Cannot assign to 'x' because it is readonly

Item 15: Use Type Operations and Generic Types to Avoid Repeating Yourself

type Person = { firstName: string; lastName: string };
type WithBirthDate = Person & { birthDate: Date };
// Use generics and intersections to avoid duplication in types.

Item 16: Prefer More Precise Alternatives to Index Signatures

// Avoid general index signatures
interface Inventory {
  [key: string]: number; // imprecise
}
// Prefer specific fields or use mapped types
interface ExactInventory {
  apples: number;
  bananas: number;
}

Item 17: Avoid Numeric Index Signatures

interface Team {
  [index: number]: string; // Avoid numeric index signatures
}
const team: Team = { 0: "Alice", 1: "Bob" }; // Replace with an array if possible
const players: string[] = ["Alice", "Bob"]; // Prefer arrays

3. Type Inference and Control Flow Analysis

Item 18: Avoid Cluttering Your Code with Inferable Types

let name = "John"; // TypeScript infers `name` is of type `string`, no need for annotation.

Item 19: Use Different Variables for Different Types

let id: string | number; // Bad practice, split types into separate variables.
let stringId: string = "123";
let numericId: number = 123;

Item 20: Understand How a Variable Gets Its Type

let x = 10; // x is inferred as number
let y = x + 5; // TypeScript infers based on context

Item 21: Create Objects All at Once

const point = { x: 10, y: 20 }; // Declare object all at once instead of incrementally.

Item 22: Understand Type Narrowing

function printValue(val: string | number) {
  if (typeof val === "string") {
    console.log(val.toUpperCase()); // Type narrowed to string
  } else {
    console.log(val + 1); // Type narrowed to number
  }
}

Item 23: Be Consistent in Your Use of Aliases

type ID = string | number;
let id: ID = "123"; // Consistent use of the alias across the codebase

Item 24: Understand How Context Is Used in Type Inference

const add = (a: number, b: number) => a + b; // TypeScript infers the return type based on context

Item 25: Understand Evolving Types

let person = { name: "John" }; // Initially inferred as { name: string }
person.age = 30; // Error: 'age' does not exist on the inferred type

Item 26: Use Functional Constructs and Libraries to Help Types Flow

// Use map to let types flow through
const numbers = [1, 2, 3].map(n => n * 2); // Inferred as number[]

Item 27: Use async Functions Instead of Callbacks to Improve Type Flow

async function fetchData() {
  const response = await fetch("url");
  const data = await response.json();
}

Item 28: Use Classes and Currying to Create New Inference Sites

class Calculator {
  constructor(private value: number) {}
  add(n: number) { return new Calculator(this.value + n); }
}
const calc = new Calculator(5).add(10); // Inference works through class methods

4. Type Design

Item 29: Prefer Types That Always Represent Valid States

type SuccessResponse = { status: "success"; data: string };
type ErrorResponse = { status: "error"; error: string };
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
  if (response.status === "success") {
    console.log(response.data);
  }
}

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

function double(input: number | string): number {
  return typeof input === "number" ? input * 2 : parseFloat(input) * 2;
}

Item 31: Don’t Repeat Type Information in Documentation

/**
 * Concatenates two strings
 * @param a - The first string
 * @param b - The second string
 */
function concat(a: string, b: string): string {
  return a + b;
}

Item 32: Avoid Including null or undefined in Type Aliases

function getLength(input: string | null): number {
  if (input === null) return 0;
  return input.length;
}

Item 33: Push Null Values to the Perimeter of Your Types

function processInput(input: string | null): string {
  if (input === null) return "default";
  return input.trim();
}

Item 34: Prefer Unions of Interfaces to Interfaces with Unions

interface Cat { type: "cat"; meow(): void; }
interface Dog { type: "dog"; bark(): void; }
type Animal = Cat | Dog;

Item 35: Prefer More Precise Alternatives to String Types

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
function sendRequest(method: HTTPMethod, url: string) {
  console.log(`Sending ${method} request to ${url}`);
}

Item 36: Use a Distinct Type for Special Values

type InvalidDate = { type: "InvalidDate" };
type ValidDate = { type: "ValidDate"; date: Date };
type DateValue = InvalidDate | ValidDate;

Item 37: Limit the Use of Optional Properties

interface UserProfile {
  name: string;
  age?: number; // Use optional properties sparingly
}

Item 38: Avoid Repeated Parameters of the Same Type

function drawRect({ x, y }: { x: number; y: number }, { width, height }: { width: number; height: number }) {
  console.log(`Drawing rectangle at (${x}, ${y}) with dimensions ${width}x${height}`);
}

Item 39: Prefer Unifying Types to Modeling Differences

type User = { name: string; email?: string }; // Unified type for users with and without email

Item 40: Prefer Imprecise Types to Inaccurate Types

type User = { id: string; name: string }; // Imprecise but flexible
// Avoid overly accurate types that limit future flexibility.

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

type OrderStatus = "pending" | "completed"; // Use domain-specific language for clarity

Item 42: Avoid Types Based on Anecdotal Data

// Avoid creating types just based on limited or specific data
type LimitedType = { property: number }; // Avoid basing types on small datasets

5. Unsoundness and the any Type

Item 43: Use the Narrowest Possible Scope for any Types

function process(value: any) {
  const str: string = value as string; // Scope `any` to just where it's needed
}

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

function process(values: any[]) {
  values.forEach(value => console.log(value)); // More precise than plain `any`
}

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

function parseJSON(json: string): any {
  return JSON.parse(json);
}

function safelyParseJSON<T>(json: string): T {
  return parseJSON(json) as T; // Unsafe assertion hidden in a well-typed function
}

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

  function handleValue(value: unknown) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  }
}

Item 47: Prefer Type-Safe Approaches to Monkey Patching

  interface Person {
  name: string;
}

const person: Person = { name: "Alice" };
(person as any).age = 30; // Avoid monkey patching like this

Item 48: Avoid Soundness Traps

// Avoid soundness traps like unchecked object lookups
type Data = { [key: string]: string };
const data: Data = { name: "John" };
console.log(data["nonExistentKey"]); // Might lead to undefined at runtime

Item 49: Track Your Type Coverage to Prevent Regressions in Type Safety

// Use tools like type-coverage to track your project's type safety
// Command example:
$ npx type-coverage

6. Generics and Type-Level Programming

Item 50: Think of Generics as Functions Between Types

function identity<T>(arg: T): T {
  return arg;
}
// Generic functions work like functions for types

Item 51: Avoid Unnecessary Type Parameters

// Avoid adding extra type parameters that don't add value
function logLength<T>(arg: T[]): void {
  console.log(arg.length); // T[] already indicates an array, no need for extra type
}

Item 52: Prefer Conditional Types to Overload Signatures

type IsString<T> = T extends string ? true : false;
// Conditional types are more flexible than overloading

Item 53: Know How to Control the Distribution of Unions over Conditional Types

type TypeName<T> = T extends string ? "string" : "other";
type Distributed = TypeName<string | number>; // Distributes to "string" | "other"

Item 54: Use Template Literal Types to Model DSLs and Relationships Between Strings

type Path = `${string}/${string}`;
const path: Path = "user/profile"; // Template literal type for modeling string patterns

Item 55: Write Tests for Your Types

// You can use libraries like tsd to write tests for your types
$ npx tsd

Item 56: Pay Attention to How Types Display

type User = { name: string; age: number };
type Admin = User & { role: string };

This combination will display in editors as { name: string; age: number; role: string }, allowing you to check type composition visually.

Item 57: Prefer Tail-Recursive Generic Types

type TupleLength<T extends any[]> = T extends { length: infer L } ? L : never;
// Use tail-recursion to avoid type expansion limits

Item 58: Consider Codegen as an Alternative to Complex Types

// Use code generation tools for complex types instead of managing them manually

7. TypeScript Recipes

Item 59: Use Never Types to Perform Exhaustiveness Checking

type Shape = { type: 'circle' | 'square'; };
function handleShape(shape: Shape) {
  if (shape.type === 'circle') {
    console.log('Circle');
  } else if (shape.type === 'square') {
    console.log('Square');
  } else {
    const _exhaustive: never = shape; // Error if not exhaustive
  }
}

Item 60: Know How to Iterate Over Objects

const myObject = { a: 1, b: 2 };
Object.entries(myObject).forEach(([key, value]) => {
  console.log(key, value);
});

Item 61: Use Record Types to Keep Values in Sync

type RecordType = Record<string, number>;
const data: RecordType = { apples: 10, bananas: 5 };

Item 62: Use Rest Parameters and Tuple Types to Model Variadic Functions

function sum(...args: [number, number, number]): number {
  return args.reduce((a, b) => a + b, 0);
}

Item 63: Use Optional Never Properties to Model Exclusive Or

type EitherOr = { a: string; b?: never } | { a?: never; b: string };

Item 64: Consider Brands for Nominal Typing

type UserID = string & { readonly brand: unique symbol };
function createUser(id: UserID) {
  // UserID is nominally typed and cannot be confused with other strings
}

8. Type Declarations and @types

Item 65: Put TypeScript and @types in devDependencies

{
  "devDependencies": {
    "typescript": "^4.0.0",
    "@types/node": "^14.0.0"
  }
}

Item 66: Understand the Three Versions Involved in Type Declarations

  • TypeScript version
  • Library version
  • @types version

Item 67: Export All Types That Appear in Public APIs

// Export all types that are part of a public API
export interface User {
  id: string;
  name: string;
}

Item 68: Use TSDoc for API Comments

/**
 * Adds two numbers.
 * @param a - The first number.
 * @param b - The second number.
 * @returns The sum of a and b.
 */
function add(a: number, b: number): number {
  return a + b;
}

Item 69: Provide a Type for this in Callbacks if It’s Part of Their API

class MyClass {
  myMethod() {
    [1, 2, 3].forEach(function (this: MyClass, num) {
      console.log(this); // Now `this` is typed as MyClass
    }, this);
  }
}

Item 70: Mirror Types to Sever Dependencies

interface ExternalData {
  value: number;
}
interface LocalData {
  value: number;
}
// Mirror types avoid unnecessary coupling to external libraries.

Item 71: Use Module Augmentation to Improve Types

declare module 'my-module' {
  interface MyType {
    newProperty: string;
  }
}
// Augments existing types in a module.

9. Writing and Running Your Code

Item 72: Prefer ECMAScript Features to TypeScript Features

const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // Use native ECMAScript methods when possible.

Item 73: Use Source Maps to Debug TypeScript

{
  "compilerOptions": {
    "sourceMap": true
  }
}
// Ensure source maps are enabled for debugging TypeScript in its original form.

Item 74: Know How to Reconstruct Types at Runtime

function isString(value: any): value is string {
  return typeof value === "string";
}
// Use type predicates to reconstruct types at runtime.

Item 75: Understand the DOM Hierarchy

const element = document.getElementById("myElement"); // HTMLElement

Item 76: Create an Accurate Model of Your Environment

declare const process: {
  env: {
    NODE_ENV: "development" | "production";
  };
};
// Accurately model your environment to prevent errors.

Item 77: Understand the Relationship Between Type Checking and Unit Testing

// TypeScript handles types, but you still need unit tests for behavior.
function add(a: number, b: number): number {
  return a + b;
}

Item 78: Pay Attention to Compiler Performance

{
  "compilerOptions": {
    "incremental": true
  }
}
// Use incremental builds to improve compiler performance.

10. Modernization and Migration

Item 79: Write Modern JavaScript

// Use modern JavaScript features, like async/await, arrow functions, and destructuring
const fetchData = async (url: string) => {
  const response = await fetch(url);
  const data = await response.json();
  console.log(data);
};

Item 80: Use @ts-check and JSDoc to Experiment with TypeScript

// JavaScript file with @ts-check enabled for type checking
// @ts-check
/**
 * Adds two numbers
 * @param {number} a 
 * @param {number} b 
 * @returns {number}
 */
function add(a, b) {
  return a + b;
}

Item 81: Use allowJs to Mix TypeScript and JavaScript

// tsconfig.json configuration to allow JS files in a TypeScript project
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true
  },
  "include": ["src/**/*.ts", "src/**/*.js"]
}

Item 82: Convert Module by Module Up Your Dependency Graph

// Start migrating TypeScript in small modules and move upwards
// Example: Start with a utility module
export function capitalize(str: string): string {
  return str[0].toUpperCase() + str.slice(1);
}

Item 83: Don’t Consider Migration Complete Until You Enable noImplicitAny

// Enabling noImplicitAny in tsconfig.json
{
  "compilerOptions": {
    "noImplicitAny": true
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment