この記事を書いたきっかけ
「TypeScriptって何ですか」と聞かれたとき、「JavaScriptに型をつけたものです」と答えていた。間違いではないが、それだけでは伝えきれていないものがある気がずっとしていた。
チームの新しいメンバーに説明する機会があり、改めて「なぜTypeScriptを使うのか」を整理することにした。使い方の話ではなく、背景と設計思想から理解し直してみた記録だ。
まず:TypeScriptはAltJSの一種
TypeScriptはAltJS(Alternative JavaScript)と呼ばれるカテゴリに属する。直接ブラウザで実行できるわけではなく、コンパイルしてJavaScriptに変換して使う。
同じカテゴリにCoffeeScriptなどがあるが、現在の主流はTypeScriptになっている。マイクロソフトが開発したオープンソース言語で、JavaScriptの上位互換(スーパーセット)として設計されている。つまり既存のJavaScriptコードはそのままTypeScriptとして有効だ。
なぜJavaScriptだけでは辛くなるのか
TypeScriptが生まれた背景は、JavaScriptの規模が大きくなったときの辛さにある。
型定義がないため、意図しない値が関数に渡されても実行時まで気づけない。null や undefined が予期しないタイミングで現れる。プロパティ名を誤記しても、エラーになるのは実行してから。エディタの補完もほぼ効かない。
小さなスクリプトなら気にならないが、複数人で数千行を超えるコードを書いていくと、これらが積み重なって保守コストが跳ね上がる。TypeScriptはこの問題に対する答えとして設計されている。
型システムの実際
基本的な変数の型宣言
let name: string;
name = "taro";
name = 0; // エラー
配列
const fruits: string[] = [];
fruits.push("apple");
fruits.push(1); // エラー
関数の引数と返り値
function getAge(id: string): number {
return 20;
}
const age = getAge("xxx");
console.log(age.length); // エラー:number型にlengthはない
返り値の型を明示することで、関数の呼び出し側だけでなく、関数内の return の型もチェックされる。「返り値に型をつけることで、実装時のミスも検出できる」という点は、書いていて実際に助かった場面があった。
型推論の範囲を把握しておく
型を省略しても、初期値から推論してくれる。
const age = 10;
console.log(age.length); // エラー:number型と推論される
const user = { name: "taro", age: 20 };
console.log(user.age.length); // エラー:ageはnumber型
関数の返り値も同様に推論される。ただし、型を明示しないと「返り値が何の型か」がコードを読んだだけではわかりにくくなるため、公開APIや複雑な処理の関数では明示した方が可読性が上がると感じている。
union型と文字列リテラル型
union型:複数の型のどちらかを受け付ける。
let nameOrAge: string | number = "taro";
nameOrAge = 20;
nameOrAge = true; // エラー
if (typeof nameOrAge === "string") {
console.log(nameOrAge.length); // このブロック内はstring型として扱われる
}
型ガード(typeof による絞り込み)を使うと、ブロック内で自動的に型が確定する。これを「型の絞り込み」と呼ぶ。
文字列リテラル型:決まった文字列だけを許容する。
let status: "create" | "edit" | "view";
status = "creata"; // エラー:スペルミスを検出できる
ステータス管理やAPIレスポンスの区分値に使うと、タイポによるバグをコンパイル時に潰せる。これは実務でかなり助かっている機能だ。
Utility Types:既存の型から新しい型を作る
TypeScriptには、既存の型を変形して新しい型を作るUtility Typesが用意されている。よく使うのは Omit と Partial だ。
type UserType = {
name: string;
age: number;
};
type AgeOnly = Omit<UserType, 'name'>;
// → { age: number } だけの型
type PartialUser = Partial<UserType>;
// → { name?: string; age?: number } (全プロパティがオプショナルになる)
フォームの入力途中の状態(全フィールドが埋まっていない)を Partial で表現するパターンは、実装していて「なるほど」となった。型を一から書き直さず、既存の型を組み合わせて意図を表現できる。
余談だが、Utility Typesの存在を知らずに同じことをインターフェイスの継承で実現しようとして、無駄に複雑なコードを書いていた時期があった。
null/undefinedの扱いと strictNullChecks
tsconfig.json の strictNullChecks を有効にすると、null・undefinedの混入をコンパイル時に検出できる。
class Sample {
public name: string; // エラー:初期値なしでundefinedの可能性
public name2: string = ""; // OK:初期値あり
public name3?: string; // OK:undefinedもあると明示
public fn() {
console.log(this.name3.length); // エラー:undefinedの可能性
if (this.name3) {
console.log(this.name3.length); // OK:このブロック内はstring確定
}
}
}
ただし、これはあくまでTypeScriptの静的チェックの範囲だ。サーバーからのレスポンスやユーザー入力など、外部から来るデータはコンパイル時に型が保証されない。実行時の検証は別途必要になる点は注意しておきたい。
strictNullChecks を有効にするだけでは、外部入力の null/undefined は防げない。型アサーションで無理やりキャストした箇所は、実行時エラーの可能性が残る。
外部ライブラリへの型定義の当て方
JavaScriptで書かれたライブラリは、そのままではTypeScriptの型チェックが効かない。@types/[ライブラリ名] というパッケージが npm に提供されていれば、それをインストールすることで型情報を利用できる。
npm install --save-dev @types/jquery
最近はTypeScriptで書かれたライブラリが増えていて、型定義が同梱されているものも多い。型定義ファイルがない場合は .d.ts ファイルを自前で書くことになるが、その場合は any を使わざるを得ない部分が出てきて、型安全の恩恵が薄れる。
整理して気づいたこと
「型をつけるJavaScript」という理解は間違いではないが、背景にある問題意識(規模が大きくなったときの保守コスト)から理解すると、どの機能がどのために存在するかが見えやすくなった。
まだ自分の中で整理しきれていないのは、型定義ファイル(.d.ts)を自前で書く場合の設計方針だ。ライブラリに型がないケースでどこまで厳密に書くべきか、チームで方針を揃えるための基準を言語化できていない。そこは実際のプロジェクトで経験しながら整理していきたい。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc