ReScript言語によるJavaScript開発ガイド
ReScriptは、JavaScriptにコンパイルされる型安全な関数型プログラミング言語です。OCamlをベースにしており、Reactとの親和性が高く、型安全性とパフォーマンスを両立させた開発が可能です。本記事では、ReScriptの基礎から実践的な使い方まで詳しく解説します。
ReScriptとは
ReScriptは、元々BuckleScriptとして知られていたOCamlからJavaScriptへのコンパイラです。2020年にReScriptとしてリブランドされ、独自の言語仕様を持つようになりました。
主な特徴
1. 100%型安全
- null/undefinedの問題を言語レベルで解決
- 実行時エラーをコンパイル時に検出
- 完全な型推論
2. 高速なコンパイル
- インクリメンタルコンパイルにより数ミリ秒でコンパイル完了
- 大規模プロジェクトでも高速
3. 読みやすいJavaScript出力
- 手書きのJavaScriptと同等の可読性
- デバッグが容易
- バンドルサイズが小さい
4. JavaScriptとの相互運用性
- 既存のJavaScriptライブラリを簡単に利用可能
- 段階的な導入が可能
セットアップ
プロジェクトの作成
# 新規プロジェクト作成
npm create rescript-app@latest my-rescript-app
cd my-rescript-app
npm install
プロジェクト構造
my-rescript-app/
├── src/
│ └── Demo.res
├── bsconfig.json
├── package.json
└── index.html
bsconfig.jsonはReScriptコンパイラの設定ファイルです。
{
"name": "my-rescript-app",
"version": "0.1.0",
"sources": {
"dir": "src",
"subdirs": true
},
"package-specs": {
"module": "es6",
"in-source": true
},
"suffix": ".bs.js",
"bs-dependencies": []
}
基本文法
変数とデータ型
// 変数宣言
let greeting = "Hello"
let age = 25
let isActive = true
// 型注釈(オプション)
let name: string = "Alice"
let count: int = 10
// letバインディングは不変
// greeting = "Hi" // エラー!
// 可変変数が必要な場合
let message = ref("Initial")
message := "Updated"
Console.log(message.contents)
関数
// 基本的な関数
let add = (x, y) => x + y
// 型注釈付き
let multiply = (x: int, y: int): int => x * y
// 複数行の関数
let greet = name => {
let message = `Hello, ${name}!`
Console.log(message)
message
}
// カリー化された関数
let addThree = (x, y, z) => x + y + z
let addFive = addThree(2, 3)
let result = addFive(5) // 10
// パイプ演算子
let result = 5
->multiply(3)
->add(2)
->Int.toString
// "17"
Option型
null/undefinedの代わりにOption型を使用します。
// Option型の定義
type option<'a> = None | Some('a)
// 使用例
let findUser = (id: int): option<string> => {
if id == 1 {
Some("Alice")
} else {
None
}
}
// パターンマッチング
let userName = findUser(1)
switch userName {
| Some(name) => Console.log(`Found: ${name}`)
| None => Console.log("User not found")
}
// Option関数
let user = findUser(1)
let displayName = user->Option.getWithDefault("Unknown")
Result型
エラーハンドリングにはResult型を使用します。
type result<'a, 'b> = Ok('a) | Error('b)
let divide = (a: float, b: float): result<float, string> => {
if b == 0.0 {
Error("Division by zero")
} else {
Ok(a /. b)
}
}
let result = divide(10.0, 2.0)
switch result {
| Ok(value) => Console.log(`Result: ${Float.toString(value)}`)
| Error(msg) => Console.log(`Error: ${msg}`)
}
レコードとバリアント
// レコード型
type user = {
id: int,
name: string,
email: string,
}
let alice = {
id: 1,
name: "Alice",
email: "alice@example.com",
}
// イミュータブルな更新
let updatedAlice = {...alice, email: "newalice@example.com"}
// バリアント型
type color = Red | Green | Blue | Custom(string)
let toHex = color =>
switch color {
| Red => "#FF0000"
| Green => "#00FF00"
| Blue => "#0000FF"
| Custom(hex) => hex
}
React統合
ReScriptはReactとの統合が非常に優れています。
セットアップ
npm install @rescript/react
bsconfig.jsonに依存を追加:
{
"bs-dependencies": ["@rescript/react"],
"jsx": { "version": 4 }
}
Reactコンポーネント
// Button.res
module Button = {
@react.component
let make = (~text: string, ~onClick: unit => unit) => {
<button onClick={_ => onClick()}>
{React.string(text)}
</button>
}
}
// App.res
module App = {
@react.component
let make = () => {
let (count, setCount) = React.useState(() => 0)
let handleClick = () => {
setCount(prev => prev + 1)
}
<div>
<h1>{React.string("Counter App")}</h1>
<p>{React.string(`Count: ${Int.toString(count)}`)}</p>
<Button text="Increment" onClick={handleClick} />
</div>
}
}
Hooks
// カスタムフック
let useFetch = (url: string) => {
let (data, setData) = React.useState(() => None)
let (loading, setLoading) = React.useState(() => true)
let (error, setError) = React.useState(() => None)
React.useEffect(() => {
setLoading(_ => true)
Fetch.fetch(url)
->Promise.then(Response.json)
->Promise.then(json => {
setData(_ => Some(json))
setLoading(_ => false)
Promise.resolve()
})
->Promise.catch(err => {
setError(_ => Some(err))
setLoading(_ => false)
Promise.resolve()
})
->ignore
None
}, [url])
(data, loading, error)
}
// 使用例
@react.component
let make = () => {
let (data, loading, error) = useFetch("https://api.example.com/users")
if loading {
<div>{React.string("Loading...")}</div>
} else {
switch (data, error) {
| (Some(d), _) => <div>/* データ表示 */</div>
| (_, Some(e)) => <div>{React.string("Error occurred")}</div>
| _ => <div>{React.string("No data")}</div>
}
}
}
JavaScript相互運用
JavaScriptからReScriptへ
// 外部JavaScript関数をバインド
@val external alert: string => unit = "alert"
@val external setTimeout: (unit => unit, int) => unit = "setTimeout"
// オブジェクトメソッド
@send external push: (array<'a>, 'a) => unit = "push"
// オブジェクト作成
type config = {
apiKey: string,
timeout: int,
}
@module("some-library") external init: config => unit = "init"
// 使用例
init({apiKey: "abc123", timeout: 5000})
JavaScriptライブラリのバインディング
// axios.res
type response<'a> = {
data: 'a,
status: int,
}
@module("axios")
external get: string => Promise.t<response<'a>> = "get"
@module("axios")
external post: (string, 'a) => Promise.t<response<'b>> = "post"
// 使用例
let fetchUser = async (id: int) => {
try {
let response = await get(`https://api.example.com/users/${Int.toString(id)}`)
Console.log(response.data)
} catch {
| error => Console.error(error)
}
}
実践例: Todoアプリ
// Todo.res
type todo = {
id: int,
text: string,
completed: bool,
}
module TodoItem = {
@react.component
let make = (~todo: todo, ~onToggle: int => unit, ~onDelete: int => unit) => {
<li>
<input
type_="checkbox"
checked={todo.completed}
onChange={_ => onToggle(todo.id)}
/>
<span
style={ReactDOM.Style.make(
~textDecoration=todo.completed ? "line-through" : "none",
(),
)}>
{React.string(todo.text)}
</span>
<button onClick={_ => onDelete(todo.id)}>
{React.string("Delete")}
</button>
</li>
}
}
module TodoApp = {
@react.component
let make = () => {
let (todos, setTodos) = React.useState(() => [])
let (input, setInput) = React.useState(() => "")
let (nextId, setNextId) = React.useState(() => 1)
let addTodo = () => {
if input != "" {
let newTodo = {
id: nextId,
text: input,
completed: false,
}
setTodos(prev => Array.concat(prev, [newTodo]))
setNextId(prev => prev + 1)
setInput(_ => "")
}
}
let toggleTodo = (id: int) => {
setTodos(prev =>
prev->Array.map(todo =>
if todo.id == id {
{...todo, completed: !todo.completed}
} else {
todo
}
)
)
}
let deleteTodo = (id: int) => {
setTodos(prev => prev->Array.filter(todo => todo.id != id))
}
<div>
<h1>{React.string("Todo App")}</h1>
<div>
<input
type_="text"
value={input}
onChange={e => setInput(_ => ReactEvent.Form.target(e)["value"])}
placeholder="Enter todo..."
/>
<button onClick={_ => addTodo()}>
{React.string("Add")}
</button>
</div>
<ul>
{todos
->Array.map(todo =>
<TodoItem
key={Int.toString(todo.id)}
todo
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
)
->React.array}
</ul>
</div>
}
}
ベストプラクティス
1. 型を活用する
// 文字列より具体的な型を使う
type userId = UserId(int)
type email = Email(string)
type user = {
id: userId,
email: email,
name: string,
}
let createUser = (id: int, email: string, name: string): user => {
{
id: UserId(id),
email: Email(email),
name: name,
}
}
2. パターンマッチングを活用
// 網羅的なチェック
type status = Idle | Loading | Success(string) | Error(string)
let renderStatus = status =>
switch status {
| Idle => <div>{React.string("Ready")}</div>
| Loading => <div>{React.string("Loading...")}</div>
| Success(data) => <div>{React.string(data)}</div>
| Error(msg) => <div>{React.string(`Error: ${msg}`)}</div>
}
3. パイプライン演算子
// データ変換のパイプライン
let processData = data =>
data
->Array.filter(x => x > 0)
->Array.map(x => x * 2)
->Array.reduce(0, (acc, x) => acc + x)
まとめ
ReScriptは型安全性とパフォーマンスを重視したJavaScript開発を実現する優れた選択肢です。主な利点は以下の通りです。
- 型安全性: コンパイル時にほとんどのエラーを検出
- 高速: ミリ秒単位のコンパイル時間
- JavaScript互換: 既存のエコシステムを活用可能
- React統合: 優れたReactサポート
学習コストはありますが、大規模プロジェクトや長期運用を考えると、ReScriptの投資価値は高いでしょう。