Type annotations and inference are core TypeScript features. This article covers type system fundamentals and advanced typing patterns.
Introduction
Type annotations and inference provide:
- Type safety
- Code documentation
- IDE support
- Error prevention
- Better refactoring
Understanding types helps you:
- Write type-safe code
- Leverage IDE features
- Prevent runtime errors
- Document code
- Improve maintainability
Primitive Types
Basic Type Annotations
// ✅ Good: Basic type annotations
const name: string = 'John';
const age: number = 30;
const isActive: boolean = true;
const nothing: null = null;
const undefined_value: undefined = undefined;
// ✅ Good: Literal types
const status: 'active' | 'inactive' = 'active';
const count: 1 | 2 | 3 = 2;
// ✅ Good: Any type (use sparingly)
let value: any = 'string';
value = 42; // OK but not recommended
// ✅ Good: Unknown type (safer than any)
let unknown_value: unknown = 'string';
// unknown_value.toUpperCase(); // Error: must check type first
if (typeof unknown_value === 'string') {
unknown_value.toUpperCase(); // OK
}
Type Inference
// ✅ Good: Type inference
const message = 'Hello'; // inferred as string
const count = 42; // inferred as number
const isActive = true; // inferred as boolean
// ✅ Good: Inferred from function return
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
const result = add(5, 3); // result inferred as number
// ✅ Good: Inferred from array
const numbers = [1, 2, 3]; // inferred as number[]
const mixed = [1, 'two', true]; // inferred as (number | string | boolean)[]
Complex Types
Arrays and Tuples
// ✅ Good: Array types
const strings: string[] = ['a', 'b', 'c'];
const numbers: Array<number> = [1, 2, 3];
const mixed: (string | number)[] = ['a', 1, 'b'];
// ✅ Good: Tuple types
const tuple: [string, number] = ['hello', 42];
const tuple2: [string, number, boolean] = ['hello', 42, true];
// ✅ Good: Tuple with optional elements
const optional: [string, number?] = ['hello'];
const optional2: [string, number?] = ['hello', 42];
// ✅ Good: Tuple with rest elements
const rest: [string, ...number[]] = ['hello', 1, 2, 3];
Objects
// ✅ Good: Object type annotations
const user: { name: string; age: number } = {
name: 'John',
age: 30
};
// ✅ Good: Optional properties
const config: { host: string; port?: number } = {
host: 'localhost'
};
// ✅ Good: Readonly properties
const point: { readonly x: number; readonly y: number } = {
x: 10,
y: 20
};
// point.x = 30; // Error: cannot assign to readonly property
// ✅ Good: Index signatures
const map: { [key: string]: number } = {
a: 1,
b: 2,
c: 3
};
Union and Intersection Types
// ✅ Good: Union types
type Status = 'active' | 'inactive' | 'pending';
const status: Status = 'active';
type ID = string | number;
const userId: ID = 123;
const postId: ID = 'abc-123';
// ✅ Good: Intersection types
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const person: Person = {
name: 'John',
age: 30
};
// ✅ Good: Union with type guards
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
} else {
return value * 2;
}
}
Functions
Function Type Annotations
// ✅ Good: Function parameter and return types
function add(a: number, b: number): number {
return a + b;
}
// ✅ Good: Optional parameters
function greet(name: string, greeting?: string): string {
return `${greeting || 'Hello'}, ${name}!`;
}
// ✅ Good: Default parameters
function multiply(a: number, b: number = 1): number {
return a * b;
}
// ✅ Good: Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// ✅ Good: Function type
type Callback = (value: string) => void;
const callback: Callback = (value) => {
console.log(value);
};
// ✅ Good: Function overloading
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value * 2;
}
Type Guards
Type Narrowing
// ✅ Good: typeof guard
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // value is string
} else {
return value * 2; // value is number
}
}
// ✅ Good: instanceof guard
class Dog {
bark() { }
}
class Cat {
meow() { }
}
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// ✅ Good: Custom type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(value: unknown) {
if (isString(value)) {
return value.toUpperCase(); // value is string
}
}
// ✅ Good: Discriminated unions
type Circle = { kind: 'circle'; radius: number };
type Square = { kind: 'square'; side: number };
type Shape = Circle | Square;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.side ** 2;
}
}
Advanced Types
Conditional Types
// ✅ Good: Conditional types
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
// ✅ Good: Conditional with generics
type Flatten<T> = T extends Array<infer U> ? U : T;
type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number
Mapped Types
// ✅ Good: Mapped types
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type User = { name: string; age: number };
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number }
// ✅ Good: Getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Person = { name: string; age: number };
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
Utility Types
// ✅ Good: Built-in utility types
type User = { name: string; age: number; email: string };
// Partial - all properties optional
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }
// Required - all properties required
type RequiredUser = Required<PartialUser>;
// { name: string; age: number; email: string }
// Readonly - all properties readonly
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }
// Pick - select specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// { name: string; email: string }
// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { name: string; age: number }
// Record - create object with specific keys
type Status = 'active' | 'inactive' | 'pending';
type StatusCount = Record<Status, number>;
// { active: number; inactive: number; pending: number }
// Exclude - exclude types from union
type NonString = Exclude<string | number | boolean, string>;
// number | boolean
// Extract - extract types from union
type StringOrNumber = Extract<string | number | boolean, string | number>;
// string | number
// ReturnType - get function return type
type AddReturn = ReturnType<typeof add>;
// number
Practical Examples
Type-Safe API Response
// ✅ Good: Type-safe API response
interface ApiResponse<T> {
status: 'success' | 'error';
data?: T;
error?: string;
}
interface User {
id: number;
name: string;
email: string;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return {
status: 'success',
data
};
} catch (error) {
return {
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Usage
const response = await fetchUser(1);
if (response.status === 'success' && response.data) {
console.log(response.data.name); // Type-safe access
}
Type-Safe Event Handler
// ✅ Good: Type-safe event handler
type EventMap = {
'user:login': { userId: number };
'user:logout': { timestamp: number };
'error': { message: string };
};
class EventEmitter {
private listeners: Map<string, Function[]> = new Map();
on<K extends keyof EventMap>(
event: K,
listener: (data: EventMap[K]) => void
): void {
if (!this.listeners.has(event as string)) {
this.listeners.set(event as string, []);
}
this.listeners.get(event as string)!.push(listener);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void {
const listeners = this.listeners.get(event as string) || [];
listeners.forEach(listener => listener(data));
}
}
// Usage
const emitter = new EventEmitter();
emitter.on('user:login', (data) => {
console.log(`User ${data.userId} logged in`);
});
emitter.emit('user:login', { userId: 123 });
Best Practices
- Use explicit types:
// ✅ Good const user: { name: string; age: number } = { name: 'John', age: 30 }; // ❌ Bad const user: any = { name: 'John', age: 30 }; ```javascript - Leverage type inference:
// ✅ Good const numbers = [1, 2, 3]; // inferred as number[] // ❌ Bad const numbers: any[] = [1, 2, 3]; ```javascript - Use union types:
// ✅ Good type Status = 'active' | 'inactive'; // ❌ Bad type Status = string; ```javascript
Common Mistakes
- Using any:
// ❌ Bad const value: any = 'string'; // ✅ Good const value: string = 'string'; ```javascript - Not using type guards:
// ❌ Bad function process(value: string | number) { return value.toUpperCase(); // Error } // ✅ Good function process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); } }
Summary
Type annotations and inference are fundamental. Key takeaways:
- Use explicit type annotations
- Leverage type inference
- Use union and intersection types
- Implement type guards
- Use utility types
- Avoid any type
- Use discriminated unions
- Document with types
Related Resources
Next Steps
- Learn about Interfaces and Type Aliases
- Explore Generics
- Study Decorators and Metadata
- Practice type annotations
- Build type-safe applications
Comments