TypeScript vs JavaScript Cheatsheet: Types, Tooling, and Migration
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
| Aspect | JavaScript | TypeScript |
|---|---|---|
| Type checking | Runtime | Compile time |
| Compile step | Optional | Standard |
| Generics | No | Yes |
| Interfaces | No | Yes |
| Refactoring safety | Limited | Strong |
| Editor support | JSDoc / inference | Rich, type-aware |
| Learning curve | Low | Moderate |
| 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 implicitanystrictNullChecks—nullandundefinedare not everythingstrictFunctionTypes— function parameter bivariance is offstrictBindCallApply— typedbind/call/apply
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
| Type | Example | Notes |
|---|---|---|
| number | let x: number = 5; | integers and floats |
| string | let s: string = "hi"; | |
| boolean | let b: boolean = true; | |
| array | let a: number[] = [1, 2]; | or Array<number> |
| tuple | let t: [string, number] = ["a", 1]; | fixed length |
| enum | enum Color { R, G } | compiled to object |
| any | let a: any; | avoid |
| unknown | let v: unknown; | safe any |
| undefined | let u: undefined; | under strictNullChecks |
| void | function f(): void {} | no return value |
| never | function fail(): never { throw new Error(); } | never returns |
| object | let 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 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:
| Tool | Purpose |
|---|---|
| tsc | Official compiler, type-checking + emit |
| vite | Fast dev server; esbuild/swc transpile TS |
| webpack | Bundler with ts-loader |
| esbuild | Fast transpiler, no type checking |
| swc | Rust-based, very fast transpile |
| Next.js | Framework with zero-config TS |
| eslint / prettier | Lint + 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
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
| Step | Action |
|---|---|
| 1 | Install typescript and a TS-aware lint config |
| 2 | Add a permissive tsconfig.json ("allowJs": true) |
| 3 | Rename .js to .ts/.tsx module by module |
| 4 | Add JSDoc or inline types where inference is weak |
| 5 | Enable "strict": true per file or globally |
| 6 | Remove "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
| Need | Syntax |
|---|---|
| Type annotation | let x: number = 1 |
| Union type | type Id = string | number |
| Optional property | { name?: string } |
| Array type | let a: number[] = [] |
| Tuple | let pair: [string, number] |
| Generic function | function id<T>(x: T): T |
| Generic constraint | <T extends string> |
| Interface | interface User { name: string } |
| Enum | enum Role { Admin, User } |
| Type literal | "admin" | "user" as const |
| Type query | type T = typeof value |
| Indexed access | type Name = User["name"] |
| Utility type | Partial<T>, Pick<T, K> |
| Type guard | if (typeof x === "string") |
| Type assertion | value as string |
| Strict null check | string | null | undefined |
| Readonly | readonly |
| Namespace import | import * as path from "path" |
| Default export | export default class {} |
| React component | function App(): JSX.Element |
TypeScript vs JavaScript Decision Matrix
| Factor | JavaScript | TypeScript |
|---|---|---|
| Setup | None | tsc + tsconfig |
| Compilation | None (bundler only) | Compile / transpile |
| Errors | Runtime | Compile time |
| Tooling | Inference | Type-aware IDE |
| Refactoring | Fragile | Safe |
| Bundle size | Smaller | Same (types erased) |
| Team size | Small / solo | Large / long-lived |
| Documentation | JSDoc needed | Types as docs |
| Learning curve | Low | Moderate |
| Migration | n/a | Incremental |
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.