$ yuktics v0.1

T1 — Programming Foundations module 01.4 ~8–12 hrs

TypeScript — a typed language that pays

The most-employable language in 2026. Types, generics, narrowing, the React/Node ecosystem — without the cargo cult of every Twitter thread.

Prerequisites

  • 01.1

Stack

  • Node 22 LTS (via fnm)
  • TypeScript 5.x
  • tsx for running .ts directly
  • vitest for tests
  • pnpm or bun

By the end of this module

  • Set up a real TypeScript project (strict tsconfig, vitest, pnpm) without copying ten Stack Overflow answers.
  • Read and write idiomatic TS: types vs interfaces, generics, narrowing, utility types, discriminated unions.
  • Validate runtime data with zod and bridge it to compile-time types.
  • Ship a small typed CLI tool that reads JSON, validates it, and does something useful.

Pick one language to be employable in for the next decade and TypeScript is the safe bet. It runs on the server, in the browser, in serverless functions, on the edge. Every job listing for a frontend or full-stack role assumes it. Most fast-moving AI startups in 2026 are TS-on-the-frontend, TS-or-Python on the backend. If you’re going to learn one typed language well as your first job-grade language, this is the one.

The catch is that TypeScript has accumulated a lot of cargo cult around it. Twitter is full of conditional-type wizards proving they can encode chess in the type system. You will not need any of that. The version of TS that actually pays — the version every working engineer uses daily — is a small, useful subset that gives you most of the benefits of static typing without the type-Olympics.

This module is that subset. We will skip the Olympics. The opinionated take: you are not paid to write the cleverest type. You are paid to ship code that fails fast at the compiler and runs correctly in production. Most of the time, plain types and a few utility helpers do the job.

Set up

# Make sure you have Node 22 LTS via fnm (from 00.2)
fnm install 22 && fnm default 22
node --version          # v22.x

# Install pnpm (faster, saner than npm)
npm install -g pnpm
# or, if you want bleeding-edge: npm install -g bun

# New project
mkdir ts-deep && cd ts-deep
pnpm init
pnpm add -D typescript tsx vitest @types/node
pnpm exec tsc --init

Now edit tsconfig.json to the strict settings that actually matter. Ignore the 60 default options.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",

    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,

    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"]
}

strict: true turns on the seven flags that catch the most bugs. noUncheckedIndexedAccess is the one most people forget — without it, arr[0] is typed as T instead of T | undefined, which is a lie.

Add a script and a tiny first file:

# package.json scripts
pnpm pkg set scripts.dev="tsx watch src/main.ts"
pnpm pkg set scripts.test="vitest"
pnpm pkg set scripts.build="tsc"

mkdir src
echo 'console.log("hello typescript")' > src/main.ts
pnpm dev

If that prints hello typescript, you’re set up.

Read these first

Four sources, in this order, then stop.

  1. Matt Pocock — Total TypeScript Beginner’s Tutorial. link · 3 hrs · the single best modern starting point. Free.
  2. TypeScript handbook — Everyday Types + Narrowing + Object Types + Generics. docs · 90 min · the official material, surprisingly readable.
  3. Dan Abramov — The Magical Story of How TypeScript Compiles. post · 30 min · context for why TS works the way it does.
  4. Effective TypeScript — Dan Vanderkam. book · the second-pass book once you have the basics. Read selectively.

Skip “TypeScript Tips” Twitter threads. Most of them are showing off, not teaching.

Types vs interfaces

The first decision people overthink. Use this rule:

// Use `type` by default — it's more flexible.
type User = {
  id: string;
  name: string;
};

// Use `interface` when you specifically want declaration merging
// (e.g., extending a global like the Express Request).
interface Request {
  user?: User;
}

type can do unions, intersections, mapped types, conditional types — interface cannot. interface allows declaration merging — type does not. In 95% of code, use type. Don’t lose hours arguing about it.

The basics that matter

You’ll use these every day. Master them, defer everything else.

Primitives, arrays, objects, functions

const name: string = "ada";
const age: number = 30;
const isAdmin: boolean = false;
const tags: string[] = ["a", "b"];           // or Array<string>
const user: { id: string; age: number } = { id: "1", age: 30 };

function greet(name: string, formal = false): string {
  return formal ? `Hello, ${name}` : `hi ${name}`;
}

// Arrow with explicit types
const add: (a: number, b: number) => number = (a, b) => a + b;

Unions and narrowing

This is the single most distinctive TypeScript pattern. A value can be one of several types, and you narrow with typeof, in, instanceof, or a type predicate.

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

Discriminated unions

The pattern that replaces 90% of OOP inheritance hierarchies. A field shared across variants distinguishes them.

type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function unwrap<T>(r: Result<T>): T {
  if (r.ok) return r.value;        // TS knows .value exists here
  throw new Error(r.error);        // and .error exists here
}

This is how modern TypeScript codebases model state, API responses, parser outputs, anything with multiple shapes.

Generics

A function that works for many types without losing type info.

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const n = first([1, 2, 3]);        // number | undefined
const s = first(["a", "b"]);       // string | undefined

You will write generics constantly once you start writing utility code. Don’t avoid them; they’re not advanced.

Utility types — the four you need

TypeScript ships with a couple dozen utility types. These four cover ~80% of real use:

type User = { id: string; name: string; email: string; age: number };

type PublicUser = Omit<User, "email">;            // drop a field
type Patch = Partial<User>;                        // make all fields optional
type IdAndName = Pick<User, "id" | "name">;       // keep only some
type ById = Record<string, User>;                  // dict-like map

You’ll see Pick, Omit, Partial, Required, Record, ReturnType, Awaited, NonNullable in real code. The rest you can look up when you need them.

What NOT to obsess over

A short list of TypeScript features that consume a lot of energy and pay back almost none of it for working engineers in their first two years.

  • Conditional types and infer. You will read them in library code; you do not need to write them.
  • Mapped types beyond Partial/Pick/Record. Same.
  • Template literal types. Cute parlor trick. Almost never load-bearing.
  • The never type beyond exhaustiveness checks. Useful in one specific pattern, otherwise fancy.
  • Branded types via intersection tricks. A real tool, but not until your codebase is big enough to need them.

If you find yourself two hours into a Twitter thread about infer T extends ... — close the tab. You don’t need it.

async/await, the Node way

Same model as Python’s async, different surface. Promises are values that will resolve.

async function fetchUser(id: string): Promise<User> {
  const r = await fetch(`https://api.example.com/users/${id}`);
  if (!r.ok) throw new Error(`http ${r.status}`);
  return r.json() as Promise<User>;       // we'll do better than `as` in a sec
}

// Run several in parallel
async function main() {
  const ids = ["1", "2", "3"];
  const users = await Promise.all(ids.map(fetchUser));
  console.log(users);
}

Three rules:

  1. Always handle errors. .catch() on the boundary, or try/catch around await.
  2. Promise.all for parallel I/O. Sequential await in a loop is almost always a bug.
  3. Never type API responses with as — that’s a lie to the compiler. Use a runtime validator.

zod: bridging runtime to compile-time

The biggest TypeScript trap is assuming the network gives you the shape you typed. It does not. zod (or valibot, or arktype) is the common modern fix: define the shape once, get a runtime validator and a compile-time type from the same definition.

pnpm add zod
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().nonnegative(),
});

type User = z.infer<typeof UserSchema>;       // type derived from schema

async function fetchUser(id: string): Promise<User> {
  const r = await fetch(`https://api.example.com/users/${id}`);
  const raw = await r.json();
  return UserSchema.parse(raw);                // throws on shape mismatch
}

If the API ever lies — sends a number instead of a string, drops a field — you’ll get a clear runtime error at the boundary instead of a mysterious crash three frames in. Use zod (or one of its competitors) for every external boundary: HTTP responses, env variables, file inputs.

React, briefly

If you’re going to use React (which most TS jobs assume), the type surface you actually need is small.

import { useState } from "react";

type Todo = { id: string; text: string; done: boolean };

type Props = {
  initialTodos?: Todo[];
  onChange?: (todos: Todo[]) => void;
};

export function TodoList({ initialTodos = [], onChange }: Props) {
  const [todos, setTodos] = useState<Todo[]>(initialTodos);
  // ...
  return null;
}

That’s the shape: type props, type state with a generic useState, type events as React.MouseEvent parameterized by the element when you need them. Defer forwardRef, polymorphic components, and the rest until a specific PR forces you to learn them.

The build

Build a typed CLI that reads a JSON file describing some inputs, validates it with zod, and does something. Spec:

A CLI called validate-orders that reads a JSON array of orders, validates each one, computes the total revenue grouped by user, and writes the result to out.json. Use zod for validation. Use vitest for tests. tsc --noEmit should pass with strict mode.

Skeleton:

// src/main.ts
import { readFileSync, writeFileSync } from "node:fs";
import { z } from "zod";

const OrderSchema = z.object({
  id: z.string(),
  user: z.string(),
  items: z.array(z.object({ sku: z.string(), price: z.number(), qty: z.number().int() })),
});
type Order = z.infer<typeof OrderSchema>;

function totalByUser(orders: Order[]): Record<string, number> {
  const out: Record<string, number> = {};
  for (const o of orders) {
    const subtotal = o.items.reduce((s, i) => s + i.price * i.qty, 0);
    out[o.user] = (out[o.user] ?? 0) + subtotal;
  }
  return out;
}

const inPath = process.argv[2] ?? "orders.json";
const outPath = process.argv[3] ?? "out.json";

const raw = JSON.parse(readFileSync(inPath, "utf8"));
const orders = z.array(OrderSchema).parse(raw);
writeFileSync(outPath, JSON.stringify(totalByUser(orders), null, 2));
console.log(`wrote ${outPath}`);

Test:

// src/main.test.ts
import { describe, expect, it } from "vitest";
import { totalByUser } from "./main";

describe("totalByUser", () => {
  it("sums per user", () => {
    expect(
      totalByUser([
        { id: "1", user: "ada", items: [{ sku: "a", price: 10, qty: 2 }] },
        { id: "2", user: "ada", items: [{ sku: "b", price: 5, qty: 1 }] },
        { id: "3", user: "bob", items: [{ sku: "c", price: 7, qty: 3 }] },
      ]),
    ).toEqual({ ada: 25, bob: 21 });
  });
});

Run:

pnpm test
pnpm exec tsc --noEmit
echo '[{"id":"1","user":"ada","items":[{"sku":"a","price":10,"qty":2}]}]' > orders.json
pnpm tsx src/main.ts orders.json
cat out.json

If all four work, you’ve held a typed pipeline together end-to-end.

Going deeper

When you have specific questions, in this order:

  1. Total TypeScript — Matt Pocock. link · the most current curriculum, beginner through advanced.
  2. TypeScript handbook (full). docs · once you have a project, the rest of the handbook starts paying off.
  3. type-challenges. repo · puzzles, useful in moderation. Don’t grind these instead of shipping.
  4. Effective TypeScript — Dan Vanderkam. book · the patterns book.

Checkpoints

If any one wobbles, the corresponding section above is what to reread.

  1. Why strict: true plus noUncheckedIndexedAccess instead of the default tsconfig?
  2. When do you reach for type and when for interface? State the rule and one exception.
  3. Walk through a discriminated union and explain why it replaces most OOP inheritance hierarchies.
  4. What does zod give you that “just typing the response” doesn’t?
  5. Show the validate-orders CLI working with tsc --noEmit and pnpm test both clean.

If all five hold and the build passes, you’ve earned 01.4. Next: 01.5 — C or Rust, closer to the metal. You only need a low-level language once. But you need it once.