TypeScript vs JavaScript Cheatsheet: Types, Tooling, and Migration

📅 December 4, 2025
⏱️ 18 min read

JavaScript is the language of the web, and TypeScript adds a static type layer on top of it. This article compares TypeScript and JavaScript, explains why types matter at scale, covers the type system, tooling, and migration, then distils the essentials into a practical cheatsheet. Developers moving between vanilla JavaScript frontends and typed frameworks like React will find a direct reference here.

For the surrounding build and deployment pipeline, see the DevOps Ready Reckoner.

JavaScript vs TypeScript

JavaScript (JS) is a dynamically-typed, interpreted scripting language executed by browsers and runtimes like Node.js. TypeScript (TS) is a superset of JavaScript: valid JavaScript is valid TypeScript, but TypeScript adds an optional static type layer that is erased before execution.

JavaScript
  -> runs anywhere (browser, Node, Deno)
  -> types checked at runtime
  -> no compile step (by default)
  -> errors appear late (testing / production)

TypeScript
  -> compiles to JavaScript
  -> types checked at compile time (optional)
  -> tsc / transpilers (esbuild, swc, webpack, vite)
  -> errors caught before runtime
AspectJavaScriptTypeScript
Type checkingRuntimeCompile time
Compile stepOptionalStandard
GenericsNoYes
InterfacesNoYes
Refactoring safetyLimitedStrong
Editor supportJSDoc / inferenceRich, type-aware
Learning curveLowModerate
File extension.js.ts / .tsx

The Type System

The TypeScript compiler performs three kinds of checks relevant to security and reliability in production:

1. Syntactic analysis
2. Type checking (types, scopes, generics)
3. Emit (erases types, outputs JavaScript)

Type annotations are optional. A project can adopt types incrementally — one file at a time — which is what makes migration feasible without rewriting an existing codebase.

Strict mode matters

With "strict": true, TypeScript enforces:

  • noImplicitAny — no implicit any
  • strictNullChecks — null and undefined are not everything
  • strictFunctionTypes — function parameter bivariance is off
  • strictBindCallApply — typed bind/call/apply
Why types reduce risk: in JavaScript, a typo like user.nam is undefined until it throws at runtime and only if that code path runs. In TypeScript with strict mode, that is a compile error before the code ever ships.

TypeScript Types at a Glance

TypeExampleNotes
numberlet x: number = 5;integers and floats
stringlet s: string = "hi";
booleanlet b: boolean = true;
arraylet a: number[] = [1, 2];or Array<number>
tuplelet t: [string, number] = ["a", 1];fixed length
enumenum Color { R, G }compiled to object
anylet a: any;avoid
unknownlet v: unknown;safe any
undefinedlet u: undefined;under strictNullChecks
voidfunction f(): void {}no return value
neverfunction fail(): never { throw new Error(); }never returns
objectlet o: object;non-primitive

Interfaces and Type Aliases

// interface (object shape; extensible via declaration merging)
interface User {
  id: number;
  name: string;
  email?: string;          // optional
  readonly createdAt: Date;
}

// type alias (works for unions, primitives, tuples)
type Id = string | number;
type Point = { x: number; y: number };
type Pair = [number, number];

// index signature
interface Dictionary {
  [key: string]: string;
}
interface vs type: use interface for object shapes that may need to extend or benefit from declaration merging. Use type for unions, tuples, mapped types, or conditional types. Both are erased at runtime.

Generics

Generics let types depend on other types, enabling reusable, type-safe data structures.

function identity<T>(arg: T): T {
  return arg;
}

const output = identity<string>("hi");

// generic constraint
function length<T extends { length: number }>(arg: T): number {
  return arg.length;
}

// generic in a class
class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

Functions and Overloads

// typed return
function add(a: number, b: number): number {
  return a + b;
}

// optional parameter
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hi"}, ${name}`;
}

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

// function overloads
function reverse(x: string): string;
function reverse(x: any[]): any[];
function reverse(x: string | any[]): string | any[] {
  if (typeof x === "string") return x.split("").reverse().join("");
  return [...x].reverse();
}

Classes

class Person {
  readonly id: number;
  name: string;
  private ssn?: string;       // not accessible outside the class
  protected role: string;    // accessible to subclasses
  static population: number = 0;

  constructor(name: string, role: "admin" | "user" = "user") {
    this.name = name;
    this.role = role;
    Person.population++;
  }

  get display(): string {
    return `${this.name} (${this.role})`;
  }
}

const p = new Person("Aria");
console.log(Person.population);

Modules and Imports

TypeScript uses standard ES modules. .ts is for logic; .tsx is for files containing JSX (React).

// utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}
export const PI = 3.14159;

// app.ts
import { add, PI } from "./utils/math";
console.log(add(2, 3), PI);

Async Patterns

// async/await with typed response
interface Post {
  id: number;
  title: string;
}

async function fetchPosts(): Promise<Post[]> {
  const res = await fetch("/api/posts");
  return res.json();
}

// error handling
async function load(): Promise<Post[] | null> {
  try {
    return await fetchPosts();
  } catch (e) {
    console.error(e);
    return null;
  }
}

Utility Types

Partial<T>      // make all properties optional
Required<T>     // make all properties required
Readonly<T>    // make all properties readonly
Pick<T, K>       // pick subset of properties
Omit<T, K>       // omit subset of properties
Record<K, T>     // object with keys K and value type T
ReturnType<T>   // type of a function's return value

Utility types in practice

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

// for a PATCH endpoint: only some fields required
type ProductUpdate = Partial<Product>;

// a read-only view for display
type ProductView = Readonly<Pick<Product, "id" | "name" | "price">>;

// a lookup table keyed by product id
type ProductCatalog = Record<number, Product>

Type Narrowing and Guards

function format(input: string | number): string {
  if (typeof input === "number") {
    return input.toFixed(2);
  }
  return input.toUpperCase();
}

// array narrowing
function firstEl(arr: unknown[]): string | undefined {
  if (Array.isArray(arr) && arr.length) {
    const [first] = arr;
    if (typeof first === "string") return first;
  }
    return undefined;
}

keyof, Mapped, and Conditional Types

// keyof: keys of a type
type UserKey = keyof { name: string; age: number };
// => "name" | "age"

// mapped type: transform each property
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// conditional type
type NonNull<T> = T extends null | undefined ? never : T;

Type Assertions and JSDoc in JS

// type assertion (TS)
const el = document.getElementById("btn") as HTMLButtonElement;

// equivalent assertion
const el2 = <HTMLButtonElement>document.getElementById("btn");

// JSDoc provides types in plain JavaScript
/**
 * @param {string} name
 * @param {number} [count=1]
 * @returns {string}
 */
function repeat(name, count = 1) {
  return Array(count).fill(name).join(" ");
}

Tooling and Configuration

TypeScript is compiled (or transpiled) to JavaScript. The canonical compiler is tsc, but most projects use a bundler/transpiler:

ToolPurpose
tscOfficial compiler, type-checking + emit
viteFast dev server; esbuild/swc transpile TS
webpackBundler with ts-loader
esbuildFast transpiler, no type checking
swcRust-based, very fast transpile
Next.jsFramework with zero-config TS
eslint / prettierLint + format

tsconfig.json essentials

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

Common scripts

npm run build      # tsc -> dist (or vite build)
npm run type-check   # tsc --noEmit, used in CI
npm test             # vitest / jest
npm run lint         # eslint
npm run format       # prettier
CI tip: separate type-checking from building. Run tsc --noEmit (or vite build) in CI so type errors fail the pipeline even when the bundler uses esbuild/swc and skips type checking.

Migrating from JavaScript

StepAction
1Install typescript and a TS-aware lint config
2Add a permissive tsconfig.json ("allowJs": true)
3Rename .js to .ts/.tsx module by module
4Add JSDoc or inline types where inference is weak
5Enable "strict": true per file or globally
6Remove "allowJs" when fully converted

JavaScript that works today continues to work tomorrow — TypeScript is a superset, so migration can be incremental. The payoff grows as more files become typed, because refactors become safe and autocomplete becomes precise.

TypeScript Cheatsheet

NeedSyntax
Type annotationlet x: number = 1
Union typetype Id = string | number
Optional property{ name?: string }
Array typelet a: number[] = []
Tuplelet pair: [string, number]
Generic functionfunction id<T>(x: T): T
Generic constraint<T extends string>
Interfaceinterface User { name: string }
Enumenum Role { Admin, User }
Type literal"admin" | "user" as const
Type querytype T = typeof value
Indexed accesstype Name = User["name"]
Utility typePartial<T>, Pick<T, K>
Type guardif (typeof x === "string")
Type assertionvalue as string
Strict null checkstring | null | undefined
Readonlyreadonly
Namespace importimport * as path from "path"
Default exportexport default class {}
React componentfunction App(): JSX.Element

TypeScript vs JavaScript Decision Matrix

Factor JavaScript TypeScript
SetupNonetsc + tsconfig
CompilationNone (bundler only)Compile / transpile
ErrorsRuntimeCompile time
ToolingInferenceType-aware IDE
RefactoringFragileSafe
Bundle sizeSmallerSame (types erased)
Team sizeSmall / soloLarge / long-lived
DocumentationJSDoc neededTypes as docs
Learning curveLowModerate
Migrationn/aIncremental
When TypeScript pays off most: large codebases, long maintenance horizon, multiple teams, public libraries, or any codebase where a typo causes real downtime. For throwaway scripts and very small apps, plain JavaScript remains perfectly reasonable.

Conclusion

JavaScript is the runtime; TypeScript is a compile-time safety net. The right choice depends on codebase size, team maturity, and operational cost tolerance. What matters most is consistency: once a project adopts TypeScript, keep strict on, type-check in CI, and treat type errors like build failures. The discipline of naming and constraining data shapes through types is what turns fragile scripts into reliable, maintainable applications.

TypeScript does not eliminate bugs, it prevents an entire category of them before code runs.

Topics & Tags
TypeScript JavaScript Frontend React Cheat Sheet Architecture DevOps Cloud Engineering