1 điểm bởi GN⁺ 2023-07-09 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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

 
GN⁺ 2023-07-09
Ý 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

    • Đây không hẳn là yêu cầu an toàn kiểu dữ liệu ở runtime, mà gần hơn với việc muốn phản chiếu kiểu dữ liệu ở thời điểm biên dịch để tạo ra giá trị và dùng thông tin đó ở runtime
      Ví dụ, họ muốn có khả năng tạo một hàm validate tổ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ền T là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ó
    • Chỉ cần nhìn tiêu đề là đã nhận ra ngay đây là tính năng tôi mong muốn từ rất lâu, và nó gần với việc đòi hỏi https://github.com/rbuckton/reflect-metadata theo hướng chính thức hơn và được hỗ trợ tốt hơn
      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 Reflect là 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 thi
    • Nói rằng TypeScript là compiler nên không làm được là không đúng. Chỉ cần hỗ trợ một thư viện reflection xuất thông tin kiểu dữ liệu thành object JS rồi tra cứu ở runtime là được
      TS enum hiệ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ó
    • Chỉ lướt qua cũng thấy yêu cầu khá rõ. Họ muốn TypeScript xuất thông tin kiểu dữ liệu mà nó phát hiện được trong quá trình type erasure sang một kênh phụ đặt cạnh JavaScript được tạo ra
      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
    • Ngay đầu README có liên kết đến “một issue GitHub 7 năm tuổi”, nơi vấn đề được giải thích trực diện hơn
      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ý

    • Tôi mới bắt đầu học lập trình chưa lâu nên thắc mắc vì sao TypeScript không thể tạo ra các kiểu dữ liệu do người dùng định nghĩa tinh vi hơn
      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
    • Nếu compiler TypeScript có plugin tiền xử lý chính thức thì có thể sẽ hữu ích
      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 hay
    • Nếu Microsoft host MacroScript như một plugin TypeScript hoặc wrapper cấp cao nhất, vấn đề này có thể được giải quyết
      Chỉ 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
    • Tôi tò mò mọi người nghĩ sao về đề xuất khác là giữ lại thông tin kiểu dữ liệu trên object Class sau khi biên dịch
  • Có 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

    • Không hẳn vậy. Nếu một macro như generateTypeInfo!() được mở rộng thành một đối tượng JS mã hóa kiểu Foo thì nó vẫn biên dịch thành JavaScript có thể đọc được
      Tuy 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ó
    • Có thể có cả hai. Có thể hình dung một thế giới nơi mã và dữ liệu ở gần nhau như tính đồng hình của Lisp
      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
    • Tôi không thật sự hiểu lập luận đó. TypeScript vốn đã là một ngôn ngữ siêu tập mới biên dịch xuống JS
      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àm
    • Với các dự án đủ lớn, ngôn ngữ kiểu động không tốt và gần như là một thảm họa rối rắm, liên tục vấp phải những giá trị ngoài dự kiến như NaN
      Trong 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ế đó
    • TypeScript không đơn thuần là JavaScript có chú thích kiểu. Nó có thể là một trong các chế độ, nhưng còn có khả năng xuất ra các cấu trúc JS cũ nên có thể trông hoàn toàn khác với mã TS gốc
      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.parse thà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 được
  • Trướ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 Enum xuấ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

    • Ngoài Enum ra 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à modulenamespace từng là cấu trúc runtime
      Tuy 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
    • Câu “mã TS trở thành JS mà không cần biến đổi” là điều có thể nói được nếu nhìn ngôn ngữ tổng thể và phần ký hiệu kiểu theo cách đó
  • 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-ts hoặc zod để 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 ở runtime

    • Dù không hoàn hảo, tôi khá hài lòng với cách dùng zod để 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ểu
      Có 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 zod và 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ủa ts-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ệt
    • Tôi rất nghi ngờ việc JavaScript thực sự nên có tính năng như vậy
      Promise, decorator và toán tử pipe mới đều khiến tôi cảm thấy hướng triển khai đã đi sai
  • Tô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ì enum là tính năng duy nhất phát ra mã runtime
    TypeScript không thay đổi hành vi runtime, và cũng không có thứ gì kiểu {#if} dành riêng cho TypeScript

    • Điều đó là đúng. Tôi không biết đó có phải lý do duy nhất hay không, nhưng chắc chắn là một trong các lý do
      Tuy 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ố this tuy 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 JS
  • Tiê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#emitDecoratorMetadata

  • Có 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ừ enum là 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ộ tsc bằ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

    • Có nhiều cách để xuất thông tin kiểu thời gian chạy theo cách tĩnh mà không cần runtime. Có thể biến keyof thành danh sách khóa của lớp, hoặc để lớp TS khởi tạo khóa thành undefined giống lớp ES6
      Hiệ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 trong tsconfig.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ộn
  • Rấ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

    • Mục đích của TypeScript từ đầu luôn là chạy trên web, chứ không phải tạo ưu thế ngôn ngữ so với C#
      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

    • Kiểu danh nghĩa có công dụng khác
      Ví dụ đơn giản là trong miền nghiệp vụ, cần phân biệt số 2 với số tiền tệ 2