TypeScript cần phát ra thông tin kiểu thời gian chạy - Yêu cầu xuất thông tin kiểu runtime của TypeScript
(github.com/akutruff)- Lập luận về sự cần thiết của việc phát ra thông tin kiểu thời gian chạy của TypeScript, đồng thời tập hợp danh sách các cách lách vấn đề này như Type Mapping, Code Generation / External Tool và các dự án Adapter
- Vấn đề cốt lõi là khi xử lý tuần tự hóa và kiểm chứng mà không có hệ thống kiểu phản chiếu, sẽ cần đến boilerplate bất tận hoặc sinh mã tùy biến dựa trên các tệp schema
- Các giải pháp vòng như io-ts, zod được đưa ra, nhưng gây bất tiện vì phải khai báo lại kiểu theo cách riêng của từng thư viện, và các thư viện này cũng không hỗ trợ toàn bộ tính năng kiểu của TypeScript
- Dù thừa nhận việc xóa kiểu của TypeScript có ưu điểm là cho phép các dự án JavaScript sử dụng JavaScript được phát ra mà không cần kiến thức về TypeScript, bài viết vẫn cho rằng có thể phát ra thông tin kiểu dưới dạng bảng tra cứu tách rời khỏi mã
- Kèm theo đề nghị đừng giải quyết bằng decorator, bài viết đề xuất các hướng tiếp cận như hàm bậc cao được compiler nhận biết kiểu
typescript.generateRuntimeType<T>(), F# Type Providers và C# Source Generators để hỗ trợ việc dùng interface và kiểu từ thư viện bên ngoài - Liên kết tới GitHub issue đã tồn tại 8 năm như phần thảo luận liên quan, đồng thời đề nghị nếu dự án của bạn cũng gặp vấn đề tương tự thì hãy gửi PR để bổ sung vào danh sách, bất kể số lượng giải pháp là bao nhiêu
1 bình luận
Ý kiến trên Hacker News
Phải đọc mô tả vấn đề khoảng 4 lần mới hiểu họ đang đòi hỏi gì, và đây đúng là ví dụ cho thấy vì sao viết ngắn gọn, súc tích lại quan trọng
Yêu cầu thực tế có vẻ là an toàn kiểu dữ liệu ở thời gian chạy, nhưng điều này khó khả thi vì TypeScript từ trước đến nay luôn vạch rõ ranh giới rằng nó sẽ không trở thành runtime thay thế/bổ sung cho JavaScript. TypeScript chỉ làm nhiệm vụ biên dịch sang JS rồi rút lui, còn những gì diễn ra sau đó trong V8 hay nơi khác đều nằm ngoài phạm vi của nó.
Việc đòi hỏi “TypeScript phải xuất thông tin kiểu dữ liệu ở runtime” gần như giống với việc đòi hỏi một sản phẩm mới khá khác với TypeScript hiện nay
Ví dụ, họ muốn có khả năng tạo một hàm
validatetổng quát nhận vào một interface bất kỳ và một object để kiểm chứng. Có thể tưởng tượng việc dùng reflection lúc biên dịch để sinh ra mã JS kiểm tra theo từng kiểu, hoặc truyềnTlàm tham số ở runtime để so sánh, nhưng theo triết lý hiện tại của TypeScript thì điều này khá khóTypeScript biết rất nhiều thông tin kiểu dữ liệu trong lúc biên dịch, nhưng khi biên dịch xong thì nó vứt bỏ những thông tin đó. Việc không xuất chúng ra file hoặc giữ lại dưới dạng metadata trong
Reflectlà một hạn chế đáng tiếc, nhất là nếu nghĩ đến bản chất “mọi thứ đều là object” của JavaScript.Dù không thể có được đầy đủ tính an toàn kiểu dữ liệu chính xác của TS, vẫn có thể gắn một phần kiểm tra bằng getter/setter hoặc ES2015
Proxy, và nếu giữ lại thông tin kiểu dữ liệu ở runtime thì rất nhiều khả năng thú vị sẽ mở ra dưới dạng metadata có thể thực thiTS
enumhiện đã được xuất thành object JS nên có thể tra cứu ở runtime, nhưng union type của string literal thì không. Ngoài ra, TypeScript còn hỗ trợ hàm type guard, nên việc tự động sinh các hàm như vậy từ thông tin trong hệ thống kiểu cũng không có vẻ quá khóCó thể hình dung theo phép so sánh với file PDB. Đây là thông tin TypeScript vốn đã có rồi lại vứt đi, nên không hẳn cần một sản phẩm hoàn toàn mới
https://github.com/microsoft/TypeScript/issues/3628
Từ góc nhìn của PM TypeScript thì tôi hiểu nhu cầu này. Việc kiểm chứng dữ liệu thường xuyên cần đến kiểm tra kiểu dữ liệu ở runtime, và cũng có rất nhiều thư viện ra đời để lấp chỗ trống đó
Tuy vậy, chính việc tồn tại nhiều thư viện với các quyết định thiết kế khác nhau lại là dấu hiệu cho thấy đây không phải một bài toán đã được giải quyết với đáp án đúng rõ ràng. Nhóm TypeScript đã biết điều này từ những ngày đầu thiết kế, và có vẻ nguyên tắc đó đã đứng vững khá tốt.
Thay vào đó, TypeScript đã trở nên đủ mạnh để biểu diễn bằng kiểu dữ liệu chính xác những gì các thư viện kiểm tra kiểu runtime thực sự làm, và người dùng có thể dùng API để cấu thành logic kiểm chứng runtime từ các kiểu. Chừng đó có vẻ là mức linh hoạt hợp lý
Ví dụ, tôi hình dung một ngôn ngữ nơi có thể định nghĩa kiểu cho số nằm trong một khoảng nhất định hoặc chuỗi khớp mẫu mã bưu chính, rồi compiler dùng các hàm kiểm tra được viết như hàm thông thường để xác nhận tính hợp lệ. Tôi tò mò không biết việc thiếu tính năng như vậy là quyết định thiết kế nhằm tránh độ phức tạp không cần thiết, hay do các ràng buộc kỹ thuật như hiệu năng
Ngay cả bây giờ, ai cần thì cũng có thể làm một preprocessor để sinh object runtime từ thông tin kiểu trước khi đưa vào
tsc, nhưng công sức đang bị phân tán mỗi nơi một kiểu. Nếu có preprocessor dạng plugin chính thức cùng một hệ sinh thái, có thể sẽ tìm ra một lời giải hayChỉ cần nhóm lửa ban đầu thì cộng đồng sẽ giúp duy trì, và nó có thể đáp ứng nhiều nhu cầu sinh mã khác nhau, từ tạo client cho đến assertion kiểu dữ liệu ở runtime
Classsau khi biên dịchCó lý do để không làm việc này. TypeScript sẽ trở thành một kiểu runtime nằm trên JavaScript, và biến thành một ngôn ngữ mới biên dịch xuống JS
Hiện tại TypeScript gần với JavaScript có gắn chú thích kiểu hơn. Đã có rất nhiều ngôn ngữ biên dịch xuống JS rồi, nên cứ dùng một trong số đó là được. Những người muốn TypeScript có kiểu ở runtime có vẻ như muốn viết mã theo phong cách Java/OOP hơn là JavaScript, nhưng JavaScript là ngôn ngữ kiểu động và đó cũng là một ưu điểm
generateTypeInfo!()được mở rộng thành một đối tượng JS mã hóa kiểuFoothì nó vẫn biên dịch thành JavaScript có thể đọc đượcTuy vậy, macro này phá vỡ tính chất “TS = JS có chú thích kiểu và việc biên dịch chỉ cần xóa chú thích”. Vì phải thực sự tính toán kiểu cấu trúc của
Foo.Ngoài ra, kiểu TypeScript là kiểu cấu trúc nên ở runtime có thể phần nào suy ra kiểu bằng cách kiểm tra cấu trúc đối tượng, nhưng nếu cần thông tin danh nghĩa đã bị xóa như tên kiểu hay việc nó có thuộc một union chuỗi hay không, thì do hệ kiểu TypeScript là Turing-complete và có chuyển đổi cấu trúc ngầm, vấn đề sẽ nhanh chóng trở nên khó
Nói nghiêm túc thì, ngay cả không có runtime, nếu phơi bày kiểu dưới dạng dữ liệu thì vẫn có được reflection. Nhìn vào việc rất nhiều lập trình viên đã cố mô phỏng điều này, việc không làm mới giống như điều ngớ ngẩn
Kiểu ở runtime giải quyết vấn đề phải duy trì trùng lặp cả hệ kiểu cho biên dịch lẫn hệ kiểu cho xác thực dữ liệu, và có vẻ không liên quan trực tiếp tới OOP.
io-ts, thư viện workaround cho vấn đề này mà tôi thích, cũng dựa rất nhiều vào lập trình hàmNaNTrong workflow tôi từng thấy, TypeScript đã là một ngôn ngữ biên dịch xuống JS, nên đã vậy thì tốt hơn là tận dụng tối đa lợi thế đó
Nó gần với chú thích kiểu + Babel hơn, và thêm một tính năng rất hữu ích nữa là ý tưởng hay. Việc không thể chuyển chuỗi từ
JSON.parsethành một cấu trúc có kiểu một cách an toàn là điều kỳ lạ, trong khi các ngôn ngữ khác nhìn chung đều làm đượcTrước cả những điều bên dưới, đây là một chủ đề hoàn toàn hợp lệ để thảo luận và tôi không nghĩ có một đáp án đúng duy nhất một cách khách quan
TypeScript là một lớp tùy chọn trên JavaScript, ngoại trừ trường hợp
Enumxuất ra đối tượng, và mã TS sẽ thành JS nếu chỉ xóa phần kiểu. Nếu không lệch quá xa nguyên tắc này, thì yêu cầu thực chất gần với một thư viện tạo bộ tuần tự hóa/trình kiểm tra từ kiểu hơn. Những thư viện như vậy đã có rất nhiều, nên rốt cuộc yêu cầu này trông giống như muốn biến một trong số đó thành lựa chọn tiêu chuẩn chính thức.Cá nhân tôi không muốn reflection ở runtime đi vào ngôn ngữ cốt lõi của TypeScript. Ở runtime chỉ nên có JavaScript, và tôi coi trọng việc JS đầu ra vẫn dễ đọc, dễ debug ngay cả không có source map
Enumra còn có những ngoại lệ khác cũng xuất ra mã runtime, và phần lớn cũng bị xem là sai lầm. Điển hình làmodulevànamespacetừng là cấu trúc runtimeTuy vậy, trong vài năm gần đây chúng chủ yếu được dùng trong chính TypeScript, và TypeScript gần đây cũng đang rời xa chúng. Ngoài ra còn có một ngoại lệ khá phổ biến là parameter properties, tức cú pháp định nghĩa kiểu thành viên lớp ngay trong tham số constructor; có lẽ nhờ giảm bớt boilerplate lặp lại nên nó ít gây tranh cãi hơn
Thay vì cầu xin các vị thần TypeScript, có lẽ tốt hơn nên thương lượng với phía JavaScript để đưa kiểm tra kiểu vào JS
Nếu cần kiểu ở runtime thì dùng type guard, còn nếu cần thường xuyên thì dùng
io-tshoặczodđể viết kiểu dưới dạng validator/codec/schema. Trước khi JavaScript có một cách làm được đồng thuận, tôi không nghĩ đặc tả TS, trình kiểm tra kiểu và cộng đồng TS nên phải gánh phần xác thực ở runtimezodđể viết schema và validator rồi suy ra kiểu từ đó. Điều tôi thích là phần xác thực tiến hóa cùng với thay đổi kiểuCó người thấy tính năng như vậy nên đi vào ngôn ngữ, nhưng mỗi thư viện lại có nhiều lựa chọn thiết kế gây tranh cãi, nên có khi việc chọn triển khai phù hợp với yêu cầu dự án lại tốt hơn.
Tuy nhiên, các pattern như xác thực runtime của
zodvà suy ra kiểu dựa trên schema không phải lúc nào cũng ăn khớp tốt với pattern matching củats-pattern. Định nghĩa kiểu của các thư viện này giống như mê cung, và đôi khi đoạn mã tưởng như phải chạy được lại không hoạt động như mong đợi. Nếu có thể kết hợp mượt mà giữa an toàn runtime và pattern matching kiểm tra tính đầy đủ thì sẽ rất tuyệtPromise, decorator và toán tử pipe mới đều khiến tôi cảm thấy hướng triển khai đã đi saiTôi nhớ một trong các nhà phát triển TypeScript từng nói rằng nếu làm lại từ đầu thì họ đã không thêm
enum, vìenumlà tính năng duy nhất phát ra mã runtimeTypeScript không thay đổi hành vi runtime, và cũng không có thứ gì kiểu
{#if}dành riêng cho TypeScriptTuy vậy, TypeScript có vài tính năng vượt quá “JavaScript + chú thích kiểu”. Có
namespace, cú pháp định nghĩa thuộc tính lớp từ tham số constructor, các decorator thử nghiệm cũ, và cú pháp tham sốthistuy biến mất khi biên dịch nhưng trông như hơn cả việc chỉ thêm chú thích kiểu vào hàm JSTiêu đề tốt hơn có lẽ là “TypeScript, hãy cung cấp reflection/kiểu ở runtime”
Giải pháp tốt nhất hiện nay có lẽ là
emitDecoratorMetadata. https://www.typescriptlang.org/tsconfig#emitDecoratorMetadataCó vẻ tác giả đã hiểu sai mục tiêu thiết kế của TypeScript. Mục tiêu không phải là tạo ra đầu ra JS sạch sẽ, không có độ phức tạp, mà là bảo đảm ngữ nghĩa thời gian chạy của TypeScript vẫn giống hệt JavaScript
Ngoại trừ
enumlà một ngoại lệ đáng tiếc, TypeScript trở thành JavaScript chỉ bằng cách xóa các chú thích kiểu. Hệ sinh thái xung quanh TypeScript dựa vào việc xóa kiểu hoàn toàn, và nếu yêu cầu này được chấp nhận thì hỗ trợ TS của ESBuild, Deno và Bun gần như sẽ không thể thực hiện được. Vì mỗi bên sẽ phải tự triển khai lại toàn bộtscbằng ngôn ngữ của mình.Trong khi đó, các thư viện phức tạp mà OP than phiền lại được triển khai trong không gian người dùng nên vẫn tương thích với các công cụ này
keyofthành danh sách khóa của lớp, hoặc để lớp TS khởi tạo khóa thànhundefinedgiống lớp ES6Hiện tại, lớp TypeScript xóa toàn bộ khóa không được định nghĩa tường minh, nên nếu gọi
Object.keys()trên một instance mới thì sẽ không ra gì cả. Chỉ cần có một tùy chọn trongtsconfig.jsonđể chuyển lớp TS thành lớp kiểu ES6, hoặc cho phép dùng lớp ES6 cùng với TS, thì việc sinh mã sẽ dễ hơn rất nhiều.Ngoài ra, nếu có cú pháp tĩnh như
tstypeof Foo::barđược biên dịch thành"string"thì sẽ rất tuyệt. Chỉ cần có RTTI cơ bản thôi cũng có thể ngay lập tức loại bỏ rất nhiều mã TypeScript boilerplate lộn xộnRất vui khi thấy bài này. Tôi bắt đầu dùng TypeScript vào năm 2018, và khoảng 2 năm sau thì bắt đầu tin rằng về bản chất đây không phải là một lời giải hoàn chỉnh
Tôi đến từ Haskell/C#/F#, nhưng TS dù có hệ thống kiểu mạnh vẫn không mang lại được nhiều lợi ích mà các ngôn ngữ đó có, ngoại trừ một vài phần trong lúc phát triển. Khi xử lý thế giới thực, nó thực tế bị giới hạn hơn C# rất nhiều. Nếu không nhận ra rằng sau khi biên dịch nó rơi xuống JS mà không có bước kiểm tra nào nữa, thì bạn luôn phải để ý đến sự rò rỉ của tầng trừu tượng để tránh những lỗi vốn lẽ ra không thể tồn tại trong một công cụ tự nhận là có kiểu tĩnh
Nếu cần phát triển .NET thì dùng C#, còn nếu cần phát triển web thì dùng TypeScript. C# là kiểu danh nghĩa và có generic được tái hiện hóa khá tốt, còn TypeScript là kiểu cấu trúc và đánh đổi tính soundness để có một hệ thống kiểu mạnh trong việc biểu đạt các quan hệ kiểu phức tạp.
Khó nói liệu một ngôn ngữ kết hợp cả hai có thực sự tốt hay không; hai ngôn ngữ này hoạt động tốt theo những hướng khác nhau, nhưng các hướng đó lại không thật sự khớp nhau
Tôi hoàn toàn hài lòng khi dùng https://zod.dev cho việc kiểm chứng dữ liệu lúc runtime
Điều khá hay là có thể diễn đạt ý định ngay tại chỗ bằng một API fluent, mà không cần định nghĩa riêng một kiểu danh nghĩa tách biệt
Ví dụ đơn giản là trong miền nghiệp vụ, cần phân biệt số
2với số tiền tệ2