# 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.

Bản gốc: https://fdetimes.net/vi/bach-khoa/typescript-cho-fde-tu-python-sang-trong-mot-tuan/

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:

```ts
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ỗ:

```ts
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.

**Điểm mấu chốt:** Mỗi lỗi kiểu bạn sửa là một trường hợp dữ liệu thật mà code cũ đã lờ đ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.

**Thử ngay tuần này:**

- Clone một repo Node mã nguồn mở, mở package.json và viết ra giấy từng script trong `scripts` dùng để làm gì trước khi chạy cái nào.
- Bật `"strict": true` trong tsconfig của một project nhỏ, chạy tsc, rồi sửa ba lỗi đầu tiên bằng cách thu hẹp kiểu chứ không dùng `any`.
- Viết lại một script Python asyncio ngắn của bạn bằng TypeScript async/await rồi đặt hai bản cạnh nhau để so.

## Nguồn

- [TypeScript for JavaScript Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)

- [Everyday Types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html)

- [TypeScript: TSConfig Reference - Docs on every TSConfig option](https://www.typescriptlang.org/tsconfig/#strict)

- [How to use promises](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Async_JS/Promises)

- [The Node.js Event Loop | Node.js Learn](https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick)

- [package.json | npm Docs](https://docs.npmjs.com/cli/v10/configuring-npm/package-json)

- [package-lock.json | npm Docs](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json)

- [Modules: TypeScript (Node.js v26.12.1 Documentation)](https://nodejs.org/api/typescript.html)
