paizaスキルチェックはTypeScriptに対応していないけど、TypeScriptを使いたい

この記事では、ローカル環境でTypeScriptを書き、JavaScriptにコンパイルしてpaizaに提出するワークフローを紹介する。
なぜpaizaスキルチェックをTypeScriptで解くのか
Web開発の実務でTypeScriptを使っている人にとって、わざわざ素のJavaScriptに戻すのはストレスになる。
型注釈がないと変数に何が入っているか追いにくいし、関数のシグネチャも読みづらい。特にB〜Aランク以上の問題では入力データの構造が複雑になるため、型があるだけでバグの混入を減らせる。
TypeScriptのコンパイル(トランスパイル)とは何か
TypeScriptのコードはそのままでは実行できない。ブラウザもNode.jsもJavaScriptしか解釈しないため、TypeScriptからJavaScriptへの変換が必要になる。
この変換のことを「コンパイル」あるいは「トランスパイル」と呼ぶ。
コンパイル時に何が起きているのか
TypeScriptコンパイラ(tsc)は、大きく分けて2つの仕事をしている。
型チェック
コード全体を解析し、型の不整合がないかを検証する。
ランタイムではなくビルド時にエラーを検出できるのがTypeScriptの最大の強みだ。
型情報の除去とJavaScript生成
型チェックが終わると、型注釈(: string、: number、interface、typeなど)をすべて取り除き、純粋なJavaScriptコードを出力する。つまりTypeScriptの型はあくまで開発時の安全装置であり、実行時のJavaScriptには一切残らない。
// TypeScript(コンパイル前)
function greet(name: string): string {
return `Hello, ${name}!`;
}
// JavaScript(コンパイル後)
function greet(name) {
return `Hello, ${name}!`;
}
型注釈が消えただけで、ロジックはそのまま保持される。これがトランスパイルの実態だ。
環境構築の手順
プロジェクトの初期化
任意のディレクトリを作成し、npmプロジェクトとして初期化する。
mkdir paiza-ts
cd paiza-ts
npm init -y
npm install typescript @types/node --save-dev
@types/node は Node.js のAPIに型定義を付与するパッケージで、process.stdin や require('readline') などpaizaの標準入力処理に必要な型情報が含まれる。
tsconfig.json の作成
npx tsc --init
このコマンドで雛形が生成されるので、以下のように編集する。
{
"compilerOptions": {
"rootDir": "./ts/src",
"outDir": "./ts/dist",
// Environment Settings
// See also https://aka.ms/tsconfig/module
// module: paizaの提出環境はNode.js v16のCommonJS。出力をrequire/exports形式にする
"module": "commonjs",
"target": "es2020",
"types": ["node"],
// esModuleInterop: import fs from 'fs' のようにdefault importでCommonJSモジュールを読める
"esModuleInterop": true,
// For nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// and npm install -D @types/node
// Other Outputs
// sourceMap: デバッグ時にTSの行番号で追える
"sourceMap": true,
// declaration: .d.tsはライブラリ配布用の型定義。paizaに単独のファイルとして提出するだけなので不要
"declaration": false,
// declarationMap: .d.tsのソースマップ。declarationをfalseにしたので不要
"declarationMap": false,
// Stricter Typechecking Options
"noUncheckedIndexedAccess": true,
// Style Options
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// Recommended Options
"strict": true,
"jsx": "react-jsx",
// verbatimModuleSyntax: trueだとCommonJS出力でimport/exportがエラーになる。tscにrequireへの変換を任せる
"verbatimModuleSyntax": false,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}
| 設定項目 | 設定値 | 役割 |
|---|---|---|
rootDir | ./ts/src | TypeScriptソースファイルのルートディレクトリを指定する。 |
outDir | ./ts/dist | コンパイル後のJavaScriptファイルの出力先を指定する。 |
module | commonjs | 出力するモジュール形式を指定する。import/exportをrequire/exports形式へ変換する。 |
target | es2020 | コンパイル後のJavaScriptで使用するECMAScriptのバージョンを指定する。 |
types | ["node"] | 読み込む型定義をNode.js用の@types/nodeに限定する。 |
esModuleInterop | true | CommonJSモジュールをimport fs from "fs"のようなdefault importで扱いやすくする。 |
sourceMap | true | デバッグ時に、コンパイル前のTypeScriptの行番号を追えるようにする。 |
declaration | false | .d.ts型定義ファイルを生成しない。ライブラリ配布をしない場合は通常不要。 |
declarationMap | false | .d.ts用のソースマップを生成しない。 |
noUncheckedIndexedAccess | true | 配列やオブジェクトをインデックスで取得した値にundefinedの可能性を含め、より厳密にチェックする。 |
exactOptionalPropertyTypes | true | ?付きの省略可能プロパティと、明示的なundefinedを区別する。 |
strict | true | TypeScriptの厳密な型チェックをまとめて有効にする。 |
jsx | react-jsx | JSXをReactの新しいJSX変換方式で処理する。 |
verbatimModuleSyntax | false | ESMのimport/exportをそのまま維持する制約を無効にし、CommonJS向けのrequire/exportsへの変換をTypeScriptに任せる。 |
isolatedModules | true | 各ファイルを単独で変換できない書き方をチェックする。 |
noUncheckedSideEffectImports | true | 副作用目的のimport "./sample"でも、モジュールが存在するかチェックする。 |
moduleDetection | force | すべてのTypeScriptファイルをモジュールとして扱う。 |
skipLibCheck | true | node_modulesなどに含まれる.d.tsファイル内部の型チェックを省略する。 |
ディレクトリ構成
paiza-ts/
├── src/ ← TypeScriptファイルを置く
│ └── B123.ts
├── dist/ ← コンパイル後のJSが出力される
│ └── B123.js
├── tsconfig.json
└── package.json
package.json にスクリプトを追加
{
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
}
}
npm run build でコンパイル、npm run watch でファイル変更を監視して自動コンパイルが走る。
TypeScriptで問題を解く
src/ ディレクトリにTypeScriptファイルを作成し、型付きで問題を解く。
// src/B123.ts
const readline = require('readline');
const reader = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const lines: string[] = [];
reader.on('line', (line: string) => {
lines.push(line);
});
reader.on('close', () => {
const [rowCount, opCount] = lines[0].split(' ').map(Number);
const grid: number[][] = lines
.slice(1, rowCount + 1)
.map((line) => line.split(' ').map(Number));
const operations: (string | number)[][] = lines
.slice(rowCount + 1, rowCount + opCount + 1)
.map((line) =>
line.split(' ').map((item) =>
item === '0' || item === '1' ? Number(item) : item
)
);
// ここにロジックを書く
const applyOp = (row: number[], op: (string | number)[]): number[] => {
const result: number[] = [];
for (let i = 0; i < row.length; i++) {
if (op[0] === 'a') {
result[i] = row[i] === 1 || op[i + 1] === 1 ? 1 : 0;
} else if (op[0] === 'b') {
result[i] = row[i] === 0 || op[i + 1] === 0 ? 0 : 1;
} else {
result[i] = row[i] !== op[i + 1] ? 1 : 0;
}
}
return result;
};
for (const op of operations) {
for (let k = 0; k < grid.length; k++) {
grid[k] = applyOp(grid[k], op);
}
}
const scores = grid.map((r) => Number(r.join('')));
const maxScore = Math.max(...scores);
for (let i = 0; i < scores.length; i++) {
if (scores[i] === maxScore) {
console.log(i + 1);
}
}
});
tsで実行すると、スタックトレースもtsファイルのどこかを出力されます。
npx tsx src/xx.ts < inputxx.txtコンパイルしてpaizaに提出
npm run builddist/B123.js が生成されるので、その中身をpaizaのエディタにコピー&ペーストして提出する。
ウォッチモードで効率化
問題を解いている最中は、ウォッチモードを起動しておくと便利だ。
npm run watch
TypeScriptファイルを保存するたびに自動でJSが生成されるので、コンパイルコマンドを毎回手動で叩く必要がない。