Advanced Types
Advanced Types in TypeScript
TypeScript offers a rich set of advanced type features that allow you to express complex type relationships and create highly type-safe code. In this lesson, we'll explore advanced type operators and patterns that will take your TypeScript skills to the next level.
The keyof Operator
The keyof operator takes an object type and produces a string or numeric literal union of its keys. This is incredibly useful for creating type-safe property access patterns.
<interface Person {
name: string;
age: number;
email: string;
}
// Create a union type of all keys
type PersonKeys = keyof Person;
// Result: 'name' | 'age' | 'email'
// Practical example: type-safe property getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person: Person = {
name: 'John Doe',
age: 30,
email: 'john@example.com'
};
const name = getProperty(person, 'name'); // Type: string
const age = getProperty(person, 'age'); // Type: number
// Error: Argument of type 'invalid' is not assignable
// const invalid = getProperty(person, 'invalid');
>
keyof is essential for creating type-safe functions that work with object properties dynamically.
The typeof Operator
The typeof operator gets the type of a value. This is particularly useful when you want to reference the type of an existing value without explicitly defining a separate type.
<// Get type from a value
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
headers: {
'Content-Type': 'application/json'
}
};
type Config = typeof config;
// Result: {
// apiUrl: string;
// timeout: number;
// retries: number;
// headers: { 'Content-Type': string; };
// }
// Get function type
function createUser(name: string, email: string) {
return { name, email, id: Math.random() };
}
type CreateUserFn = typeof createUser;
// Result: (name: string, email: string) => { name: string; email: string; id: number; }
// Combine with ReturnType
type User = ReturnType<typeof createUser>;
// Result: { name: string; email: string; id: number; }
// Get enum values
const Direction = {
Up: 'UP',
Down: 'DOWN',
Left: 'LEFT',
Right: 'RIGHT'
} as const;
type DirectionType = typeof Direction;
type DirectionValue = typeof Direction[keyof typeof Direction];
// Result: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'
>
Indexed Access Types
Indexed access types allow you to look up specific properties on another type using bracket notation. This is useful for extracting the type of nested properties.
<interface Company {
name: string;
address: {
street: string;
city: string;
country: string;
coordinates: {
lat: number;
lng: number;
};
};
employees: Array<{
name: string;
role: string;
salary: number;
}>;
}
// Access nested property types
type Address = Company['address'];
// Result: { street: string; city: string; country: string; coordinates: {...}; }
type Coordinates = Company['address']['coordinates'];
// Result: { lat: number; lng: number; }
type EmployeeArray = Company['employees'];
// Result: Array<{ name: string; role: string; salary: number; }>
// Access array element type
type Employee = Company['employees'][number];
// Result: { name: string; role: string; salary: number; }
// Access specific property of array element
type EmployeeRole = Company['employees'][number]['role'];
// Result: string
// Multiple indexed access
type CityOrCountry = Company['address']['city' | 'country'];
// Result: string
>
[number] to access the element type of an array type.
Template Literal Types
Template literal types build on string literal types and can expand into many strings via unions. They allow you to create new string types based on existing ones.
<// Basic template literal type
type World = 'world';
type Greeting = `hello ${World}`;
// Result: 'hello world'
// Create API endpoint types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'user' | 'post' | 'comment';
type Endpoint = `/${Lowercase<Resource>}`;
// Result: '/user' | '/post' | '/comment'
type ApiRoute = `${HttpMethod} ${Endpoint}`;
// Result: 'GET /user' | 'GET /post' | ... (12 combinations)
// Event names
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = `on${Capitalize<EventName>}`;
// Result: 'onClick' | 'onFocus' | 'onBlur'
// CSS properties
type CSSProperty = 'color' | 'font-size' | 'border-width';
type CSSValue = string | number;
type StyleObject = {
[K in CSSProperty]: CSSValue;
};
// Advanced: Create getter/setter types
type PropName = 'name' | 'age' | 'email';
type Getters = {
[K in PropName as `get${Capitalize<K>}`]: () => string;
};
// Result: { getName: () => string; getAge: () => string; getEmail: () => string; }
type Setters = {
[K in PropName as `set${Capitalize<K>}`]: (value: string) => void;
};
>
Intrinsic String Manipulation Types
TypeScript provides built-in types for common string manipulations: Uppercase, Lowercase, Capitalize, and Uncapitalize.
<type Greeting = 'hello world';
// Transform to uppercase
type LoudGreeting = Uppercase<Greeting>;
// Result: 'HELLO WORLD'
// Transform to lowercase
type QuietGreeting = Lowercase<LoudGreeting>;
// Result: 'hello world'
// Capitalize first letter
type CapitalizedGreeting = Capitalize<Greeting>;
// Result: 'Hello world'
// Uncapitalize first letter
type UncapitalizedGreeting = Uncapitalize<CapitalizedGreeting>;
// Result: 'hello world'
// Practical example: HTTP headers
type HttpHeader = 'content-type' | 'authorization' | 'accept';
type HttpHeaderCapitalized = Capitalize<HttpHeader>;
// Result: 'Content-type' | 'Authorization' | 'Accept'
// Create constants
type StatusCode = 'ok' | 'error' | 'pending';
type StatusConstant = `STATUS_${Uppercase<StatusCode>}`;
// Result: 'STATUS_OK' | 'STATUS_ERROR' | 'STATUS_PENDING'
>
Recursive Types
Recursive types reference themselves in their definition. This is powerful for modeling nested or tree-like structures.
<// Simple recursive type: JSON
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const validJSON: JSONValue = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
address: {
city: 'New York',
coordinates: {
lat: 40.7128,
lng: -74.0060
}
}
};
// File system tree
interface FileSystemNode {
name: string;
type: 'file' | 'directory';
children?: FileSystemNode[];
}
const fileSystem: FileSystemNode = {
name: 'root',
type: 'directory',
children: [
{
name: 'src',
type: 'directory',
children: [
{ name: 'index.ts', type: 'file' },
{ name: 'utils.ts', type: 'file' }
]
},
{ name: 'package.json', type: 'file' }
]
};
// Deeply nested object paths
type PathImpl<T, Key extends keyof T> =
Key extends string
? T[Key] extends Record<string, any>
? `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never;
type Path<T> = PathImpl<T, keyof T> | keyof T;
interface DeepObject {
user: {
profile: {
name: string;
settings: {
theme: string;
};
};
};
}
type DeepPaths = Path<DeepObject>;
// Result: 'user' | 'user.profile' | 'user.profile.name' | 'user.profile.settings' | ...
>
Mapped Types with Key Remapping
Key remapping allows you to transform keys in mapped types using the as clause. This enables sophisticated type transformations.
<interface Person {
name: string;
age: number;
email: string;
}
// Create getters with key remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type PersonGetters = Getters<Person>;
// Result: {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
// Filter properties by type
type FilterByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
type StringPropsOnly = FilterByType<Person, string>;
// Result: { name: string; email: string; }
type NumberPropsOnly = FilterByType<Person, number>;
// Result: { age: number; }
// Remove specific prefixes
type RemovePrefix<T, Prefix extends string> = {
[K in keyof T as K extends `${Prefix}${infer Rest}` ? Rest : K]: T[K];
};
interface PrefixedData {
data_name: string;
data_age: number;
email: string;
}
type CleanedData = RemovePrefix<PrefixedData, 'data_'>;
// Result: { name: string; age: number; email: string; }
>
Conditional Types with Inference
Conditional types can infer types within the condition using the infer keyword. This enables powerful type extraction patterns.
<// Extract return type
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'John' };
}
type UserType = GetReturnType<typeof getUser>;
// Result: { id: number; name: string; }
// Extract array element type
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Numbers = ArrayElement<number[]>;
// Result: number
type Users = ArrayElement<Array<{ id: number; name: string }>>;
// Result: { id: number; name: string; }
// Extract Promise value type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type AsyncResult = UnwrapPromise<Promise<string>>;
// Result: string
type SyncResult = UnwrapPromise<number>;
// Result: number
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function processUser(id: number, name: string, active: boolean) {
// ...
}
type ProcessUserParams = Parameters<typeof processUser>;
// Result: [id: number, name: string, active: boolean]
// Extract first parameter
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
type FirstParam = FirstParameter<typeof processUser>;
// Result: number
>
infer keyword can only be used within the extends clause of a conditional type.
Distributive Conditional Types
When conditional types act on a generic type, they become distributive when given a union type. This means the conditional type is applied to each member of the union.
<// Simple distributive example
type ToArray<T> = T extends any ? T[] : never;
type StringOrNumberArray = ToArray<string | number>;
// Result: string[] | number[] (not (string | number)[])
// Filter null from union
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Result: string
// Extract types matching a pattern
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = string | number | boolean | 'specific';
type OnlyStrings = ExtractStrings<Mixed>;
// Result: string | 'specific'
// Prevent distribution with tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type CombinedArray = ToArrayNonDist<string | number>;
// Result: (string | number)[]
>
Practical Example: Type-Safe Event Emitter
<// Define event map
interface EventMap {
'user:login': { userId: number; timestamp: Date };
'user:logout': { userId: number };
'data:update': { id: string; data: any };
'error': { message: string; code: number };
}
// Extract event names
type EventName = keyof EventMap;
// Create type-safe event emitter
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(payload: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(listener);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
const eventListeners = this.listeners[event];
if (eventListeners) {
eventListeners.forEach(listener => listener(payload));
}
}
off<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
const eventListeners = this.listeners[event];
if (eventListeners) {
this.listeners[event] = eventListeners.filter(l => l !== listener) as any;
}
}
}
// Usage
const emitter = new TypedEventEmitter<EventMap>();
// Type-safe: payload is inferred correctly
emitter.on('user:login', (payload) => {
console.log(`User ${payload.userId} logged in at ${payload.timestamp}`);
});
// Type-safe: must provide correct payload
emitter.emit('user:login', {
userId: 123,
timestamp: new Date()
});
// Error: Wrong payload type
// emitter.emit('user:login', { userId: '123' }); // Error: string not assignable to number
// Error: Unknown event
// emitter.on('invalid:event', () => {}); // Error: invalid:event not in EventMap
>
- Takes an interface with various property types
- Creates a new type with only the string properties
- Transforms each property name to have a 'validate' prefix
- Changes each property type to a validation function returning boolean