Cheat SheetsJavaScript & TypeScriptTypeScript

TypeScript — Cheat Sheet

JavaScript & TypeScript · 12 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
TypeScript
JavaScript & TypeScript12 topicsQuick revision reference
1

Why TypeScript, and How It Runs

TypeScript is a checker, not a runtime. It erases to JavaScript and disappears — which explains both what it catches and everything it cannot.

  • TypeScript checks then erases — the shipped JavaScript contains no type information
  • Because types are erased you cannot use instanceof on an interface or check a type at runtime
  • strict: true enables strictNullChecks and noImplicitAny, which is where most of the value lives
  • "as" is an assertion you make to the compiler, not a check the compiler performs
  • Data from the network is unverified — use a runtime validator such as Zod at the boundary
Types are erased; runtime knows nothing
// TypeScript
interface User { id: number; name: string }
function greet(user: User): string {
  return `Hi ${user.name}`
}

// Compiled JavaScript — the interface does not exist at runtime
function greet(user) {
  return `Hi ${user.name}`
}

// Which means this is impossible:
if (value instanceof User) { }        // Error: 'User' only refers to a type

// And this compiles, then explodes at runtime:
const data = await res.json() as User   // 'as' is a claim, not a check
data.name.toUpperCase()                 // undefined if the API changed
2

Basic Types, Inference and Annotations

Annotate function boundaries and let inference handle the rest. Over-annotating is a common beginner habit that makes code noisier without making it safer.

  • TypeScript has one number type — no int, long, float or double distinction
  • Annotate function parameters and exported return types; let everything else infer
  • any disables checking entirely; unknown forces you to narrow before use and is the safe alternative
  • const infers a literal type while let widens to the primitive, which matters for unions
  • Callback parameters are contextually typed from the surrounding signature, so they rarely need annotations
any is an escape hatch; unknown is the safe one
let name: string = 'Akshay'
let count: number = 42            // one number type — no int/long/float
let big: bigint = 9007199254740993n
let ok: boolean = true
let nothing: null = null
let missing: undefined = undefined
let key: symbol = Symbol('id')

// Arrays and tuples
let scores: number[] = [90, 85]
let pair: [string, number] = ['arrays', 12]        // fixed length and order
let named: [x: number, y: number] = [1, 2]         // labels, for readability

// any switches off checking; unknown keeps it on
let a: any = getValue()
a.whatever.deeply.nested       // compiles, may explode

let u: unknown = getValue()
u.whatever                     // Error — must narrow first
if (typeof u === 'string') u.toUpperCase()   // now allowed

// never — the type with no values; the return type of a function that throws
function fail(msg: string): never { throw new Error(msg) }
3

Interfaces, Type Aliases and Structural Typing

TypeScript compares shapes, not names. Two unrelated types with the same members are interchangeable — the opposite of how Java works.

  • TypeScript is structurally typed — a value fits a type if it has the right shape, with no declared relationship
  • Fresh object literals get an excess-property check that variables do not, which catches option typos
  • Only type aliases can express unions, tuples and mapped types; only interfaces merge across declarations
  • readonly and optional are compile-time only — Object.freeze is the runtime equivalent
  • Record<Level, number> over an index signature when the keys are a known set, because missing keys are then errors
Same shape means same type
interface Point { x: number; y: number }

class Vector { constructor(public x: number, public y: number) {} }

function length(p: Point) { return Math.hypot(p.x, p.y) }

length(new Vector(3, 4))          // fine — Vector has x and y
length({ x: 3, y: 4 })            // fine — the literal has x and y
// In Java, Vector would have to declare 'implements Point'.

// Extra properties are allowed... except on fresh object literals
const v = { x: 3, y: 4, z: 5 }
length(v)                         // OK — v is a variable
length({ x: 3, y: 4, z: 5 })      // Error — excess property check
// The literal check exists to catch typos in options objects.
4

Unions, Narrowing and Discriminated Unions

Unions model "one of these", and narrowing is how the compiler follows your checks. Discriminated unions are the single most useful pattern in the language.

  • A union value cannot be used until narrowed; typeof, in, instanceof and equality checks all narrow
  • Truthiness checks exclude 0 and empty string too — use != null when those are valid values
  • A custom type guard is a function returning `x is T`, for checks the built-ins cannot express
  • A discriminated union has a shared literal tag, letting the compiler know which fields exist in each branch
  • Assigning the value to `never` in the default branch makes forgetting a new case a compile error
typeof, in, instanceof, and custom guards
function format(value: string | number | null) {
  if (value === null) return '—'          // value: string | number after this
  if (typeof value === 'string') return value.trim()   // string here
  return value.toFixed(2)                 // number — the only thing left
}

// The narrowing tools
typeof x === 'string'
x instanceof Date
'role' in user                        // property presence
Array.isArray(x)
x === null / x !== undefined
x?.length                             // optional chaining narrows too

// Truthiness narrows, and has the classic trap
if (count) { }        // excludes 0 as well as undefined — usually a bug
if (count != null) { }  // excludes null AND undefined, keeps 0

// A custom type guard, for when the built-ins cannot express it
function isProblem(x: unknown): x is Problem {
  return typeof x === 'object' && x !== null && 'slug' in x
}
if (isProblem(data)) data.slug        // narrowed by the return type
5

Generics

Generics carry a type through a function instead of losing it. Familiar from Java, with two differences worth knowing: inference is much stronger, and there is no erasure workaround because there is nothing to work around.

  • A generic preserves the relationship between input and output types instead of collapsing to any
  • TypeScript infers type arguments from the call site, so explicit <T> is rarely needed
  • extends constrains a type parameter, which is what allows you to access members on it
  • K extends keyof T is the pattern for type-safe property access by name
  • A type parameter appearing only once in a signature is doing nothing — use unknown or the concrete type
The type flows from argument to return
// Loses the type
function firstAny(arr: any[]): any { return arr[0] }
const n = firstAny([1, 2, 3])       // any — no help downstream

// Keeps it
function first<T>(arr: T[]): T | undefined { return arr[0] }
const m = first([1, 2, 3])          // number | undefined — inferred, no <number> needed

// Two parameters, related
function mapValues<T, U>(obj: Record<string, T>, fn: (v: T) => U): Record<string, U> {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [k, fn(v)]),
  ) as Record<string, U>
}

// A typed fetch wrapper — the pattern you will actually write
async function api<T>(path: string): Promise<T> {
  const res = await fetch(path)
  if (!res.ok) throw new Error(res.statusText)
  return res.json() as Promise<T>
}
const problems = await api<Problem[]>('/api/problems')
6

Utility Types

Partial, Pick, Omit, Record, ReturnType and friends. Deriving types from other types is what keeps them in sync when the source changes.

  • Derive types from a single source rather than duplicating shapes — renames then break at compile time
  • Partial, Pick, Omit and Record cover most everyday derivations; Omit<User, "id"> is the creation payload
  • ReturnType, Parameters and Awaited extract types out of existing functions and promises
  • typeof on a value yields its type, bridging the value world and the type world
  • as const preserves literals, and typeof arr[number] turns a constant array into a union — usually better than an enum
Partial, Pick, Omit, Record
interface User {
  id: number
  name: string
  email: string
  role: 'student' | 'admin'
}

Partial<User>              // every field optional — PATCH payloads
Required<User>             // every field required
Readonly<User>             // every field readonly

Pick<User, 'id' | 'name'>          // { id, name }
Omit<User, 'id'>                   // everything except id — creation payloads

Record<'easy' | 'hard', number>    // { easy: number; hard: number }

// Realistic use
type CreateUser = Omit<User, 'id'>
type UpdateUser = Partial<Omit<User, 'id'>>
type UserCard   = Pick<User, 'name' | 'role'>

// Rename a field on User and every one of these updates with it.
// Hand-written duplicates would silently drift.
7

Advanced Types — Mapped, Conditional and Template Literal

The type system is itself a small functional language. You will read far more of this than you write, and knowing the syntax is what makes library types comprehensible.

  • Mapped types iterate over keyof T and transform each key; -readonly and -? remove modifiers
  • `as` inside a mapped type remaps key names, and mapping a key to never removes it
  • Conditional types are ternaries over types; infer captures a type from a position
  • A conditional type distributes over a union unless you wrap both sides in a tuple
  • Template literal types build string unions — useful for routes and event names, but keep them readable
Transforming every key of a type
type Partial<T> = { [K in keyof T]?: T[K] }
type Readonly<T> = { readonly [K in keyof T]: T[K] }
type Nullable<T> = { [K in keyof T]: T[K] | null }

// Removing modifiers with minus
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type Concrete<T> = { [K in keyof T]-?: T[K] }

// Remapping keys with 'as'
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }

// Filtering keys — map to never to drop them
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}
8

Classes and OOP in TypeScript

Everything Java gives you is here — access modifiers, abstract classes, interfaces — but the surrounding culture uses far fewer classes, and knowing when not to reach for one matters.

  • TypeScript classes support private, protected, readonly, abstract and implements, much as Java does
  • Parameter properties declare and assign a field in the constructor signature
  • private is erased at compile time and reachable at runtime; # is genuine encapsulation and is not serialised
  • A # private field breaks structural compatibility — two classes with one are never interchangeable
  • Prefer a module of functions or a factory closure over a class that only groups stateless methods
private, abstract, implements, parameter properties
abstract class Repository<T> {
  protected constructor(protected readonly db: Db) {}
  abstract findById(id: string): Promise<T | null>

  async requireById(id: string): Promise<T> {
    const found = await this.findById(id)
    if (!found) throw new NotFoundError(id)
    return found
  }
}

class ProblemRepo extends Repository<Problem> implements Searchable {
  // parameter properties — declares and assigns in one line
  constructor(db: Db, private readonly cache: Cache) { super(db) }

  async findById(id: string) { ... }

  #secret = 'value'        // true runtime privacy (JS private field)
  private soft = 'value'   // compile-time only — visible at runtime
}

// static, getters, and index signatures all work as expected
class Config {
  static readonly VERSION = '2.0'
  get isProd() { return process.env.NODE_ENV === 'production' }
}
9

TypeScript with React

Typing props, state, events, refs and children. This is where most TypeScript in a frontend job actually lives.

  • Type props with a plain interface and annotate the parameter — React.FC adds an implicit children prop
  • Extend React.ComponentPropsWithoutRef<"button"> to inherit every native prop on a wrapper component
  • useState infers from the initial value, so annotate only when it starts null or as an empty array
  • useRef<HTMLInputElement>(null) for DOM refs; access through optional chaining
  • Return `as const` from a custom hook so the result is a tuple, and throw in the context hook to narrow away undefined
Props interfaces and ComponentPropsWithoutRef
interface ProblemCardProps {
  problem: Problem
  locked?: boolean
  onOpen: (slug: string) => void
  children?: React.ReactNode
}

export function ProblemCard({ problem, locked = false, onOpen, children }: ProblemCardProps) {
  ...
}

// Avoid React.FC — it adds an implicit children prop and complicates generics.
// A plain annotated parameter is clearer.

// Extending native element props — the pattern for design-system components
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
  variant?: 'primary' | 'ghost'
}
export function Button({ variant = 'primary', ...rest }: ButtonProps) {
  return <button className={styles[variant]} {...rest} />
}
// Now className, disabled, onClick, aria-* all typecheck for free.

// ReactNode = anything renderable. ReactElement = specifically an element.
10

Typing the API Boundary

The types at the network edge are a claim, not a fact. Validating there — and generating types from the backend rather than hand-writing them — is what keeps a full-stack app honest.

  • `as` at the network boundary is an unverified claim — validate with a schema so the type and the check share one definition
  • z.infer derives the static type from the runtime schema, so they can never disagree
  • Generate client types from the server OpenAPI schema and check for drift in CI rather than hand-maintaining them
  • fetch does not reject on 4xx/5xx — model API failures with a typed error or a Result union
  • Dates arrive as ISO strings over JSON; type the wire shape honestly and convert once at the boundary
Zod: one definition, both a check and a type
import { z } from 'zod'

const ProblemSchema = z.object({
  slug: z.string(),
  title: z.string(),
  difficulty: z.enum(['easy', 'medium', 'hard']),
  tags: z.array(z.string()),
  hints: z.array(z.string()).nullable(),
})
type Problem = z.infer<typeof ProblemSchema>    // the type, derived

export async function getProblem(slug: string): Promise<Problem> {
  const res = await fetch(`/api/problems/${slug}`)
  if (!res.ok) throw new ApiError(res.status, await res.text())
  return ProblemSchema.parse(await res.json())  // throws on mismatch
}

// Compare: 'as Problem' claims the shape and fails 200 lines later
// with "cannot read property of undefined", far from the real cause.

// safeParse when a failure is expected and handled
const result = ProblemSchema.safeParse(input)
if (!result.success) return { error: result.error.flatten() }
11

Migrating and Living With TypeScript

How to add TypeScript to an existing codebase without a rewrite, and how to keep any from spreading once it is in.

  • Adopt incrementally: allowJs with strict off, convert leaf modules first, then enable strict flags one at a time
  • @ts-check with JSDoc types checks a .js file without renaming it
  • any spreads to every value it touches; unknown at the boundary stops the spread
  • @ts-expect-error is preferable to @ts-ignore because it errors once the underlying problem is fixed
  • Bundlers strip types without checking them — tsc --noEmit in CI is what actually enforces the types
Loose config, leaf-first, then ratchet
// 1. Add TypeScript with the loosest possible config
{ "compilerOptions": { "allowJs": true, "strict": false, "noEmit": true } }

// 2. Convert leaf-first: utilities and types before components.
//    A typed module makes everything importing it better; a typed
//    component with untyped dependencies fights inference.

// 3. Turn on strict flags one at a time, fixing as you go
"noImplicitAny": true        // usually the largest single batch
"strictNullChecks": true     // usually the most valuable

// 4. Check JS files without renaming them
// @ts-check at the top of a .js file, with JSDoc types:
/** @param {string} slug @returns {Promise<Problem>} */
async function getProblem(slug) { ... }

// 5. Ratchet: never let the error count go up.
//    tsc --noEmit in CI, and each PR must not add errors.
12

TypeScript on the Server

Node with TypeScript — module resolution, environment variables, and where a typed backend genuinely pays off in a full-stack app.

  • "type": "module" plus module: NodeNext is the modern setup; relative ESM imports need the output .js extension
  • __dirname does not exist in ESM — derive it from import.meta.url
  • Every process.env value is string | undefined; parse and coerce the whole environment once at startup
  • tsx and node --experimental-strip-types run TypeScript without checking it, so keep tsc --noEmit in CI
  • A shared schema package makes a renamed field a compile error on both ends — the main reason to run TypeScript on the server
NodeNext, .js extensions, and no __dirname
// package.json
{ "type": "module" }              // .js files are ESM
// without it, .js is CommonJS

// tsconfig for modern Node
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "types": ["node"]
  }
}

// With NodeNext, relative ESM imports need the extension —
// and it is the OUTPUT extension, which trips everyone up:
import { score } from './scoring.js'     // even though the file is .ts

// Running it
node --experimental-strip-types src/index.ts    // Node 22+
tsx src/index.ts                                 // the common dev tool
// Both strip types without checking — still run tsc --noEmit separately.

// __dirname does not exist in ESM:
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/javascript