Advanced TypeScript Types
Mapped types, conditional types, template literals — TypeScript type magic
Introduction
In this lesson, you'll learn about advanced typescript types in TypeScript. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Java, you're familiar with mapped types, conditional types, template literals — typescript type magic.
TypeScript has its own approach to mapped types, conditional types, template literals — typescript type magic, which we'll explore step by step.
The TypeScript Way
Let's see how TypeScript handles this concept. Here's a typical example:
// Mapped types — transform all properties of T
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
// Utility types (built-in)
type UpdateUserDto = Partial<User>; // all props optional
type UserKeys = keyof User; // "name" | "email" | "age"
type PickedUser = Pick<User, "name" | "email">;
type OmitId = Omit<User, "id">;
// Template literal types
type EventName = "click" | "focus";
type Handler = `on${Capitalize<EventName>}`; // "onClick"|"onFocus"
// Discriminated unions (like sealed classes)
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
return s.kind === "circle"
? Math.PI * s.radius ** 2
: s.w * s.h;
}
// satisfies operator
const config = {
port: 8080, debug: true
} satisfies Record<string, number | boolean>;
// config.port is still number (not widened to number|boolean)Comparing to Java
Here's how you might have written similar code in Java:
// Java: generics with bounds
public <T extends Comparable<T>> T max(List<T> list) {
return Collections.max(list);
}
// Wildcard types
void printAll(List<? extends Number> list) {
list.forEach(System.out::println);
}
// No mapped types — each variant needs a new class
public class UpdateUserDto {
private String name; // optional
private String email; // optional
// Java: explicit optional fields or @Nullable
}
// Records for immutable data
record Point(double x, double y) {}
// Sealed hierarchy
sealed interface Shape permits Circle, Rect {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}You may be used to different syntax or behavior.
Mapped types ([K in keyof T]) transform entire object types — no Java equivalent
You may be used to different syntax or behavior.
Built-in utility types: Partial<T>, Required, Readonly, Pick, Omit, Record
You may be used to different syntax or behavior.
Conditional types (T extends X ? A : B) work at compile time — not runtime
You may be used to different syntax or behavior.
Template literal types create string patterns like `on${Capitalize<E>}`
You may be used to different syntax or behavior.
Discriminated unions (kind field) replaces sealed classes for type-safe branching
Step-by-Step Breakdown
1. Utility Types
TypeScript's built-in utility types transform existing types. Partial<T> makes all fields optional — common for update DTOs.
// Java: create UpdateDto manually with nullable fields
record UpdateDto(String name, String email) {}type UpdateDto = Partial<User>; // all fields optional
type ReadonlyUser = Readonly<User>;
type NameAndEmail = Pick<User, "name" | "email">;
type NoId = Omit<User, "id">;2. Mapped Types
Build custom transformations with [K in keyof T]. This is how all the built-in utility types are implemented.
// Java: no equivalent — each variant is a new class// Make all props nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Add prefix to keys
type Prefixed<T> = { [`get_${string & keyof T}`]: T[keyof T] };3. Conditional Types
T extends X ? A : B is evaluated at compile time. Useful for extracting types from function signatures or array elements.
// Java: Class<T> getReturnType(Method m) — runtime reflection// Extract return type at compile time
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type R = ReturnType<typeof fetch>; // Promise<Response>4. Discriminated Unions
A literal 'kind' field makes TypeScript narrow the type inside an if/switch — equivalent to Java's sealed class + pattern matching.
sealed interface Shape permits Circle, Rect {}
switch(shape) { case Circle c -> c.r(); }type Shape = { kind:"circle"; r:number } | { kind:"rect"; w:number; h:number };
if (s.kind === "circle") {
s.r; // TypeScript knows it's circle here
}Common Mistakes
When coming from Java, developers often make these mistakes:
- Mapped types ([K in keyof T]) transform entire object types — no Java equivalent
- Built-in utility types: Partial<T>, Required, Readonly, Pick, Omit, Record
- Conditional types (T extends X ? A : B) work at compile time — not runtime
Key Takeaways
- Utility types: Partial<T>, Required, Readonly, Pick, Omit — transform types without new class declarations
- Mapped types ([K in keyof T]) build custom transforms — basis for all utility types
- Conditional types (T extends X ? A : B) work at compile time with 'infer' for extraction
- Discriminated unions + literal 'kind' field = TypeScript's sealed class + pattern matching