06 - Advanced Topics
Type Guards
Narrow a type at runtime so TypeScript knows what you're working with:
// typeof guard — for primitives
function format(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript knows it's a string here
}
return value.toFixed(2); // TypeScript knows it's a number here
}
// Custom type guard — for complex types
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // ✅ TypeScript knows it's a string
}
}
// in guard — check if a property exists
interface Dog { bark(): void; }
interface Cat { meow(): void; }
function speak(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark(); // TypeScript knows it's a Dog
} else {
animal.meow(); // TypeScript knows it's a Cat
}
}Utility Types
Built-in types that transform other types — you'll use these constantly:
| Utility | What it does |
|---|---|
Partial<T> |
Makes all properties optional |
Required<T> |
Makes all properties required |
Readonly<T> |
Prevents properties from being reassigned |
Pick<T, K> |
Keeps only the specified properties |
Omit<T, K> |
Removes the specified properties |
Record<K, V> |
Creates an object type with keys K and values V |
ReturnType<T> |
Extracts a function's return type |
Parameters<T> |
Extracts a function's parameter types as a tuple |
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>; // all properties optional
type RequiredUser = Required<User>; // all properties required
type ReadonlyUser = Readonly<User>; // all properties readonly
type UserWithoutEmail = Omit<User, "email">; // remove specific properties
type UserIdAndName = Pick<User, "id" | "name">; // keep only specific properties
type UserRecord = Record<string, User>; // { [key: string]: User }Common use case — update functions:
// Partial<User> means all fields are optional — update only what you pass
function updateUser(id: number, updates: Partial<User>): void {
// ...
}
updateUser(1, { name: "Bob" }); // ✅ only update name
updateUser(1, { name: "Bob", email: "b@b.com" }); // ✅ update name and emailEnums
Named constants — useful for fixed sets of values:
// Numeric enum (auto-increments from 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
const move = Direction.Up; // 0
// String enum (explicit values — preferred for readability)
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING"
}
function setStatus(status: Status) { /* ... */ }
setStatus(Status.Active); // ✅
setStatus("ACTIVE"); // ❌ error — must use the enumAlternative: many prefer union types over enums (simpler, no runtime code):
type Status = "active" | "inactive" | "pending";Type Assertions
Tell TypeScript "I know the type better than you":
// as syntax (preferred)
const input = document.getElementById("input") as HTMLInputElement;
input.value; // ✅ TypeScript knows it's an input element
// angle bracket syntax (same thing, doesn't work in JSX/Svelte)
const input2 = <HTMLInputElement>document.getElementById("input");Use sparingly — you're overriding TypeScript's type checking.
Non-null Assertion
The ! operator tells TypeScript a value is definitely not null/undefined:
function process(value: string | null) {
console.log(value!.toUpperCase()); // "trust me, it's not null"
}Prefer optional chaining (?.) or null checks instead — ! can hide bugs.
Discriminated Unions
A pattern for handling different shapes with a shared "tag" property:
interface Success {
type: "success"; // the discriminant
data: string;
}
interface Error {
type: "error"; // the discriminant
message: string;
}
type Result = Success | Error;
function handle(result: Result) {
switch (result.type) {
case "success":
console.log(result.data); // TypeScript knows it's Success
break;
case "error":
console.log(result.message); // TypeScript knows it's Error
break;
}
}This pattern is used everywhere — API responses, state management, event handling.
Mapped Types
Transform properties of an existing type:
// Make all properties optional (this is how Partial<T> works internally)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};The infer Keyword
infer lets you extract a type from within a conditional type — like pattern matching for types:
// Extract the return type of a function
type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type A = GetReturn<() => string>; // string
type B = GetReturn<(x: number) => boolean>; // booleaninfer R says "whatever type appears here, capture it as R." This is how built-in utilities like ReturnType<T> and Parameters<T> work internally.
// Extract the element type from an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type C = ElementOf<string[]>; // string
type D = ElementOf<number[]>; // numberTemplate Literal Types
Create string pattern types using template literal syntax:
type Color = "red" | "blue";
type Size = "small" | "large";
type Combo = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"
let valid: Combo = "small-red"; // ✅
let invalid: Combo = "medium-red"; // ❌ errorTypeScript generates all possible combinations from the unions — useful for CSS classes, event names, or any structured string patterns.
Key Takeaways
- Type guards (
typeof,in, custom) narrow types at runtime - Utility types (
Partial,Omit,Pick,Record,ReturnType,Parameters) transform existing types - Discriminated unions use a shared "tag" property for type-safe branching
- Prefer union types (
"a" | "b") over enums for simple cases - Use
asassertions sparingly — prefer letting TypeScript infer !(non-null assertion) is risky — prefer?.(optional chaining)- Mapped types let you transform properties of existing types
inferextracts types from within conditional types- Template literal types create string patterns from unions