paizaスキルチェックをTypeScriptで解く方法 ── 実務スキルとアルゴリズム力を同時に鍛える

paizaスキルチェックはTypeScriptに対応していない。。

対応言語の一覧にはJava、Python、C++、JavaScript などが並ぶが、TypeScriptの名前はない。

しかし実務のWeb開発現場ではTypeScriptが事実上の標準になりつつある。React、Next.js、Nuxt、Angular──どのフレームワークを使っていても、型のある世界で書くのが当たり前になった。せっかくpaizaでアルゴリズム力を鍛えるなら、実務で使っているTypeScriptで書きたいと思うのは自然なことだ。

この記事では、ローカル環境でTypeScriptを書き、JavaScriptにコンパイルしてpaizaに提出するワークフローを紹介する。TypeScriptの「コンパイル(トランスパイル)」の仕組みそのものについても解説するので、ふだん何気なく使っているビルドプロセスへの理解も深まるはずだ。

目次

なぜpaizaスキルチェックをTypeScriptで解くのか

Web開発の実務でTypeScriptを使っている人にとって、わざわざ素のJavaScriptに戻すのはストレスになる。型注釈がないと変数に何が入っているか追いにくいし、関数のシグネチャも読みづらい。特にB〜Aランク以上の問題では入力データの構造が複雑になるため、型があるだけでバグの混入を減らせる。

さらに、TypeScriptで書いてからJavaScriptにコンパイルするという一連の流れ自体が、フロントエンド開発のビルドプロセスへの理解に直結する。つまりpaizaの問題を解きながら、TypeScriptのコンパイルの仕組みも学べるという一石二鳥の状態が作れる。

TypeScriptのコンパイル(トランスパイル)とは何か

TypeScriptのコードはそのままでは実行できない。ブラウザもNode.jsもJavaScriptしか解釈しないため、TypeScriptからJavaScriptへの変換が必要になる。

この変換のことを「コンパイル」あるいは「トランスパイル」と呼ぶ。

コンパイル時に何が起きているのか

TypeScriptコンパイラ(tsc)は、大きく分けて2つの仕事をしている。

型チェック
コード全体を解析し、型の不整合がないかを検証する。
ランタイムではなくビルド時にエラーを検出できるのがTypeScriptの最大の強みだ。

型情報の除去とJavaScript生成
型チェックが終わると、型注釈(: string: numberinterfacetypeなど)をすべて取り除き、純粋なJavaScriptコードを出力する。つまりTypeScriptの型はあくまで開発時の安全装置であり、実行時のJavaScriptには一切残らない。

// TypeScript(コンパイル前)
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// JavaScript(コンパイル後)
function greet(name) {
  return `Hello, ${name}!`;
}

型注釈が消えただけで、ロジックはそのまま保持される。これがトランスパイルの実態だ。

tsconfig.json の役割

tsconfig.json はTypeScriptコンパイラの設定ファイルで、プロジェクトのルートに置く。コンパイル対象のファイル、出力先ディレクトリ、対象とするECMAScriptのバージョン、型チェックの厳密さなどを定義する。

paizaスキルチェック用途で特に意識すべき設定項目は以下の通り。

target ── 出力されるJavaScriptのECMAScriptバージョン。paizaのNode.js環境に合わせて ES2020 程度にしておけば安心だ。

types ── コンパイル時に参照する型定義パッケージを明示的に指定する。["node"] と書くと、@types/node(Node.jsのAPI型定義)だけを読み込む。この指定がないと、node_modules/@types/ 配下にあるすべての型定義パッケージが自動で読み込まれる。paizaの問題を解く用途ではNode.jsのAPIさえ使えれば十分なので、["node"] に限定しておくとコンパイルが速くなり、意図しない型の衝突も防げる。たとえばブラウザ向けの型定義(@types/react など)がプロジェクトに混在していても、types: ["node"] と書いておけばそれらは無視される。

module ── モジュールシステムの指定。paizaでは require が使えるので commonjs が無難。

strict ── true にすると厳密な型チェックが有効になる。TypeScriptの恩恵を最大限に受けるためにはオンにすべき。

outDir ── コンパイル後のJSファイルの出力先ディレクトリ。

rootDir ── TypeScriptソースファイルのルートディレクトリ。

環境構築の手順

プロジェクトの初期化

任意のディレクトリを作成し、npmプロジェクトとして初期化する。

mkdir paiza-ts
cd paiza-ts
npm init -y
npm install typescript @types/node --save-dev

@types/node は Node.js のAPIに型定義を付与するパッケージで、process.stdinrequire('readline') などpaizaの標準入力処理に必要な型情報が含まれる。

tsconfig.json の作成

npx tsc --init

このコマンドで雛形が生成されるので、以下のように編集する。

{
  "compilerOptions": {
    "target": "ES2020",
    "types": ["node"],
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

ディレクトリ構成

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);
    }
  }
});

型注釈があることで、gridnumber[][]operations(string | number)[][] であることが明示される。変数に何が入っているか一目瞭然で、配列の添字ミスや型の混在に起因するバグを未然に防げる。

コンパイルしてpaizaに提出

npm run build

dist/B123.js が生成されるので、その中身をpaizaのエディタにコピー&ペーストして提出する。

ウォッチモードで効率化

問題を解いている最中は、ウォッチモードを起動しておくと便利だ。

npm run watch

TypeScriptファイルを保存するたびに自動でJSが生成されるので、コンパイルコマンドを毎回手動で叩く必要がない。

tsc以外のコンパイル方法 ── esbuild

tsc は型チェックとコンパイルを両方行うため、ファイル数が増えると速度が気になることがある。型チェックだけ tsc に任せて、実際のJS生成は別のツールに委ねるというアプローチも実務ではよく使われる。

その代表格が esbuild だ。Go言語で書かれた高速バンドラーで、TypeScriptのトランスパイルにも対応している。

npm install esbuild --save-dev
{
  "scripts": {
    "build": "esbuild src/B123.ts --outfile=dist/B123.js --platform=node --format=cjs",
    "typecheck": "tsc --noEmit"
  }
}

npm run build で高速にJSを生成し、型チェックが必要なときだけ npm run typecheck を走らせる。paizaの問題を解くときはこのくらい軽量なセットアップのほうが快適だ。

esbuildは型チェックを行わないので、型の恩恵を受けるにはエディタ(VS Codeなど)のリアルタイム型チェックに頼るか、提出前に tsc --noEmit を一度実行するとよい。

paizaスキルチェックを解くときのコツ

標準入力のテンプレートを用意しておく

paizaの問題では毎回標準入力の処理が必要になる。TypeScriptで型付きのテンプレートを作っておくと、問題ごとに入力パース部分で悩む時間を減らせる。

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', () => {
  // ここに処理を書く
});

これをスニペットとしてエディタに登録しておくと、問題を開いた瞬間に展開できる。

ローカルでテストしてから提出する

paizaのスキルチェックは提出が一度きり(再チャレンジしてもランクには反映されない)なので、ローカルで入力例を使って十分にテストしてから提出するのが鉄則だ。

テスト用の入力ファイルを作成し、パイプで渡すだけでよい。

node dist/B123.js < test/input01.txt

期待出力と実際の出力を目視で比較する。余裕があれば、複数のテストケースを用意してエッジケースも確認しておく。

問題文を読み解く力が最重要

paizaの問題で最もつまずきやすいのは、実はコーディングそのものではなく問題文の読解だ。とくにB〜Aランクの問題は条件が複雑で、読み落としが致命的なミスにつながる。

問題文を読んだらまず入力例と出力例を手作業でトレースし、自分の理解が正しいか確認する。いきなりコードを書き始めるより、紙やメモに処理の流れを書き出してからキーボードに向かうほうが結果的に速い。

計算量を意識する

D〜Cランクではほぼ問題にならないが、Bランク以上になると計算量(時間計算量・空間計算量)を意識しないとテストケースがタイムアウトするケースが出てくる。二重ループで O(n^2) になっているところを MapSet を使って O(n) に落とせないか検討する癖をつけておくとよい。

使い慣れたメソッドの引き出しを増やす

JavaScriptの配列メソッド(map, filter, reduce, sort, find, some, every など)は、paizaの問題を簡潔に解く上で強力な武器になる。TypeScriptで書くと引数の型が明示されるので、各メソッドのコールバックの型を意識しながら使えるようになり、理解がより深まる。

まとめ

paizaスキルチェックはTypeScriptに直接対応していないが、ローカルでコンパイルしてJSを提出する方法で問題なく運用できる。

この運用には副次的なメリットも大きい。TypeScriptのコンパイル(トランスパイル)がどういう仕組みで動いているのか、tsconfig.json の各オプションが何を制御しているのか、tscesbuild の違いは何か──こうしたことを、問題を解く中で自然と体験的に学べる。

実務でTypeScriptを使っているなら、アルゴリズム練習も同じ言語で統一するのは合理的な選択だ。型の恩恵でバグを減らしつつ、ビルドプロセスの理解も深まる。

目次