TypeScript Advanced Types: Generics, Utility Types, Mapped Types and Conditional Types
TypeScript's type system is far more expressive than most developers realize. Once you go beyond basic annotations and into generics, mapped types, and conditional types, you can write code that is simultaneously more flexible and more type-safe. These concepts appear in senior TypeScript interviews and are essential for building robust libraries and design systems.
Generics with Constraints
Generics let you write code that works across multiple types while preserving type information:
typescript-- Basic generic function function identity<T>(value: T): T { return value; } identity<string>("hello"); -- T = string identity(42); -- T inferred as number -- Generic with constraint function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user = { id: 1, name: "Alice", email: "alice@example.com" }; getProperty(user, "name"); -- string (correct type inferred) getProperty(user, "id"); -- number getProperty(user, "age"); -- Error: "age" not in keyof typeof user -- Multiple type parameters function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; } const merged = merge({ name: "Alice" }, { age: 30 }); merged.name; -- string merged.age; -- number
Generic Classes and Interfaces
typescriptinterface Repository<T, ID = number> { findById(id: ID): Promise<T | null>; findAll(): Promise<T[]>; save(entity: T): Promise<T>; delete(id: ID): Promise<void>; } class UserRepository implements Repository<User> { async findById(id: number): Promise<User | null> { return db.users.findById(id); } async findAll(): Promise<User[]> { return db.users.findAll(); } async save(user: User): Promise<User> { return db.users.save(user); } async delete(id: number): Promise<void> { await db.users.delete(id); } } -- Generic class class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } get size(): number { return this.items.length; } } const numStack = new Stack<number>(); numStack.push(1); numStack.push(2); numStack.pop(); -- number | undefined
Built-in Utility Types
TypeScript ships with a rich set of utility types for transforming existing types:
typescriptinterface User { id: number; name: string; email: string; password: string; createdAt: Date; } -- Partial: all properties optional type UserUpdate = Partial<User>; -- { id?: number; name?: string; email?: string; ... } -- Required: all properties required (inverse of Partial) type RequiredUser = Required<Partial<User>>; -- Readonly: prevents mutation type ImmutableUser = Readonly<User>; const user: ImmutableUser = { id: 1, name: "Alice", email: "a@b.com", password: "x", createdAt: new Date() }; user.name = "Bob"; -- Error: Cannot assign to 'name' because it is a read-only property -- Pick: select specific properties type UserPublic = Pick<User, "id" | "name" | "email">; -- { id: number; name: string; email: string; } -- Omit: exclude specific properties type UserWithoutPassword = Omit<User, "password">; -- { id: number; name: string; email: string; createdAt: Date; } -- Record: map keys to a type type RolePermissions = Record<"admin" | "editor" | "viewer", string[]>; const permissions: RolePermissions = { admin: ["read", "write", "delete"], editor: ["read", "write"], viewer: ["read"], }; -- Exclude / Extract: filter union types type Status = "pending" | "active" | "suspended" | "deleted"; type ActiveStatus = Exclude<Status, "deleted" | "suspended">; -- "pending" | "active" type InactiveStatus = Extract<Status, "suspended" | "deleted">; -- "suspended" | "deleted" -- NonNullable: remove null and undefined type MaybeString = string | null | undefined; type DefiniteString = NonNullable<MaybeString>; -- string -- ReturnType: extract function return type async function fetchUser(): Promise<User> { ... } type FetchUserReturn = Awaited<ReturnType<typeof fetchUser>>; -- User -- Parameters: extract function parameter types function createUser(name: string, email: string, age: number): User { ... } type CreateUserParams = Parameters<typeof createUser>; -- [string, string, number]
Mapped Types
Mapped types transform every property of an existing type:
typescript-- Make all properties nullable type Nullable<T> = { [K in keyof T]: T[K] | null; }; type NullableUser = Nullable<User>; -- { id: number | null; name: string | null; ... } -- Make all properties optional getters/setters type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type UserGetters = Getters<Pick<User, "name" | "email">>; -- { getName: () => string; getEmail: () => string; } -- Validation schema type type ValidationRules<T> = { [K in keyof T]?: { required?: boolean; minLength?: number; maxLength?: number; pattern?: RegExp; }; }; const userValidation: ValidationRules<User> = { name: { required: true, minLength: 2, maxLength: 50 }, email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }, }; -- Remapping keys with as type EventHandlers<T> = { [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void; }; type FormHandlers = EventHandlers<Pick<User, "name" | "email">>; -- { onNameChange: (value: string) => void; onEmailChange: (value: string) => void; }
Conditional Types
Types that depend on a condition β like ternary operators for types:
typescript-- Basic conditional type type IsString<T> = T extends string ? true : false; type A = IsString<string>; -- true type B = IsString<number>; -- false -- Unwrap array element type type ElementType<T> = T extends (infer U)[] ? U : never; type StrElement = ElementType<string[]>; -- string type NumElement = ElementType<number[]>; -- number type NotArray = ElementType<string>; -- never -- Unwrap Promise type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type R1 = Awaited<Promise<string>>; -- string type R2 = Awaited<Promise<Promise<number>>>; -- number -- Conditional + mapped: filter properties by type type PickByType<T, ValueType> = { [K in keyof T as T[K] extends ValueType ? K : never]: T[K]; }; type StringFields = PickByType<User, string>; -- { name: string; email: string; password: string; } type NumberFields = PickByType<User, number>; -- { id: number; }
Template Literal Types
String manipulation at the type level:
typescripttype EventName = "click" | "focus" | "blur"; type Handler = `on${Capitalize<EventName>}`; -- "onClick" | "onFocus" | "onBlur" type CSSProperty = "margin" | "padding"; type CSSDirection = "top" | "right" | "bottom" | "left"; type CSSSpacing = `${CSSProperty}-${CSSDirection}`; -- "margin-top" | "margin-right" | ... | "padding-left" -- Route builder type safety type ApiRoute = "/users" | "/posts" | "/comments"; type ApiMethod = "GET" | "POST" | "PUT" | "DELETE"; type Endpoint = `${ApiMethod} ${ApiRoute}`; -- "GET /users" | "POST /users" | "GET /posts" | ... function callApi(endpoint: Endpoint): void { ... } callApi("GET /users"); -- valid callApi("GET /invalid"); -- Error!
Type Guards
Narrow types at runtime safely:
typescript-- typeof guard function format(value: string | number): string { if (typeof value === "string") { return value.toUpperCase(); -- TypeScript knows: string } return value.toFixed(2); -- TypeScript knows: number } -- instanceof guard function handleError(error: unknown): string { if (error instanceof Error) { return error.message; -- TypeScript knows: Error } return String(error); } -- Custom type predicate interface Cat { meow(): void; } interface Dog { bark(): void; } function isCat(animal: Cat | Dog): animal is Cat { return "meow" in animal; } function makeSound(animal: Cat | Dog): void { if (isCat(animal)) { animal.meow(); -- TypeScript knows: Cat } else { animal.bark(); -- TypeScript knows: Dog } } -- Discriminated union guard type Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number } | { kind: "triangle"; base: number; height: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; case "triangle": return 0.5 * shape.base * shape.height; } }
Common Interview Questions
Q: What is the difference between interface and type in TypeScript?
Both define types. Interfaces support declaration merging (you can reopen an interface to add properties) and are slightly better for object shapes you plan to extend. Type aliases support unions, intersections, conditional types, and mapped types. In practice, use interface for object shapes and public API contracts; use type for unions, utility types, and complex type transformations.
Q: What is a mapped type and when would you use one?
A mapped type iterates over the keys of an existing type to create a new type, transforming properties in some way. Common uses: making all fields optional (Partial), readonly (Readonly), or nullable; building form validation schemas; generating event handler types. They eliminate repetition β instead of manually writing out every property, you describe the transformation once.
Q: What is infer in TypeScript?
infer is used inside conditional types to capture (infer) a type from a matched position. For example, T extends Promise<infer U> ? U : never extracts the resolved type of a Promise. It is how ReturnType, Parameters, Awaited, and many utility types are implemented internally.
Practice TypeScript on Froquiz
Advanced TypeScript types are tested in senior frontend and full-stack interviews. Test your JavaScript and TypeScript knowledge on Froquiz across all difficulty levels.
Summary
- Generics with
extendsconstraints restrict what types can be used while keeping flexibility - Built-in utility types (
Partial,Omit,Pick,Record,ReturnType) cover the most common transformations - Mapped types transform every property of a type β essential for building type-safe abstractions
- Conditional types (
T extends U ? X : Y) enable type-level logic and filtering - Template literal types enable type-safe string composition
- Type guards (
typeof,instanceof,ispredicates, discriminated unions) narrow union types safely inferinside conditional types extracts inner types from generic or complex types