TypeScript's Type Checker
So far, we’ve focused on how to design types and functions. TypeScript’s type checker is what ensures these designs are used correctly. It reads your code, verifies that types are used consistently, and catches bugs before runtime.
1. Type Inference
One of TypeScript's superpowers is type inference—it can often figure out what type something has without you explicitly saying so.
Inferring from literals
const age = 30; // TypeScript infers: number
const name = "Alice"; // TypeScript infers: string
const isActive = true; // TypeScript infers: boolean
// TypeScript catches this error:
age = "thirty"; // ✗ Type error: cannot assign string to numberInferring from function calls
function add(a: number, b: number): number {
return a + b;
}
const result = add(2, 3); // TypeScript infers: result is number
result.toUpperCase(); // ✗ Type error: numbers don't have toUpperCaseWhen to be explicit
While inference is convenient, it's sometimes good to be explicit about types:
Function parameters — Always annotate these. Without explicit types, you lose type safety.
ts// ✓ Good: parameter type is explicit function greet(name: string): string { return `Hello, ${name}`; } // ✗ Bad: type is inferred as `any`, loses safety function greetBad(name) { return `Hello, ${name}`; }Function return types — Annotate these to document intent and prevent accidental changes.
ts// ✓ Good: clear what this returns function getName(person: { name: string }): string { return person.name; } // Risky: if you change the function body, the return type changes implicitly function getNameBad(person: { name: string }) { return person.name; }Complex variables — Make the type explicit for clarity.
ts// ✓ Clear what this is const config: { host: string; port: number } = { host: "localhost", port: 3000 }; // ✗ Less clear const configBad = { host: "localhost", port: 3000 };
2. Type Narrowing
When you check or refine types in your code, TypeScript narrows the type—it figures out that in certain branches, a value must be a more specific type.
Narrowing with if checks
function processValue(value: string | number) {
if (typeof value === "string") {
// In this branch, TypeScript knows value is a string
console.log(value.toUpperCase()); // ✓ strings have toUpperCase
} else {
// In this branch, TypeScript knows value is a number
console.log(value.toFixed(2)); // ✓ numbers have toFixed
}
}Narrowing with discriminated unions
type Result = Success | Failure;
type Success = { kind: "success"; data: string };
type Failure = { kind: "failure"; error: Error };
function handleResult(result: Result) {
if (result.kind === "success") {
// TypeScript knows result is Success
console.log(result.data); // ✓ Success has data
} else {
// TypeScript knows result is Failure
console.log(result.error); // ✓ Failure has error
}
}Narrowing with truthiness
function printLength(str: string | null) {
if (str) {
// In this branch, str is not null
console.log(str.length); // ✓ str is definitely a string
} else {
console.log("Empty");
}
}Type guards
For custom types, you can write type guard functions that narrow types:
type Dog = { kind: "dog"; bark(): void };
type Cat = { kind: "cat"; meow(): void };
type Pet = Dog | Cat;
// A type guard function
function isDog(pet: Pet): pet is Dog {
return pet.kind === "dog";
}
function makeSound(pet: Pet) {
if (isDog(pet)) {
pet.bark(); // ✓ TypeScript knows pet is Dog
} else {
pet.meow(); // ✓ TypeScript knows pet is Cat
}
}3. Structural Typing
TypeScript uses structural typing, meaning types are based on their shape, not their name.
What this means
type Point = { x: number; y: number };
type Vector = { x: number; y: number };
const p: Point = { x: 1, y: 2 };
const v: Vector = p; // ✓ OK! Point and Vector have the same shape
function distance(p: Point): number {
return Math.sqrt(p.x ** 2 + p.y ** 2);
}
distance(v); // ✓ OK! v looks like a Point (same shape)Even though Point and Vector have different names, they're compatible because they have the same structure.
Structural compatibility goes both ways
type User = { name: string; email: string };
const obj = { name: "Alice", email: "alice@example.com", age: 30 };
// obj has all required properties + extra ones
const user: User = obj; // ✓ OK! Extra properties are fineBut missing properties cause errors:
const incomplete = { name: "Bob" };
const user: User = incomplete; // ✗ Error: missing emailWhy this matters
Structural typing means you can pass objects around as long as they have the right shape. You don't need explicit inheritance or interface declarations (though you can use them for documentation).
4. Union Types
A union type describes a value that could be one of several types.
Basic unions
type Id = string | number;
const userId: Id = "user-123"; // ✓ string is OK
const countId: Id = 456; // ✓ number is OK
const invalid: Id = true; // ✗ boolean is not in the unionUnion of objects
type Response = { status: "success"; data: any } | { status: "error"; message: string };
const goodResponse: Response = { status: "success", data: [1, 2, 3] };
const badResponse: Response = { status: "error", message: "Failed" };Never type
In some cases, a branch is impossible. TypeScript represents this with never:
function exhaustiveCheck(value: string | number) {
if (typeof value === "string") {
return value.length;
} else if (typeof value === "number") {
return value.toFixed();
} else {
// If you've handled all cases, this code is unreachable
const impossible: never = value; // ✓ This is fine (unreachable)
}
}This pattern is useful for ensuring you've handled all cases in a union.
5. Common Type Patterns
Optional types
type User = {
name: string;
email?: string; // email is optional (string | undefined)
};
const user1: User = { name: "Alice" }; // ✓ email is optional
const user2: User = { name: "Bob", email: "bob@example.com" }; // ✓ email provided
// Accessing optional properties:
if (user1.email) {
console.log(user1.email.length); // ✓ email is narrowed to string
}Record types (for objects with dynamic keys)
type Scores = Record<string, number>;
const scores: Scores = {
alice: 95,
bob: 87,
charlie: 92
};
// Keys can be any string
scores["diana"] = 89; // ✓ OKArray types
type StringArray = string[];
type NumberOrString = (number | string)[];
const words: StringArray = ["hello", "world"];
const mixed: NumberOrString = [1, "two", 3];Readonly types
type Config = {
readonly host: string;
readonly port: number;
};
const config: Config = { host: "localhost", port: 3000 };
config.host = "example.com"; // ✗ Error: cannot assign to readonly propertySummary
TypeScript's type checker provides several powerful features:
- Inference — The compiler figures out types when not explicitly annotated
- Narrowing — Type-safe refinement of types within branches
- Structural typing — Compatibility based on shape, not name
- Unions — A value can be one of several types
- Common patterns — Optional, readonly, arrays, records for modeling various scenarios
Together, these features let you write code that's both flexible and safe—the type checker verifies correctness before you run your program.