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

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: numberinterfacetypeなど)をすべて取り除き、純粋な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.stdinrequire('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/srcTypeScriptソースファイルのルートディレクトリを指定する。
outDir./ts/distコンパイル後のJavaScriptファイルの出力先を指定する。
modulecommonjs出力するモジュール形式を指定する。import/exportrequire/exports形式へ変換する。
targetes2020コンパイル後のJavaScriptで使用するECMAScriptのバージョンを指定する。
types["node"]読み込む型定義をNode.js用の@types/nodeに限定する。
esModuleInteroptrueCommonJSモジュールをimport fs from "fs"のようなdefault importで扱いやすくする。
sourceMaptrueデバッグ時に、コンパイル前のTypeScriptの行番号を追えるようにする。
declarationfalse.d.ts型定義ファイルを生成しない。ライブラリ配布をしない場合は通常不要。
declarationMapfalse.d.ts用のソースマップを生成しない。
noUncheckedIndexedAccesstrue配列やオブジェクトをインデックスで取得した値にundefinedの可能性を含め、より厳密にチェックする。
exactOptionalPropertyTypestrue?付きの省略可能プロパティと、明示的なundefinedを区別する。
stricttrueTypeScriptの厳密な型チェックをまとめて有効にする。
jsxreact-jsxJSXをReactの新しいJSX変換方式で処理する。
verbatimModuleSyntaxfalseESMのimport/exportをそのまま維持する制約を無効にし、CommonJS向けのrequire/exportsへの変換をTypeScriptに任せる。
isolatedModulestrue各ファイルを単独で変換できない書き方をチェックする。
noUncheckedSideEffectImportstrue副作用目的のimport "./sample"でも、モジュールが存在するかチェックする。
moduleDetectionforceすべてのTypeScriptファイルをモジュールとして扱う。
skipLibChecktruenode_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 build

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

ウォッチモードで効率化

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

npm run watch

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

命名規則など

https://github.com/idw-coder/paiza/blob/main/docs/code_review.md#%E4%BB%A5%E4%B8%8B%E3%81%AE%E9%81%8E%E5%8E%BB%E3%81%AE%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%81%A7%E8%89%AF%E3%81%8B%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%E3%82%92%E5%8F%82%E8%80%83%E3%81%AB

目次