Skip to main content

Module 9: Advanced Types

This module covers advanced type manipulation techniques including mapped types, conditional types, and index types.


1. Index Types

keyof Operator

Extract all keys from a type as a union.

interface Person {
name: string;
age: number;
email: string;
}

type PersonKeys = keyof Person; // "name" | "age" | "email"

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

let person: Person = { name: "Alice", age: 25, email: "alice@example.com" };
let name = getProperty(person, "name"); // string
let age = getProperty(person, "age"); // number

Indexed Access Types

type Person = {
name: string;
age: number;
address: {
street: string;
city: string;
};
};

type Name = Person["name"]; // string
type Age = Person["age"]; // number
type Address = Person["address"]; // { street: string; city: string; }
type City = Person["address"]["city"]; // string

2. Mapped Types

Transform properties of a type.

Basic Mapped Type

type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};

// Equivalent to:
type ReadonlyPerson2 = {
readonly name: string;
readonly age: number;
readonly email: string;
};

Optional Mapped Type

type PartialPerson = {
[K in keyof Person]?: Person[K];
};

// All properties become optional
let partialPerson: PartialPerson = {
name: "Alice"
// age and email are optional
};

Creating Reusable Mapped Types

type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};

type MyPartial<T> = {
[K in keyof T]?: T[K];
};

type MyRequired<T> = {
[K in keyof T]-?: T[K]; // Remove optionality
};

3. Conditional Types

Types that depend on conditions.

Basic Conditional Type

type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

Conditional Type with Union

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 T0 = TypeName<string>; // "string"
type T1 = TypeName<number>; // "number"
type T2 = TypeName<() => void>; // "function"

4. infer Keyword

Extract types from other types within conditional types.

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
return { name: "Alice", age: 25 };
}

type User = ReturnType<typeof getUser>; // { name: string; age: number; }

Extract Function Parameters

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number) {
return { name, age };
}

type CreateUserParams = Parameters<typeof createUser>; // [string, number]

Extract Array Element Type

type ArrayElement<T> = T extends (infer E)[] ? E : never;

type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string

5. Template Literal Types

Create types using template literal syntax.

type Greeting = `Hello ${string}`;

let greeting1: Greeting = "Hello World"; // ✅ OK
let greeting2: Greeting = "Hello Alice"; // ✅ OK
// let greeting3: Greeting = "Hi there"; // ❌ Error

Combining String Literals

type Color = "red" | "green" | "blue";
type Size = "small" | "medium" | "large";

type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "green-small" | "green-medium" | "green-large" |
// "blue-small" | "blue-medium" | "blue-large"

String Manipulation Types

type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;

type Loud = Uppercase<"hello">; // "HELLO"
type Quiet = Lowercase<"HELLO">; // "hello"
type Proper = Capitalize<"hello">; // "Hello"
type Informal = Uncapitalize<"Hello">; // "hello"

6. Utility Types (Built-in)

Partial<T>

Makes all properties optional.

interface User {
name: string;
age: number;
email: string;
}

type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }

function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}

Required<T>

Makes all properties required.

interface Config {
host?: string;
port?: number;
}

type RequiredConfig = Required<Config>;
// { host: string; port: number; }

Readonly<T>

Makes all properties readonly.

type ReadonlyUser = Readonly<User>;

let user: ReadonlyUser = { name: "Alice", age: 25, email: "alice@example.com" };
// user.name = "Bob"; // ❌ Error: Cannot assign to 'name'

Pick<T, K>

Select specific properties.

type UserPreview = Pick<User, "name" | "email">;
// { name: string; email: string; }

Omit<T, K>

Exclude specific properties.

type UserWithoutEmail = Omit<User, "email">;
// { name: string; age: number; }

Record<K, T>

Create an object type with specific keys and value type.

type Roles = "admin" | "user" | "guest";
type RolePermissions = Record<Roles, string[]>;

let permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"]
};

Exclude<T, U>

Remove types from a union.

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<string | number | boolean, boolean>; // string | number

Extract<T, U>

Extract types from a union.

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | boolean, boolean>; // boolean

NonNullable<T>

Remove null and undefined.

type T0 = NonNullable<string | number | null | undefined>; // string | number

7. Recursive Types

Types that reference themselves.

interface TreeNode<T> {
value: T;
children?: TreeNode<T>[];
}

let tree: TreeNode<number> = {
value: 1,
children: [
{ value: 2 },
{
value: 3,
children: [
{ value: 4 },
{ value: 5 }
]
}
]
};

JSON Type

type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };

let data: JSONValue = {
name: "Alice",
age: 25,
hobbies: ["reading", "coding"],
address: {
city: "New York"
}
};

8. Type Branding

Create distinct types from the same underlying type.

type Brand<K, T> = K & { __brand: T };

type USD = Brand<number, "USD">;
type EUR = Brand<number, "EUR">;

function createUSD(amount: number): USD {
return amount as USD;
}

function createEUR(amount: number): EUR {
return amount as EUR;
}

let dollars: USD = createUSD(100);
let euros: EUR = createEUR(100);

// dollars = euros; // ❌ Error: Type 'EUR' is not assignable to type 'USD'

9. Advanced Conditional Type Patterns

Flatten Type

type Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

Awaited Type

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type Response = Awaited<Promise<string>>; // string
type NestedResponse = Awaited<Promise<Promise<number>>>; // number

10. Real-World Example: API Client

interface APIEndpoint {
"/users": {
GET: { response: User[] };
POST: { body: Omit<User, "id">; response: User };
};
"/users/:id": {
GET: { response: User };
PUT: { body: Partial<User>; response: User };
DELETE: { response: void };
};
}

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";

type EndpointConfig<
Path extends keyof APIEndpoint,
Method extends keyof APIEndpoint[Path]
> = APIEndpoint[Path][Method];

function apiCall<
Path extends keyof APIEndpoint,
Method extends keyof APIEndpoint[Path] & HTTPMethod
>(
path: Path,
method: Method,
config?: EndpointConfig<Path, Method>
): Promise<EndpointConfig<Path, Method> extends { response: infer R } ? R : never> {
// Implementation
return null as any;
}

// Usage with full type safety
apiCall("/users", "GET"); // Promise<User[]>
apiCall("/users", "POST", { body: { name: "Alice", age: 25, email: "alice@example.com" } }); // Promise<User>

Key Takeaways

keyof extracts object keys as unions
Mapped types transform existing types
Conditional types create type logic
infer extracts types in conditionals
Template literals create string-based types
Utility types provide common transformations
Recursive types handle nested structures


Practice Exercises

Exercise 1: DeepReadonly

type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Exercise 2: Function Property Names

type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

interface Example {
name: string;
age: number;
greet(): void;
sayBye(): void;
}

type FuncProps = FunctionPropertyNames<Example>; // "greet" | "sayBye"

Exercise 3: Promise Unwrap

Create a type that unwraps nested promises.


Next Steps

In Module 10, we'll explore Modules and Namespaces to organize and structure large TypeScript applications.