TypeScript Advanced Type System
1. Type Aliases (type) vs Interface Declarations
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| Type Alias | type Name = Type |
Creates named reference to any type - unions, intersections, primitives | Complex unions, utility compositions |
| Interface | interface Name { } |
Defines object shape - extendable and mergeable | Object contracts, class implementations |
| Interface Extension | extends Interface |
Inherit properties from parent interfaces | Hierarchical type relationships |
| Type Intersection | Type1 & Type2 |
Combine multiple type aliases | Mixins, composed types |
| Declaration Merging | Multiple interface declarations |
Interfaces with same name automatically merge - not available for types | Augmenting libraries, modules |
Example: Type aliases vs interfaces
// Type alias - flexible for any type
type StringOrNumber = string | number;
type Coordinate = [number, number];
type UserID = string;
// Interface - best for object shapes
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: string;
department: string;
}
// Declaration merging (interfaces only)
interface Window {
customProperty: string;
}
interface Window {
anotherProperty: number;
}
// Window now has both properties
// Type composition
type WithTimestamp = { timestamp: Date };
type TrackedPerson = Person & WithTimestamp;
2. Enum Types and Const Enums
| Type | Syntax | Description | Compiled Output |
|---|---|---|---|
| Numeric Enum | enum Name { A, B } |
Auto-incrementing numeric values starting from 0 | Object with reverse mapping |
| String Enum | enum { A = "a" } |
Explicit string values - no auto-increment | Object without reverse mapping |
| Heterogeneous Enum | enum { A = 1, B = "b" } |
Mix of numeric and string values - not recommended | Mixed object |
| Const Enum | const enum Name { } |
Completely inlined at compile time - no runtime object | Values inlined, no object |
| Computed Member | A = expression |
Value computed from expression | Evaluated at compile time |
| Ambient Enum | declare enum Name { } |
Describe existing enum from external source | Type-only, no JS output |
Example: Enum types
// Numeric enum
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
// String enum (preferred)
enum Status {
Pending = "PENDING",
Active = "ACTIVE",
Completed = "COMPLETED"
}
// Custom numeric values
enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500
}
// Const enum (performance optimization)
const enum Colors {
Red = "#FF0000",
Green = "#00FF00",
Blue = "#0000FF"
}
// Usage
let status: Status = Status.Active;
let color = Colors.Red; // Inlined to "#FF0000" at compile time
Note: Prefer string enums or union of
string literals over numeric enums for better debugging and runtime safety. Use
const enum
for zero-runtime overhead.
3. Type Assertions (as keyword and angle-bracket syntax)
| Syntax | Form | Description | Use Case |
|---|---|---|---|
| as Syntax | value as Type |
Preferred modern syntax - works in JSX | All scenarios, especially TSX/JSX files |
| Angle Bracket | <Type>value |
Original syntax - conflicts with JSX | Non-JSX files only |
| Double Assertion | x as unknown as T |
Force assertion through unknown - escape hatch | Incompatible types, migrations |
| Const Assertion TS 3.4+ | as const |
Make literal types readonly and narrow to literal | Immutable data, exact types |
| Non-null Assertion | value! |
Assert value is not null/undefined | When you know value exists |
Example: Type assertions
// as syntax (preferred)
let input = document.getElementById("input") as HTMLInputElement;
input.value = "Hello";
// Angle bracket syntax (avoid in JSX)
let element = <HTMLElement>document.querySelector(".item");
// Double assertion (escape hatch)
let num = "123" as unknown as number; // Dangerous!
// Const assertion - deep readonly
let config = {
host: "localhost",
port: 3000,
flags: ["debug", "verbose"]
} as const;
// Type: { readonly host: "localhost"; readonly port: 3000; readonly flags: readonly ["debug", "verbose"] }
// Array as const
let arr = [1, 2, 3] as const; // Type: readonly [1, 2, 3]
// Non-null assertion
function process(value: string | null) {
let length = value!.length; // Assert value is not null
}
Warning: Type assertions bypass type checking - use sparingly. Prefer type guards and
narrowing.
as const is safe and recommended.
4. Type Guards and Type Narrowing Techniques
| Technique | Syntax | Description | Narrows To |
|---|---|---|---|
| typeof Guard | typeof x === "type" |
Check primitive types at runtime | string, number, boolean, etc. |
| instanceof Guard | x instanceof Class |
Check if object is instance of class | Class type |
| in Operator | "prop" in obj |
Check property existence | Type with that property |
| Truthiness Check | if (value) { } |
Filters out null, undefined, 0, "", false | Non-nullable type |
| Equality Check | x === value |
Compare with specific value | Literal type |
| User-defined Guard | is Type |
Custom type predicate function | Custom type |
| Discriminated Union | switch (x.kind) |
Use discriminant property to narrow | Specific union member |
Example: Built-in type guards
function process(value: string | number) {
// typeof guard
if (typeof value === "string") {
console.log(value.toUpperCase()); // string
} else {
console.log(value.toFixed(2)); // number
}
}
// instanceof guard
function handle(x: Date | string) {
if (x instanceof Date) {
console.log(x.getFullYear()); // Date
} else {
console.log(x.length); // string
}
}
// in operator
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Truthiness narrowing
function print(text: string | null) {
if (text) {
console.log(text.toUpperCase()); // string
}
}
Example: User-defined type guards
// Type predicate function
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase()); // Narrowed to string
}
}
// Complex type guard
interface Cat { meow: () => void; }
interface Dog { bark: () => void; }
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function speak(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
}
5. Discriminated Unions and Tagged Unions
| Component | Description | Example |
|---|---|---|
| Discriminant Property | Common property with literal type - acts as type tag | kind: "success" | "error" |
| Union Members | Each type in union has unique discriminant value | Success | Error |
| Exhaustiveness Check | Ensure all union cases are handled | default: assertNever(x) |
Example: Discriminated unions for safe state management
// Define discriminated union
type Result<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function handle<T>(result: Result<T>) {
switch (result.status) {
case "loading":
console.log("Loading...");
break;
case "success":
console.log(result.data); // Type: T
break;
case "error":
console.log(result.error.message); // Type: Error
break;
}
}
// Shape discriminated union
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
// Exhaustiveness check
function assertNever(x: never): never {
throw new Error("Unexpected: " + x);
}
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
default: return assertNever(shape); // Compile error if case missing
}
}
6. Mapped Types and Key Remapping
| Pattern | Syntax | Description | Result |
|---|---|---|---|
| Basic Mapped Type | { [K in Keys]: Type } |
Iterate over keys and map to type | New object type with mapped properties |
| Readonly Mapping | { readonly [K in Keys] } |
Make all properties readonly | Readonly<T> |
| Optional Mapping | { [K in Keys]?: Type } |
Make all properties optional | Partial<T> |
| Remove Modifiers | -readonly, -? |
Remove readonly or optional modifiers | Required<T> |
| Key Remapping TS 4.1+ | as NewKey |
Transform key names during mapping | Renamed property keys |
| Filter Keys | as K extends X ? K : never |
Conditionally include/exclude keys | Filtered property set |
Example: Mapped types
// Basic mapped type
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type User = { name: string; age: number };
type NullableUser = Nullable<User>;
// Result: { name: string | null; age: number | null }
// Add/remove modifiers
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
// Key remapping with template literals
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// Result: { getName: () => string; getAge: () => number }
// Filter properties by type
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type Data = { id: number; name: string; email: string; count: number };
type StringProps = StringKeys<Data>;
// Result: { name: string; email: string }
7. Conditional Types and Built-in Utility Types
| Pattern | Syntax | Description | Use Case |
|---|---|---|---|
| Conditional Type | T extends U ? X : Y |
Type-level if-else - select type based on condition | Dynamic type selection |
| infer Keyword TS 2.8+ | infer R |
Extract type from another type within conditional | Return types, array elements |
| Distributive | T extends U when T is union |
Conditional applied to each union member separately | Union transformations |
| Non-Distributive | [T] extends [U] |
Conditional applied to whole type, not distributed | Exact type matching |
Example: Conditional types
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Extract return type with infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Func = () => string;
type Result = ReturnType<Func>; // string
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type Items = ElementType<number[]>; // number
// Distributive conditional (union)
type ToArray<T> = T extends any ? T[] : never;
type Arrays = ToArray<string | number>;
// Result: string[] | number[] (distributed)
// Non-distributive (tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type TupleArray = ToArrayNonDist<string | number>;
// Result: (string | number)[] (not distributed)
// Complex conditional - function overloads
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Value1 = UnwrapPromise<Promise<string>>; // string
type Value2 = UnwrapPromise<number>; // number
// Recursive conditional type
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Deep = Flatten<number[][][]>; // number
Key Takeaways
- Use interfaces for object shapes and type aliases for unions/intersections
- Prefer string enums or union literals over numeric enums
as constassertions are safe; regular type assertions are dangerous- Type guards enable safe type narrowing at runtime
- Discriminated unions with exhaustiveness checks ensure type safety
- Mapped types transform object properties systematically
- Conditional types with
inferenable advanced type extraction