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)
- Annotate values, parameters, and return types with primitive and literal types
- Apply type inference and choose union/intersection types over
any - Model object shapes with interfaces, optional and readonly properties
- Write reusable generic functions and constrained type parameters
- Configure
tsconfig.jsonfor strict, production-grade type checking
Table of Contents
- Architectural Overview & Core Schema
- Deep Dive & Implementation
- Anti-Patterns & Common Pitfalls
- Independent Challenge
- Consolidation & Key Invariants
- 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:
- Autocomplete — the editor knows
user.emailisstring | undefined - Refactoring — renaming a property updates every usage
- Type checking — incompatible assignments are caught at build time
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:
- A
Methodliteral union coveringGET | POST | PUT | PATCH | DELETE. - A
Request<Body, Params>generic interface that captures method, path, query params, headers, and body. Body should default tounknownand be narrowed by method (onlyPOST,PUT,PATCHaccept a body). - A
Response<T>type that is a discriminated union:{ status: 2xx; data: T } | { status: 4xx | 5xx; error: { code: string; message: string } }. - 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. - A typed wrapper for one concrete endpoint:
getUser(id: number): Promise<Response<User>>.
Constraints:
- No
anyanywhere.unknownis allowed and must be narrowed before use. - The compiler must reject
getUser(1).then(r => r.data.email.toLowerCase())if the narrowing is missing. - The design should let the IDE autocomplete
req.bodyonly onPOST/PUT/PATCHand not onGET/DELETE.
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
- Types are erased at runtime. TypeScript validates before deploy; it does not protect against untrusted runtime data — pair it with a schema validator.
strict: trueis the default for new projects. It enablesstrictNullChecks,noImplicitAny, and the rest of the strict family in one flag.- Prefer
unknownoverany.unknownforces a runtime check before use;anyopts out of checking entirely. - Interfaces describe object shapes; type aliases describe unions, intersections, and primitives. Choose the form that matches the shape you are modeling.
- Use
asand angle-bracket assertions only after a runtime check. Type assertions are promises, not validations. - Generics preserve type information through reusable code. Without them, you reach for
anyand lose autocomplete and safety. - Narrowing follows control flow. The compiler refines types inside
if (typeof x === "number")blocks, so order checks carefully and use early returns.
6. Next Steps
After completing this article, explore runtime execution and server-side TypeScript:
- Runtime — How TypeScript actually runs: Bun, Node, Deno, the
tscpipeline, and source maps. - Backend Development — Build HTTP services with typed APIs, request validation, and ORM-mapped models.
- Practice: convert an existing JavaScript file to TypeScript incrementally — add interfaces for data models, generics for utility functions, and enable
strict: trueonce everything type-checks.
Content depth matches kb/containers/docker-concepts.md — production-level patterns, real syntax, and practical examples.
Change Log
2026-08-29
- Full hydration of TypeScript Basics lesson
- Added sections: Why TypeScript, Types and Inference, Interfaces, Union Types, Generics, tsconfig and Tooling
- Included production-level TypeScript patterns and syntax examples
- 2026-09-15 — Migrated to canonical 6-section template (Architectural Overview, Deep Dive with Guided Checkpoint, Anti-Patterns, Independent Challenge, Consolidation, Next Steps). Preserved all prior content, prerequisites, and change log.