Ruby3.0 からは、型定義を処理するための rbs
gem が同梱されていて、これは外部の *.rbs ファイルに記述した内容に従って、Rubyコードの型チェックを可能にしてくれる。
github.com
最近、この RBS の型定義を TypeScript の型定義に変換できないかな〜と思い、 rbs2ts
という gem を実験的に作ってる。
結構荒削りなので、細々した部分での挙動は正直怪しいが、ある程度それっぽく動くようになったので公開してある。
rubygems.org
github.com
Gemのいまのところの挙動
いまのところ次のような変換ができる
Alias
RBS
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;
RBS
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
RBS
type IntersectionType = String & Integer & Bool
type UnionType = String | Integer | Bool
変換後TypeScript
export type IntersectionType = string & number & boolean;
export type UnionType = string | number | boolean;
Optional
RBS
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
RBS
type RecordType = {
s: String,
nest: {
i: Integer,
f: Float
}?
}
変換後TypeScript
export type RecordType = {
s: string;
next: {
i: number;
f: number;
} | null | undefined;
};
クラス
クラスっていうかメソッド。これが一番やばい。
これであってるのかホントに
RBS
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;
};
モジュール
RBS
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;
};
};
インタフェース
RBS
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での型の体験を味わうと、逆に次のようなものをなんとかしたくなってくる。
これらは、フロントエンド側でインタフェースを独自で型定義すれば、受け取った後についてはある程度検査できるが、あくまでも独自定義なので、当然バックエンドの変更に対して自動で追従することはできなくて、人力でなんとかする必要がある。
GraphQL以外の方法
全部GraphQLに置き換えちゃえばいいじゃん、というのがまず思い浮かぶストレートな解決策なんだけど、まあそれはほら、大変ですよね。
また、別のアプローチとして OpenAPI や gRPC を利用する方法もあるはず。gRPCなんかはそこまで詳しくないので的外れなことを言ってる可能性はあるが、すでに存在するAPI群に対して後追いで適用するには少しハードルが上がるものかな〜と思っている。
もちろんゼロから構築できるのであれば積極的に導入を検討してもよさそう。(楽しそうだし)
今回は、自分自身のリアルな課題への対処として、段階的にかつコスト低く徐々に保護される範囲を広げていく手段がほしいなぁと考えてた。
RBSからいけない?
というわけで、RBSが使えないかな?と思ったのが発端。
REST用のレスポンスはPresenterクラス的なもので構築してたりするので、バックエンドではそれを RBS 型定義でチェックした上で、その RBS から TypeScript の型定義が出力できれば、現実で稼働しているAPIを大幅に変更することなく、型定義の恩恵だけいい感じに受けられるのでは??という発想。
今後
まず自分が使わなければな..と思っている。
ドッグフーディングとは 意味/解説 - シマウマ用語集
一応出力されてる型定義は TypeScript Playground にペタッとしてエラーになってないことは確認してるけど、実際に使い物になるかは本気で使ってみないとわからんなという気持ちになってる。
ともあれ、実は何気にちゃんとRubyGems作るのも初めてなので、結構楽しい。
RBSのsyntaxとかを見るとまだまだサポートできていない部分がたくさんあるので、ちまちま更新していきたい。