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

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: type
và interface
. 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:
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
:
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:
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
:
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
:
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:
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:
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:
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
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
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
type UserPreview = Pick<User, "id" | "name">;
✅ Omit<T, K>
– Loại bỏ một số field khỏi type
type UserWithoutId = Omit<User, "id">;
✅ Readonly<T>
– Ngăn không cho chỉnh sửa object
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:
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:
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:
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:
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
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?
type NeededType = {
type: 'link' | 'title';
href?: string;
title?: string;
}
Chúng ta sẽ không làm cách như trên
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 Link
và Title
. Đ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
là"link"
, thì object đó bắt buộc phải cóhref
. - Nếu
type
là"title"
, thì object đó bắt buộc phải cótitle
.
// ✅ 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ụ:
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 T
là string
, 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:
type flattenArrayType<T> = T extends Array<infer ArrayType> ? ArrayType : T
Cách hoạt động:
- Nếu
T
là mộtArray
, 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ínhT
.
Kiểm tra với các kiểu dữ liệu khác nhau
type foo = flattenArrayType<string[]>;
// foo = string
string[]
là mộtArray
, nên nó trích xuấtstring
.
type foo = flattenArrayType<number[]>;
// foo = numbe
number[]
là mộtArray
, nên nó trích xuấtnumber
.
type foo = flattenArrayType<number>;
// foo = number
number
không phảiArray
, 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 type và biế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
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ủaType
.- Mỗi thuộc tính sẽ được gán kiểu
boolean
.
Ví dụ 2: Xóa readonly
khỏi một type
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óareadonly
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 (?
)
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
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
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
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ínhfirstname
.- 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! 🚀