- PR Node.js #53725 đã được hợp nhất, bổ sung hỗ trợ thử nghiệm cho việc chạy tệp TypeScript thông qua cờ
--experimental-strip-types
- Tính năng này loại bỏ cú pháp kiểu khỏi mã nguồn TypeScript để chuyển đổi thành mã nguồn JavaScript, và không thực hiện kiểm tra kiểu trong quá trình chuyển đổi
- Bản triển khai ban đầu không chuyển đổi các tính năng riêng của TypeScript như
Enum, namespaces, nên chỉ giới hạn ở những đoạn mã có thể trở thành JavaScript chỉ bằng cách loại bỏ kiểu
@swc/wasm-typescript được chọn để triển khai, vì sự đơn giản: không cần thêm Rust hay Go vào toolchain của Node.js
- Trong phần thảo luận, các ràng buộc chính được đề cập gồm quy tắc phần mở rộng tệp bắt buộc của ESM, thông lệ TypeScript dùng import
.js trỏ tới tệp .ts thực tế, cùng chi phí của sourcemap và vị trí trong error stack
Cách hoạt động của --experimental-strip-types
- Khi đặt
--experimental-strip-types, Node.js có thể chạy tệp TypeScript
- Node.js transpile mã nguồn TypeScript thành mã nguồn JavaScript
- Trong quá trình transpile, Node.js không thực hiện kiểm tra kiểu và loại bỏ thông tin kiểu
- Ví dụ như sau
const foo: string = "foo";
const foo = "foo";
Lý do chỉ chọn loại bỏ kiểu và các giới hạn ban đầu
- Trọng tâm của PR này là loại bỏ cú pháp kiểu trong cú pháp TypeScript
- Các runtime khác đôi khi chuyển đổi những tính năng riêng của TypeScript không có trong JavaScript, như
enum, sang JavaScript
- Bản triển khai ban đầu của PR này không thực hiện các chuyển đổi như vậy
- Không thể dùng
Enum
- Không thể dùng
namespaces
- Các tính năng riêng khác của TypeScript cũng không thuộc phạm vi chuyển đổi
- PR có kèm hướng dẫn tham khảo
typescript.md trong các thay đổi của PR để biết chi tiết triển khai và giới hạn
Chọn @swc/wasm-typescript làm công cụ triển khai
@swc/wasm-typescript là một gói nhỏ, gồm một tệp wasm và một tệp JavaScript để binding với nó
- Các công cụ khác có yêu cầu phải thêm Rust hoặc Go vào toolchain của Node.js
- SWC cũng đang được Deno dùng cho cùng mục đích, và tác giả PR xem đây là một lựa chọn đã được kiểm chứng
- Trong tương lai, tính năng này có thể được triển khai ở lớp native
- PR này tham chiếu tới nodejs/loaders#217
Thảo luận về phân giải import và phần mở rộng
- Import không có phần mở rộng như
import foo from "./foo" không được hỗ trợ
- Node.js ESM có quy tắc mandatory file extensions, nên việc không hỗ trợ import không có phần mở rộng được xem là hành vi có chủ đích
- Có ý kiến cho rằng trong các codebase module TypeScript, ngay cả khi tệp thực tế là
.ts, câu lệnh import vẫn thường dùng phần mở rộng .js
- Ví dụ:
import foo from './foo.js'
- Trường hợp trong hệ thống tệp thực tế không có
./foo.js mà chỉ có ./foo.ts
- Tác giả PR cho rằng vấn đề này có thể được xử lý trong các vòng lặp sau, và việc suy đoán phần mở rộng sẽ cần thêm kiểm tra hệ thống tệp cùng heuristic
- Nếu không có
./foo.js thì kiểm tra ./foo.ts
- Nếu không có
./foo.ts thì kiểm tra ./foo.js
- Thảo luận này được theo dõi tại nodejs/loaders#208
Vị trí nguồn, sourcemap và error stack
- Trong quá trình review, có ý kiến rằng trong ngắn hạn cần vị trí nguồn chính xác trong error stack
- Một cách khả dĩ là không thay đổi vị trí nguồn trong quá trình loại bỏ kiểu, qua đó tránh việc tạo và ánh xạ sourcemap
- Sourcemap có chi phí về mức sử dụng bộ nhớ và hiệu năng, đồng thời có ràng buộc là không đơn giản
- Tác giả PR cho rằng vì SWC đã hỗ trợ sourcemap, có thể thêm một cờ để kích hoạt tính năng này
- Cũng có thể yêu cầu SWC cung cấp tùy chọn thay thế cú pháp bị loại bỏ bằng khoảng trắng để không làm thay đổi error stack gốc
Các vấn đề thiết kế còn lại trước khi ổn định
- Hỗ trợ TypeScript được chia thành hai trục
- Loại bỏ cú pháp kiểu
- Điều chỉnh quy tắc phân giải để chấp nhận tệp
.ts
- Có lo ngại rằng việc thay đổi quy tắc phân giải có thể xung đột với các loader TypeScript hiện có
- Tác giả PR trả lời rằng PR này không override quy tắc phân giải, và cần có phần mở rộng tệp
- Có ý kiến cho rằng có thể hỗ trợ dạng
import './file.ts' thông qua allowImportingTsExtensions, và cách này cũng phù hợp với Deno
- Nếu muốn chạy trực tiếp cùng một tệp nguồn và cũng tạo đầu ra JavaScript bằng công cụ build, trách nhiệm đổi
file.ts thành file.js có thể thuộc về công cụ build
Hợp nhất và các công việc tiếp theo
- PR được hợp nhất vào
nodejs:main ngày 24/07/2024
- Quy mô thay đổi gồm 8 commit, 89 tệp, thêm 2.190 dòng và xóa 25 dòng
- PR được gắn các nhãn như
semver-minor, experimental, module, esm, notable-change
- Đã có thảo luận về việc có nên đề cập tính năng này trên trang hướng dẫn TypeScript của website hay không, và PR cập nhật liên quan cũng được liên kết
- Việc đưa phiên bản TypeScript mà binary Node dùng để loại bỏ kiểu vào metadata phát hành cũng được thảo luận như một issue tiếp theo
1 bình luận
Ý kiến trên Hacker News
Xóa kiểu TypeScript là điều không thể nếu không có cú pháp TypeScript. Việc xóa kiểu không phải là thao tác ở mức token, và cú pháp TypeScript vẫn tiếp tục thay đổi
Ví dụ,
foo < bar & baz > ( x )trong TypeScript 1.5 được phân tích như(foo (x))vìbar&bazkhông phải là biểu thức kiểu hợp lệ, nhưng sau khi toán tử kiểu giao nhau được thêm vào thì nó được phân tích thànhfoo<(bar & baz)>(x)và bị khử kiểu thànhfoo(x)Nếu muốn tiếp tục dùng các tính năng TypeScript mới nhất thì phải biên dịch sang JS hoặc liên tục cập nhật phiên bản Node, và với các nhóm thích ở lại Node LTS thì đây có thể là một sự đánh đổi khó chấp nhận
Cách này là giữ một phiên bản ổn định trong core, còn nếu TypeScript phát hành tính năng mới, thay đổi phá vỡ tương thích, hoặc người dùng muốn dùng tính năng thử nghiệm mới thì sẽ nâng cấp riêng qua NPM
Làm vậy sẽ tránh việc người dùng bị khóa cứng, đồng thời vẫn có thể hỗ trợ một phiên bản TypeScript cụ thể trong vòng đời 3 năm của bản phát hành Node.js: https://github.com/nodejs/loaders/issues/217
Thay đổi cú pháp lớn cuối cùng có lẽ là conditional types trong 2.8, khi một cú pháp giống toán tử tam phân
ifđược đưa vào vị trí kiểu. Nếu muốn kiểm tra kiểu đầy đủ thì mô hình kiểu đã thay đổi rất nhiều từ sau 2.x, nhưng cú pháp nhìn chung vẫn ổn định, và phần lớn đổi mới của TypeScript cũng nằm ở mô hình kiểu và suy luận kiểuDù vẫn đang ở giai đoạn đầu là Stage 1, hiện cũng có nỗ lực chuẩn hóa một phần đáng kể cú pháp kiểu TypeScript cho mục đích xóa kiểu chứ không phải kiểm tra kiểu: https://github.com/tc39/proposal-type-annotations
Các nhóm không mấy quan tâm đến tính năng mới của V8 hay Node và thích ở lại LTS cũng có khả năng ít quan tâm đến các tính năng TypeScript mới nhất hơn
Nếu Node.js có thể chạy trực tiếp file TypeScript thì trình biên dịch TypeScript có thể chỉ còn được dùng như trình kiểm tra kiểu, thay vì để xóa kiểu và chuyển đổi sang JavaScript. Cách này khá giống Python, nơi trình kiểm tra kiểu kiểm tra type rồi để nguyên, còn trình thông dịch thì bỏ qua type hint
Điều thú vị là trong Python đã xuất hiện nhiều trình kiểm tra kiểu phổ biến dùng cùng một cú pháp type hint nhưng có ngữ nghĩa khác nhau. Trong khi đó ở JavaScript thì có vẻ TypeScript gần như đã trở thành trình kiểm tra kiểu phổ biến duy nhất
Tôi cũng từng nghe rằng trong Python có trường hợp người ta không thực sự kiểm tra kiểu mà chỉ viết kiểu vào mã nguồn như một dạng cú pháp chú thích thuận tiện hơn. Nếu Node.js hỗ trợ bỏ qua kiểu thì JavaScript cũng có thể làm theo cách đó
Cá nhân tôi thấy bất tiện lớn nhất của JSDoc là cú pháp import, nhưng từ TypeScript 5.5 trở đi điều đó đã thay đổi nên không còn là vấn đề nữa
Tuy vậy, một số bundler đã làm việc này bằng các trình biên dịch không chính thức như SWC, và Node cũng đang thử nghiệm SWC cho tính năng này
Type hint không chỉ là chú thích đơn thuần mà còn là tín hiệu để IDE đưa ra khả năng tự động hoàn thành tốt hơn, gần giống như tên gọi
Tôi cho rằng nó nên trở thành một phần của đặc tả ngôn ngữ
Mình tò mò hệ sinh thái NPM sẽ phản ứng thế nào nếu tính năng này trở thành mặc định thay vì nằm sau một cờ. Không rõ khi các contributor phát hành module NPM thì họ vẫn sẽ build cả phiên bản CJS và ESM, hay chỉ cần thêm
engine: nodejs >= 25vàopackage.jsonrồi bỏ qua bước buildCá nhân mình sẽ thích hơn nếu các module NPM có mã nguồn TS ngừng phát hành
dist/.cjs, vì như vậy sẽ rõ ràng hơn là nên đặt debugger vàconsole.logở đâu. Với phía contributor NPM thì sức hút của việc bỏ hẳn bước build cũng sẽ rất lớnNhưng chỉ cần một dependency phát hành file TS thì giả định rằng “mọi người đều nhận file TS” có thể lan rất nhanh. Ngay khi Node.js đưa chuyện này ra khỏi cờ thử nghiệm dạng opt-in, có lẽ toàn bộ cộng đồng sẽ ngầm kỳ vọng người dùng chấp nhận file TS
Nếu vậy thì chỉ trong vài tháng, Firefox và Safari cũng sẽ bị gây áp lực phải chấp nhận, và mọi compiler JS sẽ buộc phải loại bỏ chú thích kiểu TS
Cá nhân mình cũng thấy điều đó ổn. Bên dùng TS hiện cũng đang phải đưa vào module NPM một bước chuyển đổi để biến mã TS thành JS và
.d.ts, chỉ để hỗ trợ một nhóm người dùng JS mang tính giả định nào đó. Nhưng nếu Node chấp nhận file.ts, mình thắc mắc sẽ có cơ chế nào để ngăn việc nhà phát hành NPM vô tình bỏ bước chuyển đổi đó mà không nhận ra là mọi thứ đã hỏng, rồi dừng phát hành filejs/d.tsCách làm là có thể đưa mã TypeScript lên mà không cần bước build, và khi lập trình viên khác cài vào thì họ sẽ thấy mã nguồn TypeScript gốc thay vì JavaScript đã được chuyển đổi
.tsbên trongnode_modules. Đây là một trong những ràng buộc chính để tránh phá vỡ hệ sinh thái.cjsvà.mjscho npm, nên có lẽ họ sẽ phát hành cả ba phiên bảnCòn nếu là dự án mới viết từ đầu thì có thể chỉ phát hành TypeScript và không chuyển đổi
Sửa: theo bình luận anh em của satanacchio, người có vẻ là tác giả PR và là thành viên ủy ban chỉ đạo kỹ thuật của Node.js, hỗ trợ TS chỉ áp dụng cho mục cấp cao nhất chứ không áp dụng cho thư viện bên trong
node_modules.tscủa mình lên npm, nhưng nhóm TypeScript trước giờ phản đối rất mạnh vì các vấn đềtsconfigvà hiệu năng. Dù vậy mình vẫn hy vọngBiết đâu một ngày nào đó Node có thể phản chiếu lúc runtime các kiểu này ngay trong JS
Nếu làm được thì lợi ích sẽ rất lớn. Trong Python có thể phản chiếu kiểu, nên những công cụ tuyệt vời như pydantic có thể sinh ra phần kiểm tra từ chính kiểu dữ liệu
Chỉ với một chuẩn ký pháp duy nhất, bạn có thể có kiểm tra kiểu, kiểm tra dữ liệu lúc runtime, tạo API và tạo tài liệu API
Còn trong JS hiện nay, các công cụ như Zod về cơ bản phải phát minh lại những gì TypeScript đã làm, kiểu như
const mySchema = z.string();z.string()của Zod không chỉ cung cấp an toàn kiểu như TypeScript. TypeScript cung cấp kiểm tra kiểu tại thời điểm biên dịch, còn Zod bổ sung xác thực và phân tích lúc runtimez.string()biếnmySchemathành một schema dạng hàm có thể parse và validate.mySchema.parse("some data")sẽ thành công, cònmySchema.parse(321)sẽ ném ra ngoại lệMình đã dùng nó ở những chỗ cần xác thực lúc runtime và kiểm tra trong tiến trình, thấy hoạt động khá tốt, đồng thời cũng có thể rút ra kiểu như
const A = z.string(); type A = z.infer; // stringTức là nếu định nghĩa kiểu trước trong Zod rồi suy luận kiểu từ đó, bạn sẽ có cả kiểm tra kiểu lúc biên dịch lẫn kiểm tra kiểu lúc runtime. Dù vậy, với codebase nhỏ phát triển nhanh thì có thể là hơi quá tay, nhưng trong các tình huống cần xác thực trong tiến trình thì nó khá hữu ích và khối lượng công việc cũng không lớn
Tuy vậy, TypeScript có cho phép plugin. Dù không được dùng nhiều, chúng có thể thêm vào ngôn ngữ các tính năng khác được chuyển đổi sang JavaScript đầu ra
Có một plugin liên quan đến kiểu runtime là Typia, cho phép dùng chữ ký kiểu TypeScript ở runtime trong các guard như
assert(myValue). Nó chặn lời gọi hàm này và sinh ra các câu lệnhifexhaustive trong JavaScript đã chuyển đổi để kiểm tra tính chất của biến được truyền vàoCó lẽ trong 4~6 năm tới chuyện này chưa trở thành một phần của ngôn ngữ, nhưng ngay hôm nay đã có các thư viện cho phép làm việc như vậy
Thứ thực sự cần là một hệ thống kiểu sound, nhưng TS thì cố ý không sound. Cần một hệ thống kiểu không cho phép những thói quen lập trình tệ như TS, và buộc dùng các thiết kế giúp chương trình chạy nhanh hơn. Cần một hệ thống kiểu Hindley-Milner
Nếu muốn biến module thành mã có kiểu, chỉ cần thêm
"use type"là được. Chế độ này nên cấm những phần xấu như ép kiểu, đồng thời ngăn các yếu tố làm giảm hiệu năng như đổi hình dạng đối tượng, đổi kiểu giá trị, hoặc tạo mảng trộn lẫn đủ loại giá trịDữ liệu đi vào từ module không có kiểu thì phải được chuyển đổi, hoặc nếu không thể chuyển đổi thì ném lỗi; khi đó compiler có thể tối ưu sâu mã có kiểu với bảo đảm kiểu mạnh hơn và không có rủi ro phải bỏ dở giữa chừng
ValueErrorhay không, hoặc một đối tượng có phải là instance củaSomeClasshay không. Mình mới học TS nên vẫn nghĩ ngoài mảng, đối tượng, số và chuỗi thì JS không có các kiểu khác, còn Zod hay các hàm type guard thì chỉ trả về các đối tượng thường theo kiểu “cứ tin tôi đi”Trải nghiệm lập trình viên của Bun ở mảng này đạt đến mức khá chưa từng có, và giờ hầu hết các trường hợp sử dụng đã được bao phủ, hoặc ít nhất Bun không còn bị crash khi viết script chạy bằng
bun runTrong khi đó Node thậm chí không thể cấu hình để không bắt buộc phần mở rộng trong import, và cũng không thể để
tsctự động gắn phần mở rộng.jsvào kết quả biên dịch, nên phải thêm bundlerHỗ trợ TypeScript native có lẽ sẽ giải quyết khá nhiều bất tiện nhỏ này, nhưng ngay cả khi được ổn định hóa thì cũng khó tưởng tượng trải nghiệm người dùng hay hiệu năng của nó có thể ngang Bun
Trải nghiệm lập trình viên và công cụ cũng thuộc hàng tốt nhất. Deno còn có thể kiểm tra kiểu, và nếu nhớ không nhầm thì có tích hợp TSC
Bun rất nổi, nhưng với công việc nghiêm túc thì hiện tại Deno rõ ràng là lựa chọn tốt hơn. Dù vậy, tầm nhìn và tốc độ thực thi của Bun vẫn rất ấn tượng và là điều tốt cho lập trình viên
bun installđều thất bại nên chưa lần nào hoạt động tử tế cả: https://github.com/search?q=repo%3Aoven-sh%2Fbun+bun+install...Tôi đã mong có TypeScript runtime từ lâu và cũng thật sự rất thích TypeScript, nhưng buồn cười là đã rời bỏ Java từ lâu rồi cuối cùng lại đi tìm một thứ gần với Java hơn rất nhiều
Có vẻ rốt cuộc thứ chúng ta muốn là Java có JIT, hệ thống kiểu phong phú hơn, và kiểu hóa dần dần. Hệ sinh thái npm cũng có nhiều điểm tệ, nhưng dùng thư viện trong hệ này ít áp lực hơn nhiều và cũng vui hơn
Thật ngạc nhiên là Rust ở đầu kia của phổ ngôn ngữ nhưng đôi khi cũng cho cảm giác tương tự
Sửa lại: dùng từ JIT là không phù hợp. Tôi viết JIT một cách lười biếng, nhưng điều tôi muốn nói là khác biệt về thời gian khởi động và lúc chạy khi thực thi thứ gì đó trên JVM và V8. Java cho cảm giác nặng nề, còn hệ sinh thái JavaScript thì linh hoạt hơn nhiều
Tôi cũng hoàn toàn không thấy TypeScript gần Java hơn ở điểm nào. Thứ hai bên cùng chia sẻ chỉ là cái tên sai lệch “Javascript” và việc hỗ trợ lập trình mệnh lệnh, còn lại thì tôi thấy rất khác nhau
Tuy nhiên, thứ có trong Java mà tôi nhớ ở TypeScript là phản chiếu kiểu lúc runtime. Hệ sinh thái TypeScript có vô số cách lách để làm điều này, nhưng tôi thấy tất cả đều hơi xấu xí
AoT cũng ổn, và sẽ tốt nếu có tùy chọn một file nhị phân duy nhất. Lý tưởng nhất là không có runtime, có thể loại bỏ thông minh, và càng tốt hơn nếu vẫn chạy JIT được trên trình duyệt
Tôi cũng muốn các tiện ích của compiler và trình kiểm tra kiểu như exhaustive pattern matching
Tính năng Deno mà tôi thích đang đi thẳng vào Node. Rất tuyệt
Có khi giờ tôi sẽ không còn phải luôn cài esbuild chỉ để loại bỏ kiểu nữa. Tôi rất mong việc viết rồi dùng script bằng TypeScript sẽ dễ hơn nhiều
Gần đây tôi vẫn thích Python hơn cho các script dùng một lần, nhưng về mặt kiểu thì cá nhân tôi thấy TypeScript tốt hơn Python. Đặc biệt với các script lớn mà vài tháng sau phải xem lại thì lợi ích của kiểu rất rõ
ts-node, nhưng cá nhân tôi thíchtsxhơnhttps://github.com/privatenumber/tsx
Đây đúng là một tháng đầy biến động với Node. Đầu tiên là node:sqlite được thêm trong v22.5.0, giờ lại sắp có cả hỗ trợ TypeScript. Tôi thích hướng đi hiện tại của Node
Tôi là người viết PR. Cứ hỏi gì cũng được
Tôi bắt đầu dùng Node.js cho công việc backend từ lâu, và thấy nó có nhiều ưu điểm hơn so với viết code bằng PHP, đồng thời dường như không mang theo quá nhiều vấn đề của Java
Tuy nhiên, Node hơi thô ráp, và tạo cảm giác như một ngôn ngữ mà muốn đúng ý thì phải gắn thêm đủ thứ. Cuối cùng tôi chuyển sang dùng Go, thấy dễ dùng hơn nhiều, và dù đôi khi dài dòng hơn đáng kể, tính an toàn kiểu lại giúp việc lập trình trở nên đơn giản hơn
TypeScript có vẻ là một lựa chọn tốt, nhưng rốt cuộc nó vẫn là một lớp chắp vá nữa. Tôi không rõ sẽ nhận được giá trị gì khi dùng TypeScript thay vì Go. Có kiểu dữ liệu rõ ràng thì tốt, nhưng nó không giải quyết những vấn đề khác của ngôn ngữ mà Go đã xử lý. Deno cũng giải quyết được một phần
Ưu điểm lớn của Node so với Go là có thể tạo prototype rất nhanh, nhưng khi dùng TypeScript thì lợi thế đó dường như giảm đi khá nhiều. Vì vậy khó mà nói đây là một bước tiến tốt, hay là đang làm mất đi những đặc tính từng khiến Node trở thành một lựa chọn tốt
Nhìn vào cách gọi là “lớp chắp vá”, có lẽ vấn đề không nằm ở bản thân ngôn ngữ mà là sự thiếu vắng một framework định hướng mạnh kiểu như Django, thứ quản lý mọi thứ theo mặc định. Tôi thích dùng Django, nhưng cũng khó rời khỏi con đường đã được định sẵn hơn một chút
cdktfnhờ kinh nghiệm frontend trước đó, và nó có vẻ là lựa chọn hợp lý hơn Go