Ruby3.0 からは、型定義を処理するための rbs
gem が同梱されていて、これは外部の *.rbs ファイルに記述した内容に従って、Rubyコードの型チェックを可能にしてくれる。
最近、この RBS の型定義を TypeScript の型定義に変換できないかな〜と思い、 rbs2ts
という gem を実験的に作ってる。
結構荒削りなので、細々した部分での挙動は正直怪しいが、ある程度それっぽく動くようになったので公開してある。
Gemのいまのところの挙動
いまのところ次のような変換ができる
Alias
type TypeofInteger = Integer type TypeofFloat = Float type TypeofNumeric = Numeric type TypeofString = String type TypeofBool = Bool type TypeofVoid = void type TypeofUntyped = untyped type TypeofNil = nil
変換後TypeScript
export type TypeofInteger = number; export type TypeofFloat = number; export type TypeofNumeric = number; export type TypeofString = string; export type TypeofBool = boolean; export type TypeofVoid = void; export type TypeofUntyped = any; export type TypeofNil = null;
リテラル
type IntegerLiteral = 123 type StringLiteral = 'abc' type TrueLiteral = true type FalseLiteral = false
変換後TypeScript
export type IntegerLiteral = 123; export type StringLiteral = "abc"; export type TrueLiteral = true; export type FalseLiteral = false;
Intersection, Union
type IntersectionType = String & Integer & Bool type UnionType = String | Integer | Bool
変換後TypeScript
export type IntersectionType = string & number & boolean; export type UnionType = string | number | boolean;
Optional
type OptionalType = String?
変換後TypeScript
export type OptionalType = string | null | undefined;
Array, Tuple
type ArrayType = Array[String] type TupleType = [ ] type TupleEmptyType = [String, Integer]
変換後TypeScript
export type ArrayType = string[]; export type TupleType = []; export type TupleEmptyType = [string, number];
Record
type RecordType = { s: String, nest: { i: Integer, f: Float }? }
変換後TypeScript
export type RecordType = { s: string; next: { i: number; f: number; } | null | undefined; };
クラス
クラスっていうかメソッド。これが一番やばい。 これであってるのかホントに
class Klass attr_accessor a: String attr_reader b: Integer attr_writer c: Bool def required_positional: (String) -> void def required_positional_name: (String str) -> void def optional_positional: (?String) -> void def optional_positional_name: (?String? str) -> void def rest_positional: (*String) -> void def rest_positional_name: (*String str) -> void def rest_positional_with_trailing: (*String, Integer) -> void def rest_positional_name_with_trailing: (*String str, Integer trailing) -> void def required_keyword: (str: String) -> void def optional_keyword: (?str: String?) -> void def rest_keywords: (**String) -> void def rest_keywords_name: (**String rest) -> void end
変換後TypeScript
export declare class Klass { a: string; readonly b: number; c: boolean; requiredPositional(arg1: string): void; requiredPositionalName(str: string): void; optionalPositional(arg1?: string): void; optionalPositionalName(str?: string | null | undefined): void; restPositional(...arg1: string[]): void; restPositionalName(...str: string[]): void; restPositionalWithTrailing(arg1: string[], arg2: number): void; restPositionalNameWithTrailing(str: string[], trailing: number): void; requiredKeyword(arg1: { str: string }): void; optionalKeyword(arg1: { str?: string | null | undefined }): void; restKeywords(arg1: { [key: string]: unknown; }): void; restKeywordsName(arg1: { [key: string]: unknown; }): void; };
モジュール
module Module def func: (String, Integer) -> { str: String, int: Integer } end
変換後TypeScript
export namespace Module { export declare function func(arg1: string, arg2: number): { str: string; int: number; }; };
インタフェース
interface _Interface def func: (String, Integer) -> { str: String, int: Integer } end
変換後TypeScript
export interface Interface { func(arg1: string, arg2: number): { str: string; int: number; }; };
作ろうと思った動機
GraphQL Code Generator べんり!!
最近、ちゃんとGraphQLを使う機会があり、GraphQL Ruby によって出力されたスキーマファイルを元に GraphQL Code Generator で TypeScript 型定義に変更し、それをフロントエンドで利用するスタイルで開発していた。
この体験が非常に良くて、ある程度のバックエンド側の変更であれば、フロントエンド側への影響は TypeScript の型検査で拾うことができ、名前をタイポしてましたみたいな悲しい不具合はほぼ防げてた。
RESTつらい
GraphQLでの型の体験を味わうと、逆に次のようなものをなんとかしたくなってくる。
- REST API経由
- DOMのdata属性経由
これらは、フロントエンド側でインタフェースを独自で型定義すれば、受け取った後についてはある程度検査できるが、あくまでも独自定義なので、当然バックエンドの変更に対して自動で追従することはできなくて、人力でなんとかする必要がある。
GraphQL以外の方法
全部GraphQLに置き換えちゃえばいいじゃん、というのがまず思い浮かぶストレートな解決策なんだけど、まあそれはほら、大変ですよね。
また、別のアプローチとして OpenAPI や gRPC を利用する方法もあるはず。gRPCなんかはそこまで詳しくないので的外れなことを言ってる可能性はあるが、すでに存在するAPI群に対して後追いで適用するには少しハードルが上がるものかな〜と思っている。 もちろんゼロから構築できるのであれば積極的に導入を検討してもよさそう。(楽しそうだし)
今回は、自分自身のリアルな課題への対処として、段階的にかつコスト低く徐々に保護される範囲を広げていく手段がほしいなぁと考えてた。
RBSからいけない?
というわけで、RBSが使えないかな?と思ったのが発端。
REST用のレスポンスはPresenterクラス的なもので構築してたりするので、バックエンドではそれを RBS 型定義でチェックした上で、その RBS から TypeScript の型定義が出力できれば、現実で稼働しているAPIを大幅に変更することなく、型定義の恩恵だけいい感じに受けられるのでは??という発想。
今後
まず自分が使わなければな..と思っている。 ドッグフーディングとは 意味/解説 - シマウマ用語集
一応出力されてる型定義は TypeScript Playground にペタッとしてエラーになってないことは確認してるけど、実際に使い物になるかは本気で使ってみないとわからんなという気持ちになってる。
ともあれ、実は何気にちゃんとRubyGems作るのも初めてなので、結構楽しい。 RBSのsyntaxとかを見るとまだまだサポートできていない部分がたくさんあるので、ちまちま更新していきたい。