Type Annotations and Type Inference in TypeScript
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 }; -
Leverage type inference:
// โ Good const numbers = [1, 2, 3]; // inferred as number[] // โ Bad const numbers: any[] = [1, 2, 3]; -
Use union types:
// โ Good type Status = 'active' | 'inactive'; // โ Bad type Status = string;
Common Mistakes
-
Using any:
// โ Bad const value: any = 'string'; // โ Good const value: string = 'string'; -
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