Union Types and Type Guards
## Learning Objectives
- Create and use union types
- Work with discriminated unions
- Master type guards
- Narrow types effectively
- Handle null and undefined
## Union Types
### Basic Syntax
```typescript
let id: string | number;
id = "ABC123"; // OK
id = 456; // OK
// id = true; // Error
```
### In Functions
```typescript
function printId(id: string | number): void {
console.log(`ID: ${id}`);
}
printId("ABC123"); // OK
printId(456); // OK
```
## Union Type Operations
### Limited Operations
```typescript
function printId(id: string | number): void {
// id.toUpperCase(); // Error: number has no toUpperCase
if (typeof id === "string") {
console.log(id.toUpperCase()); // OK within block
} else {
console.log(id.toFixed(2)); // OK within block
}
}
```
## Discriminated Unions
### Common Property
```typescript
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
side: number;
}
type Shape = Circle | Square;
function area(shape: Shape): number {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
} else {
return shape.side ** 2;
}
}
```
### Example: API Responses
```typescript
interface SuccessResponse {
status: "success";
data: User[];
}
interface ErrorResponse {
status: "error";
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse): void {
if (response.status === "success") {
console.log(response.data);
} else {
console.error(response.message);
}
}
```
## Type Guards
### typeof
```typescript
function print(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
```
### instanceof
```typescript
class Dog {
bark(): void {
console.log("Woof!");
}
}
class Cat {
meow(): void {
console.log("Meow!");
}
}
function speak(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
```
### Property Check
```typescript
interface Book {
title: string;
author: string;
}
interface Magazine {
name: string;
issue: number;
}
function print(item: Book | Magazine): void {
if ("title" in item) {
console.log(item.title, item.author);
} else {
console.log(item.name, item.issue);
}
}
```
## Custom Type Guards
### Function Returns boolean
```typescript
interface Person {
name: string;
age: number;
}
interface Animal {
name: string;
species: string;
}
function isPerson(obj: any): obj is Person {
return "age" in obj && typeof obj.age === "number";
}
function check(obj: Person | Animal): void {
if (isPerson(obj)) {
console.log(obj.age);
} else {
console.log(obj.species);
}
}
```
### Type Predicate
```typescript
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown): void {
if (isString(value)) {
console.log(value.toUpperCase()); // TypeScript knows it's string
}
}
```
## Null and Undefined
### Nullable Types
```typescript
let name: string | null = null;
name = "Alice";
// name = undefined; // Error if not in type
```
### Optional Chaining
```typescript
interface User {
address?: {
city: string;
};
}
let user: User = {};
// Old way
let city = user && user.address && user.address.city;
// Optional chaining
let city2 = user?.address?.city; // undefined if missing
```
### Nullish Coalescing
```typescript
let name: string | null = null;
// Default value when null/undefined
let displayName = name ?? "Anonymous";
// vs || (also catches empty string)
let empty = "";
let result = empty ?? "default"; // ""
let result2 = empty || "default"; // "default"
```
## Type Narrowing
### In else Block
```typescript
function process(value: string | number | null): void {
if (value === null) {
console.log("null value");
} else {
// TypeScript knows: string | number
console.log(value);
}
}
```
### After throw
```typescript
function process(value: string | null): void {
if (value === null) {
throw new Error("Value is null");
}
// value is string here
console.log(value.toUpperCase());
}
```
### Never Type After Exhaustion
```typescript
function process(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
// value is number here
return value.toFixed(2);
}
```
## Intersection Types
### Intersection Types Syntax
```typescript
interface A {
a: string;
}
interface B {
b: number;
}
type AB = A & B;
let obj: AB = { a: "hello", b: 42 };
```
### Combine Unions
```typescript
type UnionA = "a" | "b";
type UnionB = 1 | 2;
type Combined = UnionA & UnionB;
// Combined = "a" & 1 | "a" & 2 | "b" & 1 | "b" & 2
// = never (in most cases)
```
## typeof for Variables
```typescript
const config = {
host: "localhost",
port: 8080,
debug: true
};
type Config = typeof config;
// {
// host: string;
// port: number;
// debug: boolean;
// }
```
## Keyof
Get keys as union type:
```typescript
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User;
// "id" | "name" | "email"
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}
let user: User = { id: 1, name: "Alice", email: "alice@example.com" };
getProperty(user, "name"); // string
```
## Exhaustive Unions
### Never in Default
```typescript
type Shape = Circle | Square | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "triangle": return 0.5 * shape.base * shape.height;
default: {
// If we add a new shape, this catches it
const _exhaustive: never = shape;
return _exhaustive;
}
}
}
```
## Summary
- Union types: `string | number` allows either
- Discriminated unions use a common `kind` property
- Type guards narrow types: `typeof`, `instanceof`, property checks
- Custom guards: `obj is Type` return type
- Optional chaining: `obj?.prop?.nested`
- Nullish coalescing: `value ?? default`
- Intersection types: `A & B`
- Keyof gets union of object keys
- Use `never` for exhaustive checking
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →