| name | typescript-advanced-types |
| description | Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects. |
TypeScript Advanced Types
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
When to Use This Skill
- Building type-safe libraries or frameworks
- Creating reusable generic components
- Implementing complex type inference logic
- Designing type-safe API clients
- Building form validation systems
- Creating strongly-typed configuration objects
- Implementing type-safe state management
- Migrating JavaScript codebases to TypeScript
Core Concepts
1. Generics
Purpose: Create reusable, type-flexible components while maintaining type safety.
Basic Generic Function:
function identity<T>(value: T): T {
return value
}
const num = identity<number>(42)
const str = identity<string>("hello")
const auto = identity(true)
Generic Constraints:
interface HasLength {
length: number
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length)
return item
}
logLength("hello")
logLength([1, 2, 3])
logLength({ length: 10 })
Multiple Type Parameters:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const merged = merge({ name: "John" }, { age: 30 })
2. Conditional Types
Purpose: Create types that depend on conditions, enabling sophisticated type logic.
Basic Conditional Type:
type IsString<T> = T extends string ? true : false
type A = IsString<string>
type B = IsString<number>
Extracting Return Types:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() {
return { id: 1, name: "John" }
}
type User = ReturnType<typeof getUser>
Distributive Conditional Types:
type ToArray<T> = T extends any ? T[] : never
type StrOrNumArray = ToArray<string | number>
Nested Conditions:
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object"
type T1 = TypeName<string>
type T2 = TypeName<() => void>
3. Mapped Types
Purpose: Transform existing types by iterating over their properties.
Basic Mapped Type:
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
interface User {
id: number
name: string
}
type ReadonlyUser = Readonly<User>
Optional Properties:
type Partial<T> = {
[P in keyof T]?: T[P]
}
type PartialUser = Partial<User>
Key Remapping:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>
Filtering Properties:
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
}
interface Mixed {
id: number
name: string
age: number
active: boolean
}
type OnlyNumbers = PickByType<Mixed, number>
4. Template Literal Types
Purpose: Create string-based types with pattern matching and transformation.
Basic Template Literal:
type EventName = "click" | "focus" | "blur"
type EventHandler = `on${Capitalize<EventName>}`
String Manipulation:
type UppercaseGreeting = Uppercase<"hello">
type LowercaseGreeting = Lowercase<"HELLO">
type CapitalizedName = Capitalize<"john">
type UncapitalizedName = Uncapitalize<"John">
Path Building:
type Path<T> = T extends object
? {
[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never
}[keyof T]
: never
interface Config {
server: {
host: string
port: number
}
database: {
url: string
}
}
type ConfigPath = Path<Config>
5. Utility Types
Built-in Utility Types:
type PartialUser = Partial<User>
type RequiredUser = Required<PartialUser>
type ReadonlyUser = Readonly<User>
type UserName = Pick<User, "name" | "email">
type UserWithoutPassword = Omit<User, "password">
type T1 = Exclude<"a" | "b" | "c", "a">
type T2 = Extract<"a" | "b" | "c", "a" | "b">
type T3 = NonNullable<string | null | undefined>
type PageInfo = Record<"home" | "about", { title: string }>
Advanced Patterns
Pattern 1: Type-Safe Event Emitter
type EventMap = {
"user:created": { id: string; name: string }
"user:updated": { id: string }
"user:deleted": { id: string }
}
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>
} = {}
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event]!.push(callback)
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const callbacks = this.listeners[event]
if (callbacks) {
callbacks.forEach((callback) => callback(data))
}
}
}
const emitter = new TypedEventEmitter<EventMap>()
emitter.on("user:created", (data) => {
console.log(data.id, data.name)
})
emitter.emit("user:created", { id: "1", name: "John" })
Pattern 2: Type-Safe API Client
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE"
type EndpointConfig = {
"/users": {
GET: { response: User[] }
POST: { body: { name: string; email: string }; response: User }
}
"/users/:id": {
GET: { params: { id: string }; response: User }
PUT: { params: { id: string }; body: Partial<User>; response: User }
DELETE: { params: { id: string }; response: void }
}
}
type ExtractParams<T> = T extends { params: infer P } ? P : never
type ExtractBody<T> = T extends { body: infer B } ? B : never
type ExtractResponse<T> = T extends { response: infer R } ? R : never
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
async request<Path extends keyof Config, Method extends keyof Config[Path]>(
path: Path,
method: Method,
...[options]: ExtractParams<Config[Path][Method]> extends never
? ExtractBody<Config[Path][Method]> extends never
? []
: [{ body: ExtractBody<Config[Path][Method]> }]
: [
{
params: ExtractParams<Config[Path][Method]>
body?: ExtractBody<Config[Path][Method]>
},
]
): Promise<ExtractResponse<Config[Path][Method]>> {
return {} as any
}
}
const api = new APIClient<EndpointConfig>()
const users = await api.request("/users", "GET")
const newUser = await api.request("/users", "POST", {
body: { name: "John", email: "john@example.com" },
})
const user = await api.request("/users/:id", "GET", {
params: { id: "123" },
})
Pattern 3: Builder Pattern with Type Safety
type BuilderState<T> = {
[K in keyof T]: T[K] | undefined
}
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T]
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
}[keyof T]
type IsComplete<T, S> =
RequiredKeys<T> extends keyof S
? S[RequiredKeys<T>] extends undefined
? false
: true
: false
class Builder<T, S extends BuilderState<T> = {}> {
private state: S = {} as S
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
this.state[key] = value
return this as any
}
build(this: IsComplete<T, S> extends true ? this : never): T {
return this.state as T
}
}
interface User {
id: string
name: string
email: string
age?: number
}
const builder = new Builder<User>()
const user = builder
.set("id", "1")
.set("name", "John")
.set("email", "john@example.com")
.build()
Pattern 4: Deep Readonly/Partial
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P]
}
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[P]>
: T[P]
}
interface Config {
server: {
host: string
port: number
ssl: {
enabled: boolean
cert: string
}
}
database: {
url: string
pool: {
min: number
max: number
}
}
}
type ReadonlyConfig = DeepReadonly<Config>
type PartialConfig = DeepPartial<Config>
Pattern 5: Type-Safe Form Validation
type ValidationRule<T> = {
validate: (value: T) => boolean
message: string
}
type FieldValidation<T> = {
[K in keyof T]?: ValidationRule<T[K]>[]
}
type ValidationErrors<T> = {
[K in keyof T]?: string[]
}
class FormValidator<T extends Record<string, any>> {
constructor(private rules: FieldValidation<T>) {}
validate(data: T): ValidationErrors<T> | null {
const errors: ValidationErrors<T> = {}
let hasErrors = false
for (const key in this.rules) {
const fieldRules = this.rules[key]
const value = data[key]
if (fieldRules) {
const fieldErrors: string[] = []
for (const rule of fieldRules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message)
}
}
if (fieldErrors.length > 0) {
errors[key] = fieldErrors
hasErrors = true
}
}
}
return hasErrors ? errors : null
}
}
interface LoginForm {
email: string
password: string
}
const validator = new FormValidator<LoginForm>({
email: [
{
validate: (v) => v.includes("@"),
message: "Email must contain @",
},
{
validate: (v) => v.length > 0,
message: "Email is required",
},
],
password: [
{
validate: (v) => v.length >= 8,
message: "Password must be at least 8 characters",
},
],
})
const errors = validator.validate({
email: "invalid",
password: "short",
})
Pattern 6: Discriminated Unions
type Success<T> = {
status: "success"
data: T
}
type Error = {
status: "error"
error: string
}
type Loading = {
status: "loading"
}
type AsyncState<T> = Success<T> | Error | Loading
function handleState<T>(state: AsyncState<T>): void {
switch (state.status) {
case "success":
console.log(state.data)
break
case "error":
console.log(state.error)
break
case "loading":
console.log("Loading...")
break
}
}
type State =
| { type: "idle" }
| { type: "fetching"; requestId: string }
| { type: "success"; data: any }
| { type: "error"; error: Error }
type Event =
| { type: "FETCH"; requestId: string }
| { type: "SUCCESS"; data: any }
| { type: "ERROR"; error: Error }
| { type: "RESET" }
function reducer(state: State, event: Event): State {
switch (state.type) {
case "idle":
return event.type === "FETCH"
? { type: "fetching", requestId: event.requestId }
: state
case "fetching":
if (event.type === "SUCCESS") {
return { type: "success", data: event.data }
}
if (event.type === "ERROR") {
return { type: "error", error: event.error }
}
return state
case "success":
case "error":
return event.type === "RESET" ? { type: "idle" } : state
}
}
Type Inference Techniques
1. Infer Keyword
type ElementType<T> = T extends (infer U)[] ? U : never
type NumArray = number[]
type Num = ElementType<NumArray>
type PromiseType<T> = T extends Promise<infer U> ? U : never
type AsyncNum = PromiseType<Promise<number>>
type Parameters<T> = T extends (...args: infer P) => any ? P : never
function foo(a: string, b: number) {}
type FooParams = Parameters<typeof foo>
2. Type Guards
function isString(value: unknown): value is string {
return typeof value === "string"
}
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(guard)
}
const data: unknown = ["a", "b", "c"]
if (isArrayOf(data, isString)) {
data.forEach((s) => s.toUpperCase())
}
3. Assertion Functions
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string")
}
}
function processValue(value: unknown) {
assertIsString(value)
console.log(value.toUpperCase())
}
Best Practices
- Use
unknown over any: Enforce type checking
- Prefer
interface for object shapes: Better error messages
- Use
type for unions and complex types: More flexible
- Leverage type inference: Let TypeScript infer when possible
- Create helper types: Build reusable type utilities
- Use const assertions: Preserve literal types
- Avoid type assertions: Use type guards instead
- Document complex types: Add JSDoc comments
- Use strict mode: Enable all strict compiler options
- Test your types: Use type tests to verify type behavior
Type Testing
type AssertEqual<T, U> = [T] extends [U]
? [U] extends [T]
? true
: false
: false
type Test1 = AssertEqual<string, string>
type Test2 = AssertEqual<string, number>
type Test3 = AssertEqual<string | number, string>
type ExpectError<T extends never> = T
type ShouldError = ExpectError<AssertEqual<string, number>>
Common Pitfalls
- Over-using
any: Defeats the purpose of TypeScript
- Ignoring strict null checks: Can lead to runtime errors
- Too complex types: Can slow down compilation
- Not using discriminated unions: Misses type narrowing opportunities
- Forgetting readonly modifiers: Allows unintended mutations
- Circular type references: Can cause compiler errors
- Not handling edge cases: Like empty arrays or null values
Performance Considerations
- Avoid deeply nested conditional types
- Use simple types when possible
- Cache complex type computations
- Limit recursion depth in recursive types
- Use build tools to skip type checking in production