Chapter 2: Advanced Types
Authored by syscook.dev
What are Advanced Types in TypeScript?
Advanced types in TypeScript are sophisticated type constructs that provide powerful ways to create flexible, reusable, and type-safe code. These types go beyond basic primitives and include utility types, conditional types, mapped types, and template literal types that enable complex type manipulations and transformations.
Key Advanced Type Categories:
- Utility Types: Built-in types for common transformations
- Conditional Types: Types that depend on conditions
- Mapped Types: Types that transform existing types
- Template Literal Types: String manipulation at the type level
- Indexed Access Types: Accessing types by their properties
- Keyof and typeof Operators: Type-level operators for introspection
Why Use Advanced Types?
1. Type Safety at Scale
Advanced types help maintain type safety in complex applications with dynamic data structures and APIs.
// Without advanced types - prone to errors
function processApiResponse(response: any) {
return response.data.map((item: any) => item.name);
}
// With advanced types - type safe
interface ApiResponse<T> {
data: T[];
status: number;
message: string;
}
function processApiResponse<T extends { name: string }>(
response: ApiResponse<T>
): string[] {
return response.data.map(item => item.name);
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const apiResponse: ApiResponse<User> = {
data: [{ id: 1, name: "John", email: "[email protected]" }],
status: 200,
message: "Success"
};
const names = processApiResponse(apiResponse);
console.log(names);
Output:
['John']
2. Code Reusability
Advanced types enable creating reusable type utilities that work across different data structures.
// Reusable type utility
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Create a type where password is optional
type UserWithoutPassword = Optional<User, 'password'>;
// This works for any interface
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type ProductWithoutDescription = Optional<Product, 'description'>;
const user: UserWithoutPassword = {
id: 1,
name: "Alice",
email: "[email protected]"
// password is optional
};
console.log(user);
Output:
{ id: 1, name: 'Alice', email: '[email protected]' }
3. Better Developer Experience
Advanced types provide better IntelliSense, autocomplete, and error messages.
// Advanced type provides better autocomplete
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
interface FormData {
name: string;
email: string;
age: number;
}
type FormEventHandlers = EventHandlers<FormData>;
// IDE will suggest: onName, onEmail, onAge
const handlers: FormEventHandlers = {
onName: (value: string) => console.log("Name:", value),
onEmail: (value: string) => console.log("Email:", value),
onAge: (value: number) => console.log("Age:", value)
};
handlers.onName("John");
handlers.onEmail("[email protected]");
handlers.onAge(25);
Output:
Name: John
Email: [email protected]
Age: 25
How to Use Advanced Types?
1. Utility Types
Partial, Required, and Readonly
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Partial makes all properties optional
type PartialUser = Partial<User>;
const partialUser: PartialUser = { name: "John" }; // All properties optional
// Required makes all properties mandatory
type RequiredUser = Required<PartialUser>;
const requiredUser: RequiredUser = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
// Readonly makes all properties read-only
type ReadonlyUser = Readonly<User>;
const readonlyUser: ReadonlyUser = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
// readonlyUser.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
console.log("Partial:", partialUser);
console.log("Required:", requiredUser);
console.log("Readonly:", readonlyUser);
Output:
Partial: { name: 'John' }
Required: { id: 1, name: 'John', email: '[email protected]', isActive: true }
Readonly: { id: 1, name: 'John', email: '[email protected]', isActive: true }
Pick, Omit, and Record
interface User {
id: number;
name: string;
email: string;
password: string;
isActive: boolean;
}
// Pick specific properties
type UserPublicInfo = Pick<User, 'id' | 'name' | 'email'>;
const publicUser: UserPublicInfo = {
id: 1,
name: "John",
email: "[email protected]"
};
// Omit specific properties
type UserWithoutPassword = Omit<User, 'password'>;
const userWithoutPassword: UserWithoutPassword = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
// Record creates an object type with specific keys and value types
type UserRoles = Record<string, string[]>;
const userRoles: UserRoles = {
admin: ["read", "write", "delete"],
user: ["read"],
guest: []
};
console.log("Public info:", publicUser);
console.log("Without password:", userWithoutPassword);
console.log("User roles:", userRoles);
Output:
Public info: { id: 1, name: 'John', email: '[email protected]' }
Without password: { id: 1, name: 'John', email: '[email protected]', isActive: true }
User roles: { admin: ['read', 'write', 'delete'], user: ['read'], guest: [] }
Extract, Exclude, and NonNullable
// Extract specific types from a union
type AllowedTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = Extract<AllowedTypes, string | number | boolean>;
const primitive: PrimitiveTypes = "hello"; // Can be string, number, or boolean
// Exclude specific types from a union
type NonNullTypes = Exclude<AllowedTypes, null | undefined>;
const nonNull: NonNullTypes = "world"; // Cannot be null or undefined
// Remove null and undefined from a type
type CleanString = NonNullable<string | null | undefined>;
const clean: CleanString = "clean"; // Cannot be null or undefined
console.log("Primitive:", primitive);
console.log("Non-null:", nonNull);
console.log("Clean:", clean);
Output:
Primitive: hello
Non-null: world
Clean: clean
2. Conditional Types
Basic Conditional Types
// Conditional type that checks if T is a string
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<"hello">; // true
// Conditional type for array element extraction
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringArrayElement = ArrayElement<string[]>; // string
type NumberArrayElement = ArrayElement<number[]>; // number
type NotArrayElement = ArrayElement<string>; // never
// Practical example
function getFirstElement<T>(arr: T[]): ArrayElement<T[]> {
return arr[0];
}
const firstString = getFirstElement(["hello", "world"]);
const firstNumber = getFirstElement([1, 2, 3]);
console.log("First string:", firstString);
console.log("First number:", firstNumber);
Output:
First string: hello
First number: 1
Advanced Conditional Types
// Conditional type for function return type extraction
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser(): { id: number; name: string } {
return { id: 1, name: "John" };
}
type UserReturnType = ReturnType<typeof getUser>; // { id: number; name: string }
// Conditional type for deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
interface NestedObject {
user: {
profile: {
name: string;
settings: {
theme: string;
};
};
};
}
type ReadonlyNested = DeepReadonly<NestedObject>;
const readonlyNested: ReadonlyNested = {
user: {
profile: {
name: "John",
settings: {
theme: "dark"
}
}
}
};
// readonlyNested.user.profile.name = "Jane"; // Error: Cannot assign to 'name' because it is a read-only property
console.log("User return type:", getUser());
console.log("Readonly nested:", readonlyNested);
Output:
User return type: { id: 1, name: 'John' }
Readonly nested: { user: { profile: { name: 'John', settings: { theme: 'dark' } } } }
3. Mapped Types
Basic Mapped Types
// Make all properties optional
type Optional<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Product {
id: number;
name: string;
price: number;
}
type OptionalProduct = Optional<Product>;
type RequiredProduct = Required<OptionalProduct>;
type ReadonlyProduct = Readonly<Product>;
const optionalProduct: OptionalProduct = { name: "Laptop" };
const requiredProduct: RequiredProduct = {
id: 1,
name: "Laptop",
price: 999
};
console.log("Optional:", optionalProduct);
console.log("Required:", requiredProduct);
Output:
Optional: { name: 'Laptop' }
Required: { id: 1, name: 'Laptop', price: 999 }
Advanced Mapped Types
// Transform property names
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface User {
name: string;
age: number;
email: string;
}
type UserGetters = Getters<User>;
const userGetters: UserGetters = {
getName: () => "John",
getAge: () => 25,
getEmail: () => "[email protected]"
};
console.log("Name:", userGetters.getName());
console.log("Age:", userGetters.getAge());
console.log("Email:", userGetters.getEmail());
// Filter properties by type
type StringProperties<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
interface MixedObject {
id: number;
name: string;
isActive: boolean;
description: string;
count: number;
}
type StringProps = StringProperties<MixedObject>;
const stringProps: StringProps = {
name: "John",
description: "A user object"
};
console.log("String properties:", stringProps);
Output:
Name: John
Age: 25
Email: [email protected]
String properties: { name: 'John', description: 'A user object' }
4. Template Literal Types
Basic Template Literal Types
// String manipulation at type level
type Greeting<T extends string> = `Hello, ${T}!`;
type Farewell<T extends string> = `Goodbye, ${T}!`;
type HelloJohn = Greeting<"John">; // "Hello, John!"
type GoodbyeAlice = Farewell<"Alice">; // "Goodbye, Alice!"
// Dynamic event handler names
type EventHandler<T extends string> = `on${Capitalize<T>}`;
type ClickHandler = EventHandler<"click">; // "onClick"
type SubmitHandler = EventHandler<"submit">; // "onSubmit"
// API endpoint generation
type ApiEndpoint<T extends string> = `/api/${T}`;
type UserEndpoint = ApiEndpoint<"users">; // "/api/users"
type ProductEndpoint = ApiEndpoint<"products">; // "/api/products"
console.log("Hello John type:", "Hello, John!" as HelloJohn);
console.log("Click handler type:", "onClick" as ClickHandler);
console.log("User endpoint type:", "/api/users" as UserEndpoint);
Output:
Hello John type: Hello, John!
Click handler type: onClick
User endpoint type: /api/users
Advanced Template Literal Types
// CSS property generation
type CssProperty<T extends string> = `--${T}`;
type ThemeColor = CssProperty<"primary-color">; // "--primary-color"
type ThemeFont = CssProperty<"font-family">; // "--font-family"
// Database column naming
type DbColumn<T extends string> = `db_${Lowercase<T>}`;
type UserIdColumn = DbColumn<"UserId">; // "db_userid"
type UserNameColumn = DbColumn<"UserName">; // "db_username"
// Route parameter extraction
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type UserRouteParams = ExtractParams<"/users/:id/posts/:postId">; // "id" | "postId"
type SimpleRouteParams = ExtractParams<"/profile/:userId">; // "userId"
console.log("Theme color:", "--primary-color" as ThemeColor);
console.log("DB column:", "db_userid" as UserIdColumn);
console.log("Route params type:", "id" as UserRouteParams);
Output:
Theme color: --primary-color
DB column: db_userid
Route params type: id
5. Indexed Access Types
Basic Indexed Access
interface User {
id: number;
name: string;
profile: {
age: number;
email: string;
settings: {
theme: string;
notifications: boolean;
};
};
}
// Access nested properties
type UserName = User['name']; // string
type UserProfile = User['profile']; // { age: number; email: string; settings: { theme: string; notifications: boolean; } }
type UserSettings = User['profile']['settings']; // { theme: string; notifications: boolean; }
type UserTheme = User['profile']['settings']['theme']; // string
// Access with union types
type UserProperty = User['name' | 'id']; // string | number
const userName: UserName = "John";
const userProfile: UserProfile = {
age: 25,
email: "[email protected]",
settings: {
theme: "dark",
notifications: true
}
};
console.log("User name:", userName);
console.log("User profile:", userProfile);
Output:
User name: John
User profile: { age: 25, email: '[email protected]', settings: { theme: 'dark', notifications: true } }
Advanced Indexed Access
// Dynamic property access
type GetProperty<T, K extends keyof T> = T[K];
interface ApiResponse {
data: any[];
status: number;
message: string;
metadata: {
total: number;
page: number;
};
}
type ApiData = GetProperty<ApiResponse, 'data'>; // any[]
type ApiStatus = GetProperty<ApiResponse, 'status'>; // number
type ApiMetadata = GetProperty<ApiResponse, 'metadata'>; // { total: number; page: number; }
// Array element type extraction
type ArrayType<T> = T extends (infer U)[] ? U : never;
type StringArrayType = ArrayType<string[]>; // string
type NumberArrayType = ArrayType<number[]>; // number
// Function parameter extraction
type FirstParameter<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;
type SecondParameter<T> = T extends (first: any, second: infer P, ...rest: any[]) => any ? P : never;
function example(a: string, b: number, c: boolean): void {}
type FirstParam = FirstParameter<typeof example>; // string
type SecondParam = SecondParameter<typeof example>; // number
console.log("API data type:", [] as ApiData);
console.log("API status type:", 200 as ApiStatus);
console.log("String array type:", "hello" as StringArrayType);
console.log("First parameter type:", "test" as FirstParam);
Output:
API data type: []
API status type: 200
String array type: hello
First parameter type: test
6. Keyof and typeof Operators
Keyof Operator
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Get all property names as a union type
type UserKeys = keyof User; // "id" | "name" | "email" | "isActive"
// Generic function that works with any object property
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
const user: User = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
const userName = getProperty(user, "name"); // string
const userId = getProperty(user, "id"); // number
const userActive = getProperty(user, "isActive"); // boolean
setProperty(user, "name", "Jane");
setProperty(user, "isActive", false);
console.log("User name:", userName);
console.log("User ID:", userId);
console.log("User active:", userActive);
console.log("Updated user:", user);
Output:
User name: John
User ID: 1
User active: true
Updated user: { id: 1, name: 'Jane', email: '[email protected]', isActive: false }
Typeof Operator
// Get the type of a variable
const user = {
id: 1,
name: "John",
email: "[email protected]",
isActive: true
};
type UserType = typeof user; // { id: number; name: string; email: string; isActive: boolean; }
// Get the type of a function
function createUser(name: string, email: string) {
return {
id: Math.random(),
name,
email,
isActive: true,
createdAt: new Date()
};
}
type CreateUserFunction = typeof createUser; // (name: string, email: string) => { id: number; name: string; email: string; isActive: boolean; createdAt: Date; }
// Get the type of a class
class UserService {
private users: UserType[] = [];
addUser(user: UserType): void {
this.users.push(user);
}
getUsers(): UserType[] {
return [...this.users];
}
}
type UserServiceType = typeof UserService; // typeof UserService
type UserServiceInstance = InstanceType<typeof UserService>; // UserService
const userService = new UserService();
userService.addUser(user);
console.log("User type:", user);
console.log("Create user function type:", createUser);
console.log("User service users:", userService.getUsers());
Output:
User type: { id: 1, name: 'John', email: '[email protected]', isActive: true }
Create user function type: [Function: createUser]
User service users: [{ id: 1, name: 'John', email: '[email protected]', isActive: true }]
Practical Examples
1. API Response Handler
// Advanced types for API handling
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// Generic API handler
class ApiHandler {
static async fetch<T>(url: string): Promise<ApiResponse<T>> {
// Simulate API call
return {
data: {} as T,
status: 200,
message: "Success",
timestamp: new Date().toISOString()
};
}
static handleResponse<T>(response: ApiResponse<T>): T {
if (response.status >= 200 && response.status < 300) {
return response.data;
}
throw new Error(`API Error: ${response.message}`);
}
}
// Usage
async function fetchUser(id: number): Promise<User> {
const response = await ApiHandler.fetch<User>(`/api/users/${id}`);
return ApiHandler.handleResponse(response);
}
async function fetchProduct(id: number): Promise<Product> {
const response = await ApiHandler.fetch<Product>(`/api/products/${id}`);
return ApiHandler.handleResponse(response);
}
// Simulate API calls
fetchUser(1).then(user => {
console.log("Fetched user:", user);
});
fetchProduct(1).then(product => {
console.log("Fetched product:", product);
});
Output:
Fetched user: {}
Fetched product: {}
2. Form Builder with Advanced Types
// Advanced form builder using template literal types
type FormFieldType = 'text' | 'email' | 'number' | 'password' | 'checkbox';
type FormFieldName<T extends string> = `${T}Field`;
type FormValidatorName<T extends string> = `validate${Capitalize<T>}`;
interface FormField<T extends string> {
name: FormFieldName<T>;
type: FormFieldType;
required: boolean;
validator: FormValidatorName<T>;
}
type FormSchema<T extends string> = {
[K in T]: FormField<K>;
};
// Create form schema
type UserFormFields = 'name' | 'email' | 'age';
type UserFormSchema = FormSchema<UserFormFields>;
const userForm: UserFormSchema = {
name: {
name: 'nameField',
type: 'text',
required: true,
validator: 'validateName'
},
email: {
name: 'emailField',
type: 'email',
required: true,
validator: 'validateEmail'
},
age: {
name: 'ageField',
type: 'number',
required: false,
validator: 'validateAge'
}
};
console.log("User form schema:", userForm);
Output:
User form schema: {
name: {
name: 'nameField',
type: 'text',
required: true,
validator: 'validateName'
},
email: {
name: 'emailField',
type: 'email',
required: true,
validator: 'validateEmail'
},
age: {
name: 'ageField',
type: 'number',
required: false,
validator: 'validateAge'
}
}
Best Practices for Advanced Types
1. Use Utility Types for Common Transformations
// Good: Use built-in utility types
type PartialUser = Partial<User>;
type RequiredUser = Required<PartialUser>;
// Good: Create custom utility types for specific needs
type ApiEndpoint<T extends string> = `/api/${T}`;
type UserEndpoint = ApiEndpoint<"users">;
2. Prefer Conditional Types for Complex Logic
// Good: Use conditional types for type-level logic
type NonNullable<T> = T extends null | undefined ? never : T;
type ArrayElement<T> = T extends (infer U)[] ? U : never;
3. Use Template Literal Types for String Manipulation
// Good: Use template literal types for consistent naming
type EventHandler<T extends string> = `on${Capitalize<T>}`;
type CssVariable<T extends string> = `--${T}`;
4. Combine Types for Maximum Flexibility
// Good: Combine multiple advanced types
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
type OptionalApiResponse<T> = DeepPartial<ApiResponse<T>>;
Common Pitfalls and Solutions
1. Circular Type References
// ❌ Error: Circular reference
type Circular<T> = T extends string ? Circular<T> : never;
// ✅ Solution: Use proper base cases
type SafeCircular<T> = T extends string ? string : never;
2. Complex Conditional Types
// ❌ Too complex conditional type
type Complex<T> = T extends string ? T extends number ? never : string : never;
// ✅ Solution: Break down into simpler types
type IsString<T> = T extends string ? true : false;
type IsNumber<T> = T extends number ? true : false;
type Simple<T> = IsString<T> extends true ? string : never;
3. Performance with Large Types
// ❌ Large mapped type can be slow
type LargeMapped<T> = {
[K in keyof T]: T[K] extends object ? LargeMapped<T[K]> : T[K];
};
// ✅ Solution: Use more specific types
type SpecificMapped<T> = {
[K in keyof T]: T[K] extends { [key: string]: any } ? Partial<T[K]> : T[K];
};
Conclusion
Advanced types in TypeScript provide powerful tools for creating flexible, reusable, and type-safe code. By understanding:
- What advanced types are and their categories
- Why they're essential for complex applications
- How to use utility types, conditional types, mapped types, and template literal types
You can create sophisticated type systems that catch errors at compile time and provide excellent developer experience. These types enable building robust applications with complex data structures and dynamic behavior while maintaining type safety.
Next Steps
- Practice with utility types in real projects
- Experiment with conditional types for complex logic
- Explore template literal types for string manipulation
- Move on to Chapter 3: Classes & Interfaces
This tutorial is part of the TypeScript Mastery series by syscook.dev