FDE PulseViệc làm FDE đang mở 618Mới trong 7 ngày 33Công ty đang tuyển 94Nhận làm từ xa 24%Lương trung vị (Mỹ) $209kTuyển nhiều nhất Databricks 121
EN

Tờ báo của nghề Forward Deployed Engineer

Bách khoa

TypeScript cho FDE quen Python: kiểu, async và npm đủ để sửa code Node của khách trong một tuần

Phần lớn những gì bạn cần đã có sẵn trong đầu từ Python. Phần còn lại là ba thói quen mới, cộng với một lệnh phải chạy trước mỗi lần merge.

Màn hình laptop hiển thị code TypeScript, một lập trình viên đang ngồi sửa lỗi tại bàn làm việc
Ảnh: Mohammad Rahmani / Unsplash
Chia sẻLinkedInFacebookX

Tóm tắt nhanh

  • Code JavaScript đang chạy cũng là code TypeScript hợp lệ, nên bạn bật kiểm tra kiểu dần dần được, không cần viết lại.
  • Hàm async luôn trả về Promise: dùng kết quả mà quên `await` thì bạn đang cầm Promise chứ không phải dữ liệu, nên hãy để tsc soát mọi chỗ kết quả đó được đọc tiếp.
  • Node tự chạy được file .ts nhưng chỉ xoá kiểu đi chứ không kiểm tra gì, vì vậy `tsc` vẫn phải nằm trong `npm run` trước mỗi lần merge.
Hai khung code đặt cạnh nhau. Khung "Trước": const user = getUser(id) có dấu lỗi số 1, và await sendMail(user.email) có dấu lỗi số 2 và 3. Khung "Sau": thêm await (1, thiếu await), thêm if (!user) return (2, user có thể undefined), thêm if (user.email === null) return (3, email có thể null), rồi mới gọi await sendMail(user.email). Các dòng tương ứng ở hai khung nằm trên cùng một hàng.
Thêm await và bật strictNullChecks, tsc báo lần lượt ba lỗi. Mỗi lỗi là một trường hợp dữ liệu thật mà code cũ bỏ qua.

Thử hình dung: sáng thứ Hai bạn được cấp quyền vào repo backend của khách. Bạn đã viết Python năm năm, còn repo này là Node viết bằng TypeScript. Một endpoint gửi email thông báo đôi khi không gửi gì, và log chỉ có một dòng undefined.

Ở vị trí FDE, bạn không được nói “đội em không làm Node”. Khách cần lỗi được sửa trong tuần này, trên stack của họ. Tin tốt là nhiều khái niệm bạn sẽ gặp đều có bản tương ứng trong Python. Phần thật sự mới chỉ xoay quanh ba thứ: kiểu theo hình dạng, Promise, và npm.

Bạn đã biết nhiều hơn bạn nghĩ

Tài liệu chính thức của TypeScript nói rõ: code JavaScript đang chạy được cũng là code TypeScript. Với FDE, điều đó có giá trị thực tế, vì bạn có thể thêm kiểu từng file một vào code của khách thay vì phải xin phép viết lại cả module.

Cú pháp cũng quen. Chú thích kiểu đặt sau tên biến, giống type hint của Python: function getUser(id: string). TypeScript tự suy ra kiểu trong nhiều trường hợp, nên bạn chú thích ít hơn bạn tưởng. Còn any thì giống Any của Python: nó tắt kiểm tra. Tài liệu khuyên tránh dùng, vì giá trị kiểu any không được kiểm tra gì cả.

Trong Python Trong TypeScript/Node
x: str | None x: string | null
Any any (nên tránh)
coroutine, await Promise, await
vòng lặp asyncio event loop của Node
requirements và dev-requirements dependencies và devDependencies
lock file package-lock.json

Kiểu được xét theo hình dạng, không theo tên lớp

Đây là khác biệt lớn nhất về tư duy. Python mặc định so kiểu theo tên lớp. TypeScript dùng structural typing: hai object có cùng hình dạng thì được coi là cùng kiểu, bất kể ai khai báo chúng.

Vì thế ở repo của khách, bạn sẽ ít thấy class và gặp nhiều type User = { id: string; email: string | null }. Bất kỳ object nào có đủ hai trường đó đều được chấp nhận là User, kể cả JSON vừa parse từ API.

Union type cũng có một luật cần nhớ: TypeScript chỉ cho phép một thao tác khi thao tác đó hợp lệ với mọi thành viên của union. Với string | null, bạn không gọi .toLowerCase() được cho đến khi đã kiểm tra loại null ra.

Khi bật strictNullChecks, mọi giá trị có thể là null hay undefined đều phải được kiểm tra trước khi dùng thuộc tính hay method, giống như bạn phải kiểm tra None trước khi dùng một giá trị Optional trong Python. Cờ strict trong tsconfig gộp các kiểm tra chặt hơn này lại, và bạn nên bật nó.

Promise chính là awaitable của Node

Theo MDN, Promise là object mà một hàm bất đồng bộ trả về, đại diện cho trạng thái hiện tại của thao tác. Nếu bạn đã viết asyncio thì mô hình này quen: Node mặc định chạy JavaScript trên một luồng, và event loop cho phép nó làm I/O không chặn.

Cái bẫy nằm ở một câu ngắn trong tài liệu: hàm async luôn trả về một Promise. Gọi hàm đó mà quên await thì bạn không nhận được dữ liệu mà nhận được một lời hứa sẽ có dữ liệu. Khi đọc code của người khác, một chữ await bị thiếu rất khó nhận ra bằng mắt.

Lỗi trong repo của khách, sửa từng bước

Quay lại endpoint email. Giả sử đoạn code trông như sau:

type User = { id: string; email: string | null };

async function getUser(id: string): Promise<User | undefined> {
  const row = await db.findById(id);
  return row ?? undefined;
}

export async function notify(id: string) {
  const user = getUser(id);      // thiếu await
  await sendMail(user.email);    // user là Promise, không phải User
}

Với đoạn code này, tsc từ chối dòng user.email ngay cả khi chưa bật strict, vì Promise không có thuộc tính email. Đây là trường hợp dễ: kết quả của hàm async được dùng ngay ở dòng sau, nên có một thao tác sai kiểu để trình biên dịch bắt.

Hai trường hợp còn lại khó hơn. Nếu ai đó ép user thành any cho hết báo lỗi, tsc không còn kiểm tra gì. Và nếu một lời gọi như sendMail(...) mất await mà kết quả không được dùng tiếp, thì xét theo kiểu chẳng có thao tác nào sai, nên đừng trông vào tsc để phát hiện; bạn phải tự rà những lời gọi đó.

Thêm await vào, và đây là lúc strict phát huy tác dụng. Với strictNullChecks bật, tsc báo tiếp lỗi thứ hai: user có kiểu User | undefined, nên bạn chưa được đọc .email. Sửa xong lỗi đó lại lộ ra lỗi thứ ba: email có thể là null. Bản sửa cuối cùng thu hẹp kiểu ở cả hai chỗ:

export async function notify(id: string) {
  const user = await getUser(id);
  if (!user) {
    logger.warn({ id }, "user not found");
    return;
  }
  if (user.email === null) {
    logger.warn({ id }, "user has no email");
    return;
  }
  await sendMail(user.email);    // ở đây email chắc chắn là string
}

Hãy để ý cách bạn đi đến bản sửa. Bạn không phải đoán. Mỗi lỗi tsc báo ra là một trường hợp thật của dữ liệu khách mà code cũ bỏ qua, và mỗi nhánh return sớm là một dòng log giúp đội của khách hiểu vì sao email không được gửi.

npm là bản đồ của repo

Trước khi đọc dòng code nào, hãy mở package.json. Trường scripts ánh xạ từng tên lệnh tới lệnh thật cần chạy, vì vậy npm run build hay npm run test là lối vào chuẩn để build và test repo. Bạn không cần đoán khách dùng công cụ gì, vì tất cả được ghi ở đó.

Tiếp theo, nhìn hai nhóm phụ thuộc. Tài liệu npm yêu cầu không đặt test harness, transpiler hay công cụ chỉ dùng lúc phát triển vào dependencies.

Chúng thuộc về devDependencies, giống cách bạn tách requirements chạy thật với dev-requirements. package-lock.json ghim chính xác cây phụ thuộc và được thiết kế để commit vào repo.

Nếu bản sửa của bạn làm file này thay đổi, hãy đưa nó vào cùng PR.

Có một cái bẫy mới. Các bản Node gần đây chạy được file .ts trực tiếp bằng cách thay cú pháp TypeScript bằng khoảng trắng, và hoàn toàn không kiểm tra kiểu.

Tính năng nào cần biến đổi cú pháp, như enum, còn báo lỗi luôn. Vì vậy “Node chạy được” không chứng minh code đúng kiểu. Lỗi thiếu await ở trên vẫn được Node chạy mà không cảnh báo gì; chỉ khi tsc được gọi nó mới lộ ra.

Thứ còn ở lại sau khi bạn rời đi

Thứ tự làm việc trong tuần đã nằm trong sơ đồ đầu bài. Điều đáng nhấn thêm là bước cuối: kiểm tra rằng tsc nằm trong một script mà CI chạy trước khi merge, và nếu chưa có thì đề xuất thêm vào. Bản sửa của bạn giúp khách tuần này; còn tsc trong CI tiếp tục chặn lỗi cùng loại sau khi bạn đã sang khách hàng khác.

Những lỗi người Python hay mắc

Lỗi phổ biến nhất là dùng any cho trình biên dịch thôi báo lỗi. Làm vậy chỉ xoá đi thông tin bạn cần. Tiếp theo là quên await rồi đi tìm lỗi ở tầng database. Rồi đến thói quen coi Node chạy được file .ts là bằng chứng code đúng.

Về npm, đừng cài công cụ build vào dependencies và đừng bỏ package-lock.json ra khỏi commit. Cả hai đều khiến môi trường production của khách khác với máy bạn mà không ai biết.

Về nghề nghiệp, khi đọc tin tuyển FDE, hãy để ý phần mô tả stack của khách hàng: nếu có Node hay TypeScript, đó là kỹ năng nên chuẩn bị bằng chứng từ trước. Trong CV, đừng chỉ ghi “TypeScript”. Hãy viết một dòng cụ thể: bạn bật strict cho một codebase có sẵn, số lỗi kiểu đã sửa, và việc đưa tsc vào CI.

Nhà tuyển dụng đọc dòng đó sẽ hiểu bạn làm được việc trên stack của khách, chứ không chỉ trên stack của mình.

Bài tập tuần này: trong repo bạn đang làm, xoá await ở một lời gọi async mà kết quả được đọc ngay ở dòng sau, rồi chạy tsc; sau đó làm lại với một lời gọi mà kết quả bị bỏ đi. Lần đầu tsc sẽ báo, lần sau thì im lặng, và khoảng im lặng đó chính là chỗ bạn phải tự soát ở khách hàng tiếp theo.

Bài này có hữu ích không?

Dùng cùng trợ lý AIHỏi Claude ↗Hỏi ChatGPT ↗
8 nguồn
Đọc tiếp trên lộ trình · Chặng 2: Kỹ thuật rộngSemantic layer: vì sao text-to-SQL trả sai “doanh thu” và cách định nghĩa chỉ số một lầnCâu SQL chạy không lỗi, con số trông hợp lý, nhưng vẫn sai, vì chưa ai cho model biết khách hàng của bạn tính MRR theo cách nào.