TypeScript-specific best practices
This article introduces best practices for using the TypeScript type system effectively.
Key points
- Avoid any and use appropriate types
- Understand unknown and never
- Use generics
- Combine utility types
- Narrow types
Avoid any
any disables type checking, so you lose TypeScript's type safety.
Bad
❌ Bad: Using any
// ❌ Using any
function processData(data: any): any {
return data.value.toUpperCase();
}
const result = processData({ value: 123 }); // Runtime error
Good
✅ Good: Define an appropriate type
// ✅ Define an appropriate type
interface DataWithValue {
value: string;
}
function processData(data: DataWithValue): string {
return data.value.toUpperCase();
}
const result = processData({ value: 'hello' }); // 'HELLO'
// processData({ value: 123 }); // Compile error
Use unknown
When a type is unknown, use unknown instead of any.
✅ Good: Receive as unknown, then use after checking the type
// ✅ Receive as unknown, then use after checking the type
function parseJson(json: string): unknown {
return JSON.parse(json);
}
const data = parseJson('{"name": "Taro"}');
// data.name; // Error: properties do not exist on type 'unknown'
// Narrow with a type guard
function isUser(value: unknown): value is { name: string } {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
typeof (value as { name: unknown }).name === 'string'
);
}
if (isUser(data)) {
console.log(data.name); // OK
}
Understand never
never is the type of a value that can never occur. Use it for unreachable code and exhaustiveness checks.
// Exhaustiveness check
type Color = 'red' | 'green' | 'blue';
function getColorCode(color: Color): string {
switch (color) {
case 'red':
return '#ff0000';
case 'green':
return '#00ff00';
case 'blue':
return '#0000ff';
default:
// If every case is handled, this is unreachable
const exhaustiveCheck: never = color;
throw new Error(`Unknown color: ${exhaustiveCheck}`);
}
}
// Return type of a function that throws an exception
function throwError(message: string): never {
throw new Error(message);
}
Use generics
Generics let you write type-safe, reusable code.
✅ Good: A general-purpose function with generics
// ✅ A general-purpose function with generics
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
const firstNumber = getFirst([1, 2, 3]); // number | undefined
const firstString = getFirst(['a', 'b']); // string | undefined
// ✅ Constrained generics
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
interface User extends HasId {
name: string;
}
const users: User[] = [{ id: 1, name: 'Taro' }];
const user = findById(users, 1); // User | undefined
// ✅ Multiple type parameters
function map<T, U>(items: T[], fn: (item: T) => U): U[] {
return items.map(fn);
}
const lengths = map(['hello', 'world'], s => s.length); // number[]
Combine utility types
Use TypeScript's built-in utility types.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial: make all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }
// Required: make all properties required
type RequiredUser = Required<PartialUser>;
// Pick: extract only specific properties
type UserSummary = Pick<User, 'id' | 'name'>;
// { id: number; name: string }
// Omit: exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age: number }
// Record: an object type with specified key and value types
type UserById = Record<number, User>;
// Readonly: make all properties read-only
type ReadonlyUser = Readonly<User>;
// ReturnType: get the return type of a function
function createUser(): User {
return { id: 1, name: 'Taro', email: 'taro@example.com', age: 25 };
}
type CreatedUser = ReturnType<typeof createUser>; // User
// Parameters: get a function's parameter types as a tuple
type CreateUserParams = Parameters<typeof createUser>; // []
Type narrowing
Narrowing types in conditional branches lets you write safer code.
// Narrowing with typeof
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase(); // Can be treated as string
}
return value.toFixed(2); // Can be treated as number
}
// Narrowing with the in operator
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function makeSound(animal: Dog | Cat): void {
if ('bark' in animal) {
animal.bark(); // Can be treated as Dog
} else {
animal.meow(); // Can be treated as Cat
}
}
// Narrowing with instanceof
function formatError(error: unknown): string {
if (error instanceof Error) {
return error.message; // Can be treated as Error
}
return String(error);
}
// Custom type guard
interface ApiResponse<T> {
success: true;
data: T;
}
interface ApiError {
success: false;
error: string;
}
type ApiResult<T> = ApiResponse<T> | ApiError;
function isSuccess<T>(result: ApiResult<T>): result is ApiResponse<T> {
return result.success === true;
}
function handleResult<T>(result: ApiResult<T>): T | null {
if (isSuccess(result)) {
return result.data; // Can be treated as ApiResponse<T>
}
console.error(result.error); // Can be treated as ApiError
return null;
}
const assertion
Using as const lets values be inferred as literal types.
// Without as const
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
// Type: { apiUrl: string; timeout: number }
// With as const
const configConst = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const;
// Type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }
// Using it with arrays
const colors = ['red', 'green', 'blue'] as const;
// Type: readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"
Template literal types
You can combine string literal types to create new types.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = '/users' | '/posts' | '/comments';
// Template literal type
type ApiRoute = `${HttpMethod} ${Endpoint}`;
// "GET /users" | "GET /posts" | "GET /comments" | "POST /users" | ...
// Event name type
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
type ChangeEvent = EventName<'change'>; // "onChange"
Exercises
Refactor the following code by leveraging TypeScript's type features
// Problem
function fetchData(url: any): any {
// Fetch data from the API
return fetch(url).then(res => res.json());
}
function processItems(items: any[]): any[] {
return items.map(item => ({
id: item.id,
name: item.name.toUpperCase(),
}));
}
Answer:
✅ Good: After refactoring
// ✅ After refactoring
interface FetchOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>;
body?: string;
}
async function fetchData<T>(url: string, options?: FetchOptions): Promise<T> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
interface RawItem {
id: number;
name: string;
}
interface ProcessedItem {
id: number;
name: string;
}
function processItems(items: RawItem[]): ProcessedItem[] {
return items.map(item => ({
id: item.id,
name: item.name.toUpperCase(),
}));
}
// Usage example
interface User {
id: number;
name: string;
email: string;
}
async function getUsers(): Promise<User[]> {
return fetchData<User[]>('/api/users');
}
Improvements:
- Eliminate any and define concrete types
- A reusable fetchData with generics
- Clarify input and output with interfaces
- Add error handling with async/await
Summary of TypeScript-specific best practices
TypeScript-specific best practices
- Avoid
anyand useunknown - Define appropriate types
- Write reusable code with generics
- Use utility types
- Improve safety with type narrowing
- Leverage literal types with
as const - Prevent omissions with exhaustiveness checks
What to read next
- TypeScript Guide: Advanced type operations — the system of keyof / typeof / mapped types
- Same: Generics basics — how generics work and how to use them
- Same: Utility types — combining Partial / Pick / Omit