1. Generic Functions
A generic is a type parameter — a placeholder type, filled in when the function is actually called, that lets one function definition work correctly across many different types without losing type safety:
// Unsafe: works for anything, but tells you nothing about what comes back out
function firstElementAny(arr: any[]): any {
return arr[0];
}
const num = firstElementAny([1, 2, 3]); // typed as `any` -- no safety at all
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const num = firstElement([1, 2, 3]); // T is inferred as number -- num: number
const str = firstElement(["a", "b", "c"]); // T is inferred as string -- str: string
num.toFixed(2); // fine -- TypeScript knows num is a number
// str.toFixed(2); // ERROR -- TypeScript knows str is a string, not a number
<T> declares a type parameter named T (a
conventional name — it could be anything) that stands in for "whatever type is
actually passed in." TypeScript infers T automatically from the
argument at each call site, so firstElement([1,2,3]) and
firstElement(["a","b"]) both stay fully and correctly typed, using the
exact same function definition — no duplication, and no falling back to
any.
2. Generic Interfaces
Interfaces can take type parameters too — useful for a shape that wraps some other type, like an API response envelope:
interface ApiResponse<T> {
success: boolean;
data: T;
error?: string;
}
interface User {
id: number;
name: string;
}
const userResponse: ApiResponse<User> = {
success: true,
data: { id: 1, name: "Priya" }, // data is typed as User specifically here
};
const numbersResponse: ApiResponse<number[]> = {
success: true,
data: [1, 2, 3], // data is typed as number[] here instead
};
ApiResponse<T> is written once, but ApiResponse<User>
and ApiResponse<number[]> are two distinct, fully-typed shapes
derived from it — data's type changes to match whatever T
was filled in with at each usage. This is exactly how you'd type a real API layer:
one reusable envelope shape, wrapping whatever specific payload each endpoint
actually returns.
3. Utility Types
TypeScript ships several built-in generic types that transform an existing type instead of requiring a new one written from scratch:
interface User {
id: number;
name: string;
email: string;
}
// Partial<T>: every property becomes optional -- perfect for an "update" function
type UserUpdate = Partial<User>;
// equivalent to: { id?: number; name?: string; email?: string }
function updateUser(id: number, changes: UserUpdate) { /* ... */ }
updateUser(1, { name: "New Name" }); // fine -- only name is being changed
// Pick<T, Keys>: keep ONLY the listed properties
type UserPreview = Pick<User, "id" | "name">;
// equivalent to: { id: number; name: string }
// Omit<T, Keys>: keep everything EXCEPT the listed properties
type UserWithoutEmail = Omit<User, "email">;
// equivalent to: { id: number; name: string }
// Record<Keys, ValueType>: an object type with specific keys, all of one value type
type UserRolePermissions = Record<"admin" | "editor" | "viewer", boolean>;
// equivalent to: { admin: boolean; editor: boolean; viewer: boolean }
All four are derived from User rather than redefined by
hand — if a property is later added to User, Partial<User>
and Omit<User, "email"> both pick that change up automatically,
with nothing else needing to be updated. This is the real value of utility types:
one source of truth for a shape, with variations derived from it instead of
duplicated and allowed to drift out of sync.
4. Type Narrowing & Type Guards
Narrowing is how TypeScript moves from a broad type (like a union,
or unknown from Week 11) down to a more specific one, based on a runtime
check:
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript knows: value is `string` in this branch
}
return value.toFixed(2); // and knows: value is `number` in this branch
}
interface Cat { meow(): void; }
interface Dog { bark(): void; }
// The `pet is Cat` return type is a TYPE GUARD -- it tells TypeScript what
// TRUE actually means for the type of `pet`, not just that it's a boolean.
function isCat(pet: Cat | Dog): pet is Cat {
return "meow" in pet;
}
function makeSound(pet: Cat | Dog) {
if (isCat(pet)) {
pet.meow(); // narrowed to Cat here
} else {
pet.bark(); // narrowed to Dog here
}
}
Inside the if (typeof value === "string") branch, TypeScript
automatically treats value as a plain string — calling
.toUpperCase() is safe there in a way it wouldn't be on the original
union type. A custom type guard (pet is Cat) extends this same narrowing
to your own logic, whenever the check is too specific for typeof or
instanceof alone to express.
5. Enums
An enum is TypeScript's dedicated syntax for a fixed set of named
constants — an alternative to Week 12's string-literal union for the same
"one of these exact options" idea:
enum Status {
Pending = "PENDING",
Active = "ACTIVE",
Completed = "COMPLETED",
}
function setStatus(status: Status) {
console.log(`Status: ${status}`);
}
setStatus(Status.Active); // "Status: ACTIVE" -- referenced via the enum, not a bare string
An enum groups the related constants under one namespace (Status.Active,
not a bare, disconnected string) and gives autocomplete a concrete list to suggest.
A union of string literals ("pending" | "active" | "completed") achieves
a very similar practical result with no extra runtime object generated at all —
enums are worth knowing and recognizing, but plenty of TypeScript codebases prefer
literal unions specifically to avoid that small amount of extra generated code.
6. Hands-on Exercise
Build a typed generic API cache
Combine generics, utility types and narrowing into one small, realistic utility.
Requirements:
- Write a generic function
getFirstMatch<T>(items: T[], predicate: (item: T) => boolean): T | undefined(reusing Week 5'sfindlogic underneath). - Define a
Productinterface withid,name,price, andcategory. - Write a function
updateProduct(product: Product, changes: Partial<Product>): Productthat returns a new, merged object (spread, from Week 6) without mutating the original. - Define
type ProductPreview = Pick<Product, "id" | "name">, and write a function that converts an array of fullProducts into an array ofProductPreviews usingmap. - Write a function accepting
unknownthat uses a custom type guard to check whether the value is a validProduct(has all four expected properties with the right types) before treating it as one.
Step 5's type guard can check each property with typeof: typeof value === "object" && value !== null && "id" in value && typeof (value as any).id === "number" && ... — verbose, but this is genuinely what validating unknown data (like a parsed JSON API response) looks like in practice.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is function firstElement<T>(arr: T[]): T better than typing the same function's parameter and return value as any?
Why is function firstElement<T>(arr: T[]): T better than typing the same function's parameter and return value as any?
The generic version stays fully type-safe for every call: passing a number[] in gives back a properly-typed number, and passing a string[] in gives back a properly-typed string — TypeScript infers T from the actual argument each time. An any-typed version accepts and returns anything with zero checking at all, silently allowing mistakes an any return value would never catch.
Q2
Given interface User {'{'} id: number; name: string; email: string; {'}'}, what does Omit<User, "email"> produce?
Given interface User {'{'} id: number; name: string; email: string; {'}'}, what does Omit<User, "email"> produce?
A type equivalent to {'{'} id: number; name: string; {'}'} — every property from User except email. If User later gains a new property, Omit<User, "email"> automatically includes it too (still excluding only email), since it's derived from User rather than written out separately.
Q3
Inside if (typeof value === "string") {'{'} ... {'}'}, why can you safely call value.toUpperCase() even if value's declared type is string | number?
Inside if (typeof value === "string") {'{'} ... {'}'}, why can you safely call value.toUpperCase() even if value's declared type is string | number?
This is narrowing. TypeScript recognizes the typeof value === "string" check and, specifically inside that if block, treats value as the narrower type string rather than the full string | number union — so calling a string-only method there is safe and doesn't need an additional type assertion.
Q4
What does the special return type pet is Cat tell TypeScript, that a plain boolean return type wouldn't?
What does the special return type pet is Cat tell TypeScript, that a plain boolean return type wouldn't?
A plain boolean only tells the caller "true or false" — TypeScript wouldn't know why it was true, so it couldn't narrow anything from it. pet is Cat is a type predicate: it tells TypeScript specifically that when this function returns true, the argument's type can be narrowed to Cat from that point on — which is exactly what lets pet.meow() type-check safely inside the if branch.