TypeScript Basics — Type Fundamentals

Status: Active | Last Updated: 2026-08-29 Category: Languages — TypeScript Prerequisites: Basic programming concepts; familiarity with JavaScript is helpful (see kb/basics/) Tags: typescript, types, interfaces, generics Estimated Time: 4-6 hours (Self-paced, includes lab time)

Summary

TypeScript adds a static type system to JavaScript. Learn types (string, number, boolean, union, literal), how to describe objects with interfaces, and how to write reusable generic functions. This article assumes you have written code before but haven't used TypeScript specifically.

What You'll Learn (Core Competencies)

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

Why TypeScript

TypeScript is a superset of JavaScript — every valid JavaScript file is a valid TypeScript file. The difference is types. When you compile TypeScript (tsc or bun build), types are erased. At runtime you have pure JavaScript. The benefit is catching errors before you deploy. A mis-typed function call that would fall through to production in JavaScript is caught at build time. Large codebases become easier to navigate because editors can provide accurate autocomplete, jump-to-definition, and refactoring support based on the type graph. TypeScript does not enforce runtime behavior; it provides a compile-time contract that makes collaboration safer and faster. The ecosystem around TypeScript is mature — tsc ships with the language, Bun has native TypeScript support, and most modern frameworks expose typed APIs out of the box.

The Type Erasure Model

TypeScript's compiler performs type erasure: types exist only at compile time. The emitted JavaScript contains no type annotations. This is a critical architectural property:

Layer What Exists Examples
Source TypeScript with type annotations let name: string = "fogserv"
Compile-time TypeScript compiler (tsc) validates types Type errors reported
Runtime Plain JavaScript var name = "fogserv"

The implication: TypeScript cannot validate runtime data. JSON coming from an HTTP response is still any until you parse it with a type guard or a schema validator (Zod, Valibot, etc.). The type system is a compile-time contract, not a runtime guarantee.

The Type Graph

The compiler builds a type graph from your code: every value has a type, every type has structure, and relationships between types are tracked. This graph powers:

When you write interface User { id: number; name: string; email?: string }, you add three nodes to the graph: a User type, a number literal for id, and an optional string for email. Any function that takes a User parameter gets the same autocomplete experience regardless of where the function is called from.


2. Deep Dive & Implementation

Types and Type Inference

TypeScript tries to infer types from your code. When inference fails, you annotate explicitly. Type annotation syntax uses a colon after the identifier, followed by the type name. Functions declare parameter types and return types between the parameter list and the arrow or opening brace.

function greet(name: string): string {
  return `Hello, ${name}`;
}

Type inference removes noise when the compiler already knows the type. Variables initialized with a literal value receive the literal's type; arrays receive element types inferred from initial members; object properties are inferred from assigned values. When a variable must hold multiple possible types, the any type disables checks. Use any sparingly because it defeats the purpose of the type system. Prefer explicit union types or interfaces over any when you know the shape of data at design time.

Interfaces and Object Types

Interfaces describe the shape of objects. They declare property names and their types without assigning values. A function that accepts an interface can work with any object that satisfies that contract, enabling loose coupling.

interface User {
  id: number;
  name: string;
  email?: string;  // optional property
}

function sendEmail(user: User) {
  if (user.email) {
    console.log(`Emailing ${user.name}`);
  }
}

Optional properties (email?) allow objects to omit fields without errors. Readonly properties (readonly id) prevent reassignment after creation. Interfaces can extend other interfaces, creating hierarchies that model real-world relationships cleanly. Use interfaces for public contracts between modules; use type aliases when you need union or intersection types, or when describing primitives and tuples.

Union Types

Union types allow variables to hold one of several possible types. They are expressed with the pipe (|) operator. A parameter typed as number | string can receive either value. The compiler narrows the type inside conditional branches based on runtime checks, a technique called type narrowing.

function formatId(id: number | string) {
  if (typeof id === "number") {
    return id.toFixed(2);
  }
  return id.trim();
}

Literal types restrict values to exact constants ("start" | "stop" | "pause"). Combining literal types with unions creates type-safe state machines. Intersection types (A & B) combine multiple types so an object must satisfy all of them simultaneously, useful when merging configuration objects or composing behaviors.

Generics

Generics make functions and types reusable across different data types without losing type safety. They introduce a type parameter (commonly T) that acts as a placeholder for the actual type used at the call site.

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

const num = identity<number>(42);
const str = identity("hello");  // T inferred as string

Generic interfaces describe collections or containers. Array<T> is a built-in generic. Custom interfaces can be parameterized too, allowing a single definition to model different data structures. Constraints (extends) restrict which types can be passed to a generic function, enforcing that the type parameter provides specific properties or behaviors.

tsconfig and Tooling

tsconfig.json controls compilation behavior. Key options include target (JavaScript version emitted), strict (enables comprehensive type checking), esModuleInterop, and outDir. A production configuration should set strict: true, noEmitOnError: true, and declaration: true for library builds. TypeScript integrates with editors through the Language Service Protocol; tsc --noEmit runs type checks without generating files, which is ideal for CI pipelines. Bun supports TypeScript natively through its runtime, reducing build overhead for server-side applications.

Guided Checkpoint

Verify the toolchain is installed and a tiny TypeScript file type-checks under strict mode:

# Confirm tsc and a runtime are available
tsc --version && (bun --version || node --version)

# Create a temp project and check (no emit) a typed file
mkdir -p /tmp/ts-checkpoint && cd /tmp/ts-checkpoint
cat > tsconfig.json <<'EOF'
{
  "compilerOptions": {
    "target": "ES2022",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
EOF
cat > index.ts <<'EOF'
interface User { id: number; name: string; email?: string }
function sendEmail(user: User): void {
  if (user.email) console.log(`Emailing ${user.name}`);
}
sendEmail({ id: 1, name: "fogserv", email: "a@b.c" });
sendEmail({ id: 2, name: "anon" });
EOF
tsc --noEmit
# Expected: no output, exit code 0

3. Anti-Patterns & Common Pitfalls

These failure modes appear repeatedly in TypeScript codebases. Each is detectable, preventable, and rooted in a misunderstanding of the type erasure model.

Anti-Pattern: any as a Universal Escape Hatch

any disables type checking for a value. The compiler stops validating the value, so all downstream usage is untracked.

// Bad: "I'll fix the types later" — later never comes
function parseUser(payload: any) {
  return {
    id: payload.user.id,        // no error if payload.user is missing
    name: payload.user.name,    // runtime crash possible
  };
}

Detection: grep -rn ":\s*any\b" src/ or ESLint @typescript-eslint/no-explicit-any.

Fix: prefer unknown (forces a runtime check before use), define an interface, or use a schema validator (Zod) to parse untrusted payloads.

Anti-Pattern: Type Assertions Without Validation

as and angle-bracket assertions tell the compiler "trust me." When the underlying value differs, the cast is a lie and runtime crashes follow.

// Bad: trusts that the JSON has the right shape
const user = JSON.parse(rawText) as User;
console.log(user.email.toLowerCase());  // crashes if email is missing

Fix: use a runtime schema validator. Zod (User.parse(JSON.parse(rawText))) produces a typed value that is guaranteed to match User because parsing validated it.

Anti-Pattern: Optional Chaining Without Narrowing

?. returns undefined when the left side is nullish, but that result is not narrowed for the rest of the chain. Repeated ?. produces undefined | T that is easy to ignore.

// Bad: chained optionals hide missing data
const city = response?.data?.user?.address?.city;
console.log(city.toUpperCase());  // runtime error: cannot read 'toUpperCase' of undefined

Fix: assign the result to a typed variable, check it explicitly, or fail fast with a discriminated union return type that forces handling of the missing case.

Anti-Pattern: Interfaces for Union or Intersection Types

interface cannot express unions. Trying to model a Result that is either success or error with an interface alone forces a single shape.

// Won't compile cleanly
interface Result { ok: true; value: string } | { ok: false; error: Error }
// Use a type alias instead
type Result = { ok: true; value: string } | { ok: false; error: Error };

Fix: use type aliases for unions, intersections, tuples, and primitive compositions. Reserve interface for object shape contracts that benefit from declaration merging or extends.

Anti-Pattern: Strict Mode Off

Leaving "strict": false (the default) allows implicit any, null/undefined not checked, and strict function types disabled. The first bug a strict-mode-off codebase ships is a TypeError: cannot read property of undefined.

Detection: open tsconfig.json and look for strict. If absent or false, the project is shipping with weakened checks.

Fix: enable strict: true (it turns on the eight strict family flags at once) on day one of a new project. For legacy codebases, enable one flag at a time (strictNullChecks, then noImplicitAny, etc.) with // @ts-expect-error markers and convert them over time.


4. Independent Challenge

Design a type-safe HTTP request layer for a small API.

Without step-by-step guidance, produce:

  1. A Method literal union covering GET | POST | PUT | PATCH | DELETE.
  2. A Request<Body, Params> generic interface that captures method, path, query params, headers, and body. Body should default to unknown and be narrowed by method (only POST, PUT, PATCH accept a body).
  3. A Response<T> type that is a discriminated union: { status: 2xx; data: T } | { status: 4xx | 5xx; error: { code: string; message: string } }.
  4. A function request<Req extends Request<unknown, unknown>, Res>(req: Req): Promise<Response<Res>> that uses a type guard or schema validator to parse the response.
  5. A typed wrapper for one concrete endpoint: getUser(id: number): Promise<Response<User>>.

Constraints:

Deliverable: a single .ts file (or fenced blocks) that compiles under strict: true and demonstrates the discriminating behavior. No solution is provided here — verify with tsc --noEmit.


5. Consolidation & Key Invariants


6. Next Steps

After completing this article, explore runtime execution and server-side TypeScript:


Content depth matches kb/containers/docker-concepts.md — production-level patterns, real syntax, and practical examples.

Change Log

2026-08-29

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse