Quản lý kiểu TypeScript nội bộ: Giải pháp cho tổ chức lớn
TypeScript mang đến khả năng kiểm tra kiểu mạnh mẽ và chúng ta thường dựa vào DefinitelyTyped cho các kiểu thư viện công khai. Tuy nhiên, khi bạn tải các thư viện qua CDN hoặc làm việc trên nhiều dự án nội bộ mà không cung cấp kiểu riêng, mọi thứ bắt đầu gặp vấn đề. Mỗi dự án định nghĩa phiên bản của riêng mình, gây ra sự trùng lặp và TypeScript bắt đầu thông báo lỗi.
Trong các tổ chức lớn, điều này thường xảy ra: bạn muốn tất cả các dự án chia sẻ cùng một kiểu, nhưng thực tế, mỗi nhóm thường tự phát triển kiểu của riêng mình.
Giải pháp? Một kho lưu trữ kiểu TypeScript tập trung nội bộ.
Cách kiểu được truyền bá: NPM vs CDN
Khi bạn cài đặt một gói từ NPM mà đi kèm với kiểu của riêng nó, mọi thứ hoạt động ngay lập tức. Nhưng khi bạn sử dụng một thư viện qua CDN, TypeScript không thể suy diễn bất cứ điều gì. Bạn phải khai báo kiểu một cách thủ công, thường bằng cách mở rộng Window.
Khi đó, các vấn đề xuất hiện. Một nhóm khai báo một tập hợp các phương thức, nhóm khác khai báo các phương thức hơi khác. Khi kết hợp các dự án lại, TypeScript sẽ không biết định nghĩa nào cần tin cậy.
Việc tải một script qua CDN thay vì NPM phụ thuộc vào trường hợp sử dụng. Đôi khi script không tồn tại trên NPM, đôi khi bạn muốn ưu tiên một thứ tự thực thi cụ thể.
Vấn đề: kiểu bị xung đột hoặc thiếu
Lấy ví dụ về script sau được tải qua CDN, trong trường hợp này là Dailymotion Web SDK.
html
<script defer src="https://geo.dailymotion.com/player.js"></script>
Nó xuất hiện một đối tượng toàn cục window.dailymotion. Trong TypeScript, bạn sẽ khai báo như sau theo yêu cầu của bạn:
typescript
declare global {
interface Window {
dailymotion: {
createPlayer: (
selectorId: string,
options: {
player: string;
video: string;
}
) => Promise<{
on: (event: string, callback: () => void) => void;
play: () => void;
}>;
events: {
PLAYER_START: string;
PLAYER_END: string;
};
};
}
}
Bây giờ hãy tưởng tượng hai dự án sử dụng Dailymotion SDK và không khai báo chính xác cùng một kiểu. Nếu bạn kết hợp hai dự án đó, sẽ xảy ra xung đột. Điều này thường không thể tránh khỏi vì mỗi bên viết phiên bản của riêng mình, có thể khác nhau.
Trong trường hợp có xung đột, lỗi TypeScript sau có thể xuất hiện:
error TS2717: Subsequent property declarations must have the same type.
Xem chi tiết về lỗi TS2717 trên TypeScript.tv.
Tập trung các kiểu: khái niệm
Giải pháp là tập trung tất cả các định nghĩa kiểu nội bộ và liên quan đến CDN trong một kho monorepo duy nhất. Hãy nghĩ về nó như một DefinitelyTyped riêng của bạn.
Các lợi ích là ngay lập tức:
- Tính nhất quán: tất cả các dự án sử dụng cùng một định nghĩa
- Tính tái sử dụng: xuất bản một lần, cài đặt ở mọi nơi từ kho lưu trữ riêng của bạn
- An toàn kiểu: không còn kiểu không khớp giữa các dự án
- Bảo trì: sửa một kiểu một lần, tất cả người dùng đều nhận được bản cập nhật
Tại Prisma Media, chúng tôi gọi monorepo của mình là Prime-Types 🤖
Triển khai tối thiểu
Cấu trúc monorepo tối thiểu có thể trông như sau:
json
package.json // định nghĩa các workspaces
types/
dailymotion/
package.json
index.d.ts
README.md
package.json gốc của monorepo:
json
{
"name": "types",
"workspaces": ["./types/*"],
"scripts": {
"dev": "tsc --noEmit --watch"
},
"devDependencies": {
"typescript": "5.8.3"
}
}
Bên trong types/dailymotion/package.json:
json
{
"name": "@prime-types/dailymotion",
"version": "1.0.0",
"description": "TypeScript types for Dailymotion",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.d.ts"
}
},
"main": "./index.d.ts",
"types": "./index.d.ts",
"devDependencies": {
"typescript": "5.8.3"
}
}
Ví dụ định nghĩa kiểu cho Dailymotion Web SDK trong index.d.ts:
typescript
// /types/dailymotion/index.d.ts
declare global {
interface Window {
dailymotion: {
createPlayer: (
selectorId: string,
options: {
player: string;
video: string;
}
) => Promise<{
on: (event: string, callback: () => void) => void;
play: () => void;
}>;
events: {
PLAYER_START: string;
PLAYER_END: string;
};
};
}
}
// Xuất khẩu để đảm bảo tệp được xử lý như một module
export {};
💡 Thêm
export {}ở cuối tệp nếu nó không xuất khẩu bất kỳ kiểu nào hoặc chỉ có mộtdeclare global.
Để tiêu thụ các kiểu này trong một dự án, hãy cài đặt gói này trong peer-dependencies nếu có thể (xem dưới đây tại sao trong phần này).
bash
npm install @prime-types/dailymotion
Mặc định, TypeScript tìm kiếm các khai báo trong đường dẫn node_modules/@types và trong mã nguồn dự án của bạn. Phần còn lại của node_modules sẽ bị bỏ qua. Do đó, bạn cần thêm đường dẫn đến các tệp khai báo trong tsconfig.json để cho phép TypeScript tham chiếu và truyền tải chúng.
json
{
"include": [
"./src/**/*",
+ "./node_modules/@prime-types"
]
}
👀 Khám phá ví dụ này trong monorepo tối giản Prime Types trên GitHub.
Quản lý nhiều phiên bản: peer dependencies
Một cạm bẫy có thể xảy ra: nhiều phiên bản của cùng một gói kiểu.
Nếu thư viện A và thư viện B phụ thuộc vào các phiên bản khác nhau của @prime-types/dailymotion, có thể xuất hiện lỗi TypeScript.
Giải pháp là luôn khai báo gói kiểu là một peer dependency:
json
{
"name": "my-website",
"peerDependencies": {
"@prime-types/dailymotion": "1.0.0"
}
}
Như vậy, chỉ một phiên bản có thể được cài đặt ở cấp độ cao nhất, và tất cả các dự án sẽ đồng bộ.
Giải pháp thay thế: một gói duy nhất
Đối với các nhóm nhỏ, bạn có thể xem xét một phiên bản đơn giản hóa: nhóm tất cả các kiểu thành một gói duy nhất.
Ưu điểm:
- Dễ dàng thiết lập và triển khai
- Cài đặt một gói và bạn xong
Nhược điểm:
- Khó khăn trong việc phiên bản kiểu độc lập
- Kích thước gói tăng nhanh khi bạn thêm kiểu khác
Một monorepo với nhiều gói là khả thi hơn về lâu dài.
Kết luận
DefinitelyTyped là lý tưởng cho mã nguồn mở, nhưng các dự án nội bộ đôi khi cần giải pháp riêng của mình. Tập trung các kiểu TypeScript mang lại nhiều lợi ích:
- Loại bỏ sự trùng lặp và xung đột
- Cải thiện tính an toàn kiểu giữa các dự án
- Cung cấp một nguồn thông tin duy nhất
Ví dụ hoàn chỉnh có sẵn trên GitHub, vì vậy bạn có thể thử nghiệm với nó! 🧑💻
Chúng tôi chưa thảo luận về việc triển khai trong bài viết này, nhưng tôi sẽ chi tiết quy trình triển khai của chúng tôi với GitLab trong một bài viết sau.
Thực hành tốt nhất
- Luôn sử dụng
peer dependenciesđể tránh xung đột kiểu. - Tạo một bộ công cụ để tự động kiểm tra và cập nhật kiểu khi có thay đổi.
Cạm bẫy phổ biến
- Không khai báo đúng kiểu khi dùng CDN có thể dẫn đến xung đột.
- Quản lý nhiều phiên bản kiểu không đúng cách có thể gây ra lỗi TypeScript.
Mẹo hiệu suất
- Giảm thiểu kích thước gói kiểu bằng cách chỉ định các kiểu cần thiết.
- Sử dụng caching để tăng tốc độ tải kiểu từ kho lưu trữ.
Khắc phục sự cố
- Kiểm tra đường dẫn trong
tsconfig.jsonnếu kiểu không được nhận diện. - Đảm bảo rằng các kiểu được khai báo đúng trong tất cả các dự án sử dụng.
Câu hỏi thường gặp
- Tôi có thể sử dụng kiểu từ một gói khác không?
Có, nhưng hãy đảm bảo rằng các kiểu đó tương thích với gói bạn đang sử dụng. - Làm thế nào để cập nhật kiểu?
Chỉ cần sửa đổi định nghĩa trong monorepo và phát hành phiên bản mới.
So sánh với các giải pháp khác
| Tính năng | Monorepo | Gói đơn |
|---|---|---|
| Tính nhất quán | Có | Không |
| Khả năng mở rộng | Cao | Thấp |
| Dễ thiết lập | Thấp | Cao |