Skip to main content

Advanced Types

Introduction​

TypeScript offers powerful advanced type features that go beyond basic types. These features allow you to create sophisticated type systems that catch more bugs at compile time and make your code more expressive and maintainable.

In this final chapter, we'll explore TypeScript's most advanced type features that will elevate your TypeScript skills to the next level.


Concepts​

Mapped Types​

Transform properties of an existing type:

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

type Person = {
name: string;
age: number;
};

type ReadonlyPerson = Readonly<Person>;
// Result: { readonly name: string; readonly age: number; }

const person: ReadonlyPerson = { name: "Alice", age: 30 };
// person.age = 31; // ❌ Error: Cannot assign to 'age' because it is read-only

Built-in Mapped Types:

// Partial - makes all properties optional
type PartialPerson = Partial<Person>;
// { name?: string; age?: number; }

// Required - makes all properties required
type RequiredPerson = Required<Partial<Person>>;
// { name: string; age: number; }

// Pick - select specific properties
type PersonName = Pick<Person, 'name'>;
// { name: string; }

// Omit - exclude specific properties
type PersonWithoutAge = Omit<Person, 'age'>;
// { name: string; }

// Record - create object type with specific keys
type Scores = Record<string, number>;
// { [x: string]: number; }

Conditional Types​

Types that depend on conditions:

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

type A = IsString<string>; // true
type B = IsString<number>; // false

// Practical example
type NonNullable<T> = T extends null | undefined ? never : T;

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

Extract and Exclude:

type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "f">; // "a"

// Practical use
type EventType = "click" | "scroll" | "mousemove";
type ClickEvent = Extract<EventType, "click">; // "click"

Template Literal Types​

Create types using string templates:

type World = "world";
type Greeting = `hello ${World}`; // "hello world"

// Practical example
type Color = "red" | "green" | "blue";
type Shade = "light" | "dark";

type ColorShade = `${Shade}-${Color}`;
// "light-red" | "light-green" | "light-blue" |
// "dark-red" | "dark-green" | "dark-blue"

// Event names
type EventName = "click" | "scroll";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onScroll"

Utility Types​

TypeScript provides many built-in utility types:

interface User {
id: number;
name: string;
email: string;
password: string;
}

// Partial - all properties optional
type PartialUser = Partial<User>;

// Required - all properties required
type RequiredUser = Required<Partial<User>>;

// Readonly - all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick - select specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

// Omit - exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;

// Record - object with specific keys and value type
type UserMap = Record<number, User>;

// ReturnType - extract function return type
function getUser(): User {
return { id: 1, name: "Alice", email: "alice@example.com", password: "hash" };
}
type UserType = ReturnType<typeof getUser>; // User

// Parameters - extract function parameter types
function updateUser(id: number, updates: Partial<User>): void {}
type UpdateParams = Parameters<typeof updateUser>; // [number, Partial<User>]

Index Access Types​

Access property types:

interface Person {
name: string;
age: number;
address: {
city: string;
country: string;
};
}

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

// Works with unions
type NameOrAge = Person['name' | 'age']; // string | number

Type Inference with infer​

Extract types from within conditional types:

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

type Func = () => string;
type FuncReturn = ReturnType<Func>; // string

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

type NumArray = number[];
type Element = ArrayElement<NumArray>; // number

// Extract promise type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type P1 = UnwrapPromise<Promise<string>>; // string
type P2 = UnwrapPromise<number>; // number

Examples​

Form Field Types​

interface BaseField {
id: string;
label: string;
required: boolean;
}

interface TextField extends BaseField {
type: 'text';
placeholder?: string;
maxLength?: number;
}

interface NumberField extends BaseField {
type: 'number';
min?: number;
max?: number;
}

interface SelectField extends BaseField {
type: 'select';
options: string[];
}

type FormField = TextField | NumberField | SelectField;

// Extract field by type
type FieldByType<T extends FormField['type']> = Extract<
FormField,
{ type: T }
>;

type TextFieldType = FieldByType<'text'>; // TextField
type NumberFieldType = FieldByType<'number'>; // NumberField

// Make all fields optional
type OptionalFormField = {
[K in keyof FormField]?: FormField[K];
};

// Create field values type
type FieldValue<T extends FormField> =
T extends TextField ? string :
T extends NumberField ? number :
T extends SelectField ? string :
never;

// Form data type
type FormData = {
[K in FormField['id']]: FieldValue<Extract<FormField, { id: K }>>;
};

API Response Builder​

interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}

// Type-safe response creator
class ResponseBuilder {
static success<T>(data: T): ApiResponse<T> {
return { success: true, data };
}

static error<T = never>(error: string): ApiResponse<T> {
return { success: false, error };
}

static isSuccess<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { success: true; data: T } {
return response.success && response.data !== undefined;
}

static isError<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { success: false; error: string } {
return !response.success && response.error !== undefined;
}
}

// Usage
interface User {
id: number;
name: string;
}

const successResponse = ResponseBuilder.success<User>({
id: 1,
name: "Alice"
});

const errorResponse = ResponseBuilder.error("User not found");

if (ResponseBuilder.isSuccess(successResponse)) {
console.log(successResponse.data.name); // TypeScript knows data exists
}

Type-Safe Event Emitter​

type EventMap = Record<string, any>;

type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T) => void;

class TypedEventEmitter<T extends EventMap> {
private listeners: {
[K in keyof T]?: Array<EventReceiver<T[K]>>;
} = {};

on<K extends EventKey<T>>(
eventName: K,
fn: EventReceiver<T[K]>
): void {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName]!.push(fn);
}

off<K extends EventKey<T>>(
eventName: K,
fn: EventReceiver<T[K]>
): void {
const listeners = this.listeners[eventName];
if (listeners) {
const index = listeners.indexOf(fn);
if (index > -1) {
listeners.splice(index, 1);
}
}
}

emit<K extends EventKey<T>>(
eventName: K,
params: T[K]
): void {
const listeners = this.listeners[eventName];
if (listeners) {
listeners.forEach(fn => fn(params));
}
}
}

// Define event types
interface AppEvents {
login: { username: string; timestamp: Date };
logout: { username: string };
error: { message: string; code: number };
}

// Usage
const emitter = new TypedEventEmitter<AppEvents>();

emitter.on('login', (data) => {
// TypeScript knows data is { username: string; timestamp: Date }
console.log(`${data.username} logged in at ${data.timestamp}`);
});

emitter.emit('login', {
username: 'Alice',
timestamp: new Date()
});

// TypeScript error - wrong data type
// emitter.emit('login', { username: 123 });

Challenges​

1. DeepReadonly Type​

  • Create type that makes nested objects readonly
  • Use recursive mapped types
  • Test with nested objects
💡 Show Answer
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};

interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
endpoint: string;
};
}

type ReadonlyConfig = DeepReadonly<Config>;

const config: ReadonlyConfig = {
database: {
host: 'localhost',
port: 5432,
credentials: {
username: 'admin',
password: 'secret'
}
},
api: {
endpoint: 'https://api.example.com'
}
};

// All these would be errors:
// config.database.host = 'newhost';
// config.database.credentials.password = 'newpass';
// config.api.endpoint = 'https://new.com';

2. Function Property Names​

  • Extract names of function properties
  • Use mapped and conditional types
  • Test with various interfaces
💡 Show Answer
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

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

interface Person {
name: string;
age: number;
greet: () => void;
celebrate: () => void;
email: string;
}

type PersonMethods = FunctionPropertyNames<Person>;
// "greet" | "celebrate"

type PersonData = NonFunctionPropertyNames<Person>;
// "name" | "age" | "email"

// Extract only methods
type MethodsOnly<T> = Pick<T, FunctionPropertyNames<T>>;
type PersonMethodsOnly = MethodsOnly<Person>;
// { greet: () => void; celebrate: () => void; }

// Extract only data
type DataOnly<T> = Pick<T, NonFunctionPropertyNames<T>>;
type PersonDataOnly = DataOnly<Person>;
// { name: string; age: number; email: string; }

3. Flatten Array Type​

  • Extract element type from nested arrays
  • Handle single and multiple nesting
  • Use conditional types with infer
💡 Show Answer
type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<number[]>; // number
type B = Flatten<string[][]>; // string[]
type C = Flatten<boolean>; // boolean

// Deep flatten
type DeepFlatten<T> = T extends Array<infer U>
? DeepFlatten<U>
: T;

type D = DeepFlatten<number[]>; // number
type E = DeepFlatten<string[][][]>; // string
type F = DeepFlatten<boolean>; // boolean

// Usage
function flattenArray<T>(arr: T[]): Flatten<T[]>[] {
return arr.flat() as Flatten<T[]>[];
}

const nested = [[1, 2], [3, 4]];
const flattened = flattenArray(nested); // number[]

4. Required Keys​

  • Extract required (non-optional) property names
  • Use mapped and conditional types
  • Test with mixed optional/required
💡 Show Answer
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];

interface User {
id: number;
name: string;
email?: string;
phone?: string;
age: number;
}

type UserRequiredKeys = RequiredKeys<User>;
// "id" | "name" | "age"

type UserOptionalKeys = OptionalKeys<User>;
// "email" | "phone"

// Create types with only required or optional fields
type OnlyRequired<T> = Pick<T, RequiredKeys<T>>;
type OnlyOptional<T> = Pick<T, OptionalKeys<T>>;

type UserRequired = OnlyRequired<User>;
// { id: number; name: string; age: number; }

type UserOptional = OnlyOptional<User>;
// { email?: string; phone?: string; }

5. Promise Return Type​

  • Extract resolved type from Promise
  • Handle nested promises
  • Use conditional types with infer
💡 Show Answer
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<Promise<Promise<Promise<boolean>>>>; // boolean
type D = Awaited<string>; // string

// Practical usage
async function fetchUser(): Promise<{ name: string; age: number }> {
return { name: "Alice", age: 30 };
}

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

// Helper to unwrap promise types
function unwrapPromise<T>(promise: Promise<T>): Awaited<T> {
return promise as any; // Type assertion for demo
}

Summary​

Congratulations! You've completed the TypeScript Essentials course! 🎉

In this final chapter, you learned:

✅ Mapped types for transforming types
✅ Conditional types for type logic
✅ Template literal types
✅ Built-in utility types
✅ Index access types
✅ Type inference with infer
✅ Advanced type patterns

Course Review:

Throughout this course, you've learned:

  1. ✅ TypeScript fundamentals and type system
  2. ✅ Variables, operators, and functions
  3. ✅ Objects, JSON, and data structures
  4. ✅ Classes and object-oriented programming
  5. ✅ Static members and access modifiers
  6. ✅ Encapsulation and information hiding
  7. ✅ Callbacks and asynchronous patterns
  8. ✅ Promises and async/await
  9. ✅ Generics for reusable code
  10. ✅ Union and intersection types
  11. ✅ Type guards and narrowing
  12. ✅ Error handling and custom errors
  13. ✅ Testing with Jest
  14. ✅ Advanced type system features

Next Steps:

Now that you've mastered TypeScript essentials, consider:

  1. Build Real Projects

    • Create a REST API with Express and TypeScript
    • Build a React app with TypeScript
    • Develop a CLI tool with TypeScript
  2. Explore Frameworks

    • React with TypeScript
    • Angular (built with TypeScript)
    • NestJS for backend development
    • Next.js for full-stack apps
  3. Advanced Topics

    • Decorators and metadata
    • Module augmentation
    • Declaration files (.d.ts)
    • TypeScript compiler API
    • Performance optimization
  4. Best Practices

    • Strict mode configuration
    • Code organization patterns
    • Type-driven development
    • Testing strategies
  5. Community

    • Contribute to open-source TypeScript projects
    • Read the TypeScript handbook
    • Follow TypeScript releases and updates
    • Join TypeScript communities

Key Takeaways:

  • TypeScript adds type safety to JavaScript
  • Types catch bugs before runtime
  • TypeScript improves developer experience
  • Strong typing enables better refactoring
  • TypeScript scales well for large projects
  • The type system is powerful and flexible

Final Tips:

  • Start with strict mode for new projects
  • Use type inference when possible
  • Create reusable types and interfaces
  • Leverage utility types
  • Write tests for type-critical code
  • Keep learning and experimenting

Thank you for completing TypeScript Essentials! Keep coding and building amazing things with TypeScript! 🚀