Best Practices in TypeScript Programming
This article explains best practices in TypeScript programming.
This guide explains practical best practices for leveraging TypeScript's types to reduce bugs and write more readable code.
YouTube Video
Best Practices in TypeScript Programming
The greatest benefit of TypeScript is 'preventing bugs with types and making code intent clear'.
Best practices are not merely rules but a set of principles to write safe, readable, and maintainable code. Below, we introduce commonly used TypeScript best practices with practical examples.
Avoid any and always give types meaningful definitions
First, let's look at the point of 'avoiding any and giving meaningful types.'.
any completely disables type checking, which defeats the purpose of using TypeScript. Instead of using any just to make things work, it's important to provide types that are as descriptive as possible.
1// Bad
2function parse(data: any) {
3 return data.value;
4}This code can accept any value, so runtime errors cannot be prevented.
1// Good
2type ParsedData = {
3 value: string;
4};
5
6function parse(data: ParsedData): string {
7 return data.value;
8}By defining types, you clarify the intent of inputs and outputs and improve safety.
Always explicitly define object structures using type or interface.
Next, let's look at the point of 'always explicitly defining object structures using type or interface.'.
If you use objects ad-hoc, their structure can become ambiguous. Always extract types for reusability and maintainability.
1// Bad
2function createUser(user: { name: string; age: number }) {
3 console.log(user.name);
4}Even in small pieces of code, it is important to develop the habit of separating types.
1// Good
2type User = {
3 name: string;
4 age: number;
5};
6
7function createUser(user: User): void {
8 console.log(user.name);
9}By naming types, understanding the overall codebase becomes much easier.
Use union types to precisely represent all possible states.
If you use raw string or number types for conditionals, unexpected values can slip through. By using union types, you can represent only the allowed states at the type level.
1// Bad
2function setStatus(status: string) {
3 console.log(status);
4}In this code, meaningless strings or incorrect values cannot be detected at compile time.
1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5 console.log(status);
6}By using union types, you can reliably eliminate 'impossible states' at compile time. As a result, the safety of conditional branches and the reliability of the code will improve.
Handle null and undefined explicitly.
Next, let's look at the point of 'explicitly handling null and undefined.'.
In TypeScript, it's important to express the possible absence of a value in the type. If you keep things ambiguous, they may lead to runtime errors.
1type User = {
2 name: string;
3 email?: string;
4};email may not exist, so you should handle it with that assumption.
1function printEmail(user: User): void {
2 if (user.email) {
3 console.log(user.email);
4 }
5}Always check optional values before using them.
Use type assertions (as) only as a last resort.
Next, let's look at the point of 'not overusing type assertions.'.
Type assertions temporarily bypass TypeScript's type checking to declare, 'I know this value is of this type.'. Overusing them undermines type safety.
1// Bad
2const value = input as string;In this code, even if the actual value is not a string, no error occurs, which can cause runtime errors. As shown in the following code, choose to safely check using type guards first.
1// Good
2function isString(value: unknown): value is string {
3 return typeof value === "string";
4}
5
6if (isString(input)) {
7 console.log(input.toUpperCase());
8}Type guards are a mechanism to safely determine a type while checking the actual value. By prioritizing type guards over type assertions, you can more easily prevent runtime errors.
Don't rely too much on type inference for return types.
Next, let's look at the point of 'not relying too much on inference for return types.'.
TypeScript's type inference is powerful, but it's safer to explicitly annotate return types of public functions. This minimizes the potential impact of future changes.
1// Bad
2function sum(a: number, b: number) {
3 return a + b;
4}Clearly state your intent even in small functions.
1// Good
2function sum(a: number, b: number): number {
3 return a + b;
4}Writing out return types increases the stability of your API.
Handle inputs safely by using unknown.
Next, let's look at the point of 'safely accepting external input using unknown.'.
For external input such as APIs, JSON, or user input, use unknown instead of any. By doing so, you ensure that all values are validated, maintaining type safety.
1// Bad
2function handleResponse(data: any) {
3 console.log(data.id);
4}Here's how to validate types with unknown.
1// Good
2function handleResponse(data: unknown): void {
3 if (
4 typeof data === "object" &&
5 data !== null &&
6 "id" in data
7 ) {
8 console.log((data as { id: number }).id);
9 }
10}unknown cannot be used as is; it is a type that requires validation. It is especially effective when handling external input.
Increase expressiveness by combining small types.
Next, let's look at the point of 'increasing expressiveness by combining small types.'.
Defining large types all at once reduces readability and maintainability. Divide your types into meaningful units and combine them as needed.
1type Id = number;
2
3type UserProfile = {
4 id: Id;
5 name: string;
6};
7
8type UserWithStatus = UserProfile & {
9 status: "active" | "inactive";
10};Treating types as components helps organize your design.
type and interface
Advantages of interface
type and interface can both define types, but their intended uses and characteristics differ. By using them for the appropriate roles, the intent of your type definitions becomes clearer.
1// Bad
2type User = {
3 id: number;
4 name: string;
5};
6
7type AdminUser = {
8 id: number;
9 name: string;
10 role: "admin";
11};If you duplicate common parts like this, your code becomes fragile to changes.
1// Good
2interface User {
3 id: number;
4 name: string;
5}
6
7interface AdminUser extends User {
8 role: "admin";
9}interface is ideal for designs that involve extension (extends) and is best suited for expressing the 'shape' of objects.
Advantages of type
On the other hand, type is more expressive and suitable for handling union and intersection types.
1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5 | { status: "success"; data: T }
6 | { status: "error"; message: string };type is well suited for expressing states, options, and combinations.
Guidelines for choosing between type and interface
As a rule of thumb, use interface for object structures and contracts, and type for when you need the expressiveness of union, intersection, or type operations.
Both work similarly, but it's important to choose based on what communicates the reason for the type's existence.
Treat your types as documentation.
Finally, let's look at the point of 'writing types as documentation.'.
Good type definitions convey more information than comments ever could. It is important to aim for a state where 'the specification can be understood just by looking at the types.'.
1type ApiError = {
2 code: number;
3 message: string;
4 retryable: boolean;
5};In this way, one of TypeScript's major strengths is that type definitions can function as a kind of specification document.
Summary
TypeScript best practices are not about being overly strict. The essence is to clarify intent through types and write code that is robust to change.
By accumulating small rules in daily development, you can achieve long-term effects such as 'easier reviews,' 'fewer bugs,' and 'better understanding for your future self and others.'.
First, by thinking 'how can I express this with types?', you will write high-quality, TypeScript-style code.
You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.