Back to blog

Mar 06, 2025

Viết TypeScript Chuẩn Chỉnh Cho Dự Án – Những Thứ Bạn Cần Biết

PH

Phineas

@Phineas

cover

Chắc hẳn mọi người cũng không xa lạ với typescript nữa, hiện tại có lẽ typeScript đang trở thành chuẩn mực cho các dự án frontend vì nó giúp code dễ quản lý, ít lỗi hơn, và nhiều thứ hay ho ở nó. Nhưng nếu không dùng đúng cách, chúng ta vẫn có thể tự làm khó chính mình. Vậy làm sao để viết TypeScript gọn gàng, hiệu quả và dễ bảo trì? Hôm nay mình sẽ chia sẻ một số best practices về types, interfaces, generics mà bạn nhất định phải biết!

1. type vs interface – Đừng Nhầm Lẫn

TypeScript có hai cách để định nghĩa kiểu dữ liệu: typeinterface. Mọi người thường bối rối không biết dùng cái nào, nên đây là cách mà mình phân biệt theo cá nhân của mình:

  • Dùng interface nếu đang làm việc với một object có cấu trúc cố định
  • Dùng type nếu bạn cần kiểu dữ liệu phức tạp hơn như union hoặc function signature

Ví dụ với interface, nếu bạn có một user:

typescript
interface User {
  id: number;
  name: string;
}

Còn nếu bạn muốn định nghĩa một kiểu function type hoặc union, hãy dùng type:

typescript
type Status = "success" | "error" | "loading";
type FetchData = (url: string) => Promise<string>;

💡 Mẹo: Nếu cần mở rộng (extends), hãy chọn interface. Nếu cần linh hoạt hơn với union types, type là lựa chọn tốt hơn.

Refer: Interfaces vs Types in TypeScript - Stack Overflow

2. Tránh Dùng any Bừa Bãi – Hãy Dùng unknown hoặc never

Mình biết đôi khi lười khai báo kiểu, chúng ta sẽ viết đại kiểu này:

typescript
let data: any;  // Sai lầm lớn!

Ví dụ phía trên mình đã gặp qua nhiều dự án, mọi người bảo code ko kịp hay urgent task, nhưng thực sự đó là 1 sai lầm lớn. Dùng any có thể phá vỡ tính an toàn của TypeScript, gây ra lỗi nghiêm trọng và cũng như có thể gây khó khăn trong quá trình debug và maintain dự án

any giống như cửa sau cho mọi lỗi runtime. Nếu bạn chưa chắc chắn về kiểu dữ liệu, hãy dùng unknown:

typescript
let data: unknown;

data = "Hello";  // OK
data = 42;       // OK
// console.log(data.toUpperCase()); ❌ Lỗi, vì TypeScript yêu cầu kiểm tra type trước

Còn nếu bạn có một hàm chắc chắn không trả về gì, dùng never:

typescript
function throwError(message: string): never {
  throw new Error(message);
}

🔹 Tóm lại: any = mất kiểm soát, unknown = an toàn hơn, never = chỉ dùng khi hàm không có điểm kết thúc.

3. Generics – Cứu Cánh Khi Code Linh Hoạt

Nếu bạn thấy mình đang copy-paste code chỉ để thay đổi kiểu dữ liệu, thì bạn cần Generics ngay!

Ví dụ, thay vì viết riêng function cho mỗi kiểu dữ liệu:

typescript

function identityNumber(value: number): number {
  return value;
}

function identityString(value: string): string {
  return value;
}

Dùng Generics giúp bạn tổng quát hóa function này:

typescript
function identity<T>(value: T): T {
  return value;
}

console.log(identity(10));    // number
console.log(identity("I am Phineas"));  // string

💡 Mẹo: Generics cũng giúp tạo API response type cực kỳ dễ dàng:

typescript
interface ApiResponse<T> {
  success: boolean;
  data: T;
}

const userResponse: ApiResponse<{ name: string }> = {
  success: true,
  data: { name: "Phineas Tran" }
};

Không cần viết lại mỗi lần gọi API với kiểu dữ liệu khác nhau…

4. Đừng Viết Lại Type – Dùng Utility Types!

TypeScript có một loạt Utility Types giúp bạn tiết kiệm thời gian. Đây là vài cái bạn nên dùng:

Partial<T> – Biến mọi field thành optional

typescript
interface User {
  id: number;
  name: string;
}

const updateUser = (user: Partial<User>) => {
  // Chỉ cần truyền một phần thông tin cũng được
};

Required<T, K> – Ngược lại với Partial nó biến mọi field thành require

typescript
interface MovieCharacter {
  firstname?: string;
  name?: string;
  movie?: string;
}

function hireActor(character: Required<MovieCharacter>) {}

// 👍
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
  movie: 'The Lord of the Rings',
});

// 👎
hireActor({
  firstname: 'Frodo',
  name: 'Baggins',
});

Pick<T, K> – Lấy một số field từ type

typescript
type UserPreview = Pick<User, "id" | "name">;

Omit<T, K> – Loại bỏ một số field khỏi type

typescript
type UserWithoutId = Omit<User, "id">;

Readonly<T> – Ngăn không cho chỉnh sửa object

typescript
const user: Readonly<User> = { id: 1, name: "Phineas Tran" };
// user.name = "Bob"; ❌ Lỗi

5. Tránh Dùng enum, Hãy Dùng Union Type

Mình biết enum nhìn rất chuyên nghiệp:

typescript
enum Status {
  Success,
  Error,
  Loading,
}

Nhưng có vấn đề:

  • enum trong typescript có thể làm giảm performace cho app
  • enum tự động gán số nên có thể gây lỗi không mong muốn

Ví dụ:

Khi bạn khai báo một enum mà không chỉ định giá trị cụ thể, TypeScript sẽ tự động gán số, bắt đầu từ 0 và tăng dần:

typescript
enum Status {
  Success,  // 0
  Error,    // 1
  Loading   // 2
}

Tại sao đây lại là vấn đề? Vì các giá trị của enum thực chất chỉ là số, và TypeScript không kiểm tra nếu số đó có hợp lệ hay không.

Ví dụ, nếu bạn có một function nhận vào Status, ai đó có thể truyền bất kỳ số nào vào:

typescript
function checkStatus(status: Status) {
  if (status === Status.Success) {
    console.log("Thành công!");
  }
}

checkStatus(999); // Không lỗi nhưng đây là giá trị sai!

TypeScript không cảnh báo, vì 999 vẫn được xem là một number, và Status chỉ là một kiểu số.

Hãy thay thế bằng union type:

typescript
type Status = "success" | "error" | "loading";

🚀 Lợi ích: Union Type gọn gàng hơn, dễ đọc hơn, không sợ lỗi số hóa enum.

6. Sử dụng Union Type một cách hiểu quả

Bài toán, khi type là link thì bắt buộc có href, và khi type là title, thì bắt buộc phải có title

typescript
const link = {
  type: 'link',
  href: '@phineasdev'
};

const title = {
  type: 'title',
  title: 'Phineasdevone'
};

Vậy chúng ta sẽ giải quyết kiểu nào?

typescript
type NeededType = {
	type: 'link' | 'title';
	href?: string;
	title?: string;
}

Chúng ta sẽ không làm cách như trên

typescript
type Link = {
	type: "link";
	href: string;
}

type Title = {
	type: "title";
	title: string;
}

type FinalType = Link | Title; // Phải khai báo type trước khi sử dụng

FinalType ở đây là Union Type giữa hai kiểu LinkTitle. Điều này có nghĩa là một object thuộc FinalType phải thỏa mãn một trong hai cấu trúc sau:

  • Nếu type"link", thì object đó bắt buộc phải có href.
  • Nếu type"title", thì object đó bắt buộc phải có title.

typescript
// ✅ Hợp lệ
const linkExample: FinalType = {
  type: "link",
  href: "https://example.com",
}; 

const titleExample: FinalType = {
  type: "title",
  title: "Hello World",
};

const invalidExample1: FinalType = {
  type: "link",
  title: "This is wrong", // ❌ Lỗi vì thiếu `href`
};

const invalidExample2: FinalType = {
  type: "title",
  href: "https://example.com", // ❌ Lỗi vì thiếu `title`
};

7. Conditional Types trong TypeScript

Conditional Types trong TypeScript hoạt động giống như toán tử 3 ngôi (condition ? trueType : falseType), giúp xác định kiểu dữ liệu dựa trên điều kiện.

Ví dụ:

typescript
interface StringId {
  id: string;
}

interface NumberId {
  id: number;
}

type Id<T> = T extends string ? StringId : NumberId;

let idOne: Id<string>; // Có kiểu StringId
let idTwo: Id<number>; // Có kiểu NumberId

Ở đây, Id<T> kiểm tra nếu Tstring, nó trả về StringId, ngược lại trả về NumberId. Điều này giúp code linh hoạt và an toàn hơn. 🚀

8. infer - Trích xuất kiểu dữ liệu từ một kiểu khác

Khái niệm

  • infer cho phép trích xuất một kiểu dữ liệu từ một kiểu khác.
  • Có thể hiểu infer giống như tạo một biến để lưu trữ kiểu dữ liệu, sau đó có thể sử dụng nó trong logic điều kiện.

Ví dụ

Chúng ta có một utility type giúp lấy kiểu dữ liệu bên trong mảng:

typescript
type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T

Cách hoạt động:

  • Nếu T là một Array, nó sẽ trích xuất kiểu dữ liệu bên trong mảng.
  • Nếu T không phải là Array, trả về chính T.

Kiểm tra với các kiểu dữ liệu khác nhau

typescript
type foo = flattenArrayType<string[]>;
// foo = string
  • string[] là một Array, nên nó trích xuất string.
typescript
type foo = flattenArrayType<number[]>;
// foo = numbe
  • number[] là một Array, nên nó trích xuất number.
typescript
type foo = flattenArrayType<number>;
// foo = number
  • number không phải Array, nên trả về chính nó.

9. Mapped Types - Biến đổi kiểu dữ liệu

Khái niệm

  • Mapped Types giúp chuyển đổi các kiểu dữ liệu bằng cách lặp qua tất cả các thuộc tính của một typebiến đổi chúng.
  • Rất mạnh khi cần tạo utility types tùy chỉnh.

Ví dụ 1: Chuyển toàn bộ thuộc tính thành boolean

typescript
interface Character {
  playInFantasyMovie: () => void;
  playInActionMovie: () => void;
}

type toFlags<Type> = { [Property in keyof Type]: boolean };

type characterFeatures = toFlags<Character>;


// ✅ Kết quả:
type characterFeatures = {
  playInFantasyMovie: boolean;
  playInActionMovie: boolean;
}

Cách hoạt động:

  • [Property in keyof Type] lặp qua tất cả các thuộc tính của Type.
  • Mỗi thuộc tính sẽ được gán kiểu boolean.

Ví dụ 2: Xóa readonly khỏi một type

typescript
type mutable<Type> = {
  -readonly [Property in keyof Type]: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type mutableCharacter = mutable<Character>;

// ✅ Kết quả:
type mutableCharacter = {
  firstname: string;
  name: string;
}

Cách hoạt động:

  • readonly xóa readonly khỏi tất cả thuộc tính.

Ví dụ 3: Chuyển toàn bộ thuộc tính thành tùy chọn (?)

typescript
type optional<Type> = {
  [Property in keyof Type]+?: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type optionalCharacter = optional<Character>;

// ✅ Kết quả:
type optionalCharacter = {
  firstname?: string;
  name?: string;
}

Cách hoạt động:

  • +? thêm dấu ? vào tất cả thuộc tính, biến chúng thành tùy chọn.

Ví dụ 4: Xóa readonly và đồng thời thêm ?

typescript
typescript
CopyEdit
type optionalAndMutable<Type> = {
  -readonly [Property in keyof Type]+?: Type[Property];
};

type Character = {
  readonly firstname: string;
  readonly name: string;
};

type optionalMutableCharacter = optionalAndMutable<Character>;

// ✅ Kết quả:
type optionalMutableCharacter = {
  firstname?: string;
  name?: string;
}
  • Xóa readonly.
  • Thêm ? để làm cho thuộc tính tùy chọn.

10. Sử dụng Mapped Types để tạo setter

typescript
typescript
CopyEdit
type setters<Type> = {
  [Property in keyof Type as `set${Capitalize<string & Property>}`]: () => Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type characterSetters = setters<Character>;

// ✅ Kết quả:
type characterSetters = {
  setFirstname: () => string;
  setName: () => string;
}

Cách hoạt động:

  • as set${Capitalize<string & Property>} đổi tên các thuộc tính, thêm tiền tố "set", đồng thời viết hoa chữ cái đầu.
  • Mỗi thuộc tính được đổi thành một phương thức setter.

11. Sử dụng Mapped Types với Exclude<>

typescript
typescript
CopyEdit
type nameOnly<Type> = {
  [Property in keyof Type as Exclude<Property, 'firstname'>]: Type[Property];
};

type Character = {
  firstname: string;
  name: string;
};

type characterWithoutFirstname = nameOnly<Character>;

// ✅ Kết quả:
type characterWithoutFirstname = {
  name: string;
}

Cách hoạt động:

  • Exclude<Property, 'firstname'> loại bỏ thuộc tính firstname.
  • Chỉ giữ lại thuộc tính không bị loại bỏ.

🔥 Lời Kết: Nếu bạn làm dự án TypeScript, hãy áp dụng những tips nhỏ này để viết code gọn gàng, dễ bảo trì và tránh những lỗi không đáng có! Chúc bạn code vui! 🚀