TypeScript基礎講座 #2 基本の型
前回はTypeScriptとは何か、なぜ使うのか、最初のコードをコンパイルして実行するところまでを扱いました。今回は毎日使うことになる基本の型を整理します。
プリミティブ型 — string / number / boolean #
JavaScriptのプリミティブ型と一対一で対応します。
const name: string = '太郎';
const age: number = 30;
const isAdmin: boolean = false;TypeScriptのstring、number、booleanはすべて小文字です。JavaのString/Integerのような大文字のクラス型と混同しないでください — TypeScriptのString(大文字)も存在はしますが、ほとんど使うことはなく、通常は小文字のstringを使います。
number は整数も実数も含む #
JavaScriptと同じく、numberは整数と実数を区別しません。
const count: number = 42;
const pi: number = 3.14;
const big: number = 1_000_000; // 桁区切り (JavaScript の文法)
const hex: number = 0xff; // 16進数
const bin: number = 0b1010; // 2進数
大きな整数が必要ならbigint型(100n)を使うこともできますが、日常的にはほとんど使われません。
nullとundefined #
JavaScriptには2つの「値がない」がありますね。TypeScriptもそれをそのまま引き継ぎます。
const a: null = null;
const b: undefined = undefined;この2つの型は単独で使われることはほとんどなく、通常は他の型とunion(#4)で結合して使います。
let user: string | null = null;
user = '太郎';配列 #
配列の型には2つの記法があります。
const fruits: string[] = ['りんご', 'バナナ', 'チェリー'];
const numbers: Array<number> = [1, 2, 3, 4];string[]とArray<string>は完全に同じ意味です。短い[]形式が一般的で、ジェネリクス形式は少し複雑な型を表現するときに可読性がよく、ときどき使われます。
配列に違う型を入れようとすると止められます。
const fruits: string[] = ['りんご', 42]; // 🚫 numberはstring[]に入れられない
複数の型を混ぜたいならunion(#4)で:
const mixed: (string | number)[] = ['りんご', 42, 'バナナ'];タプル (tuple) #
配列の一種ですが、各位置に決まった型がある形です。
let point: [number, number] = [10, 20];
let labeled: [string, number] = ['age', 30];
point[0]; // number
point[1]; // number
labeled[0]; // string
labeled[1]; // number
labeled[2]; // 🚫 エラー: インデックス2は定義されていない
長さも固定で、各位置の型も固定されます。座標(x, y)や(key, value)ペアのように、位置ごとに意味が異なるデータによくマッチします。
JavaScriptの関数で複数の値を返すときによく使われます。
function useCounter(initial: number): [number, () => void] {
// ... (省略)
return [42, () => {}];
}
const [count, increment] = useCounter(0);ReactのuseStateもまさにこのタプル戻り値パターンです。
オブジェクト型 #
オブジェクトの形も明示できます。
const user: { name: string; age: number; isAdmin: boolean } = {
name: '太郎',
age: 30,
isAdmin: false,
};このインラインのオブジェクト型が長くなると可読性が落ちます。なのでinterfaceやtypeで別途定義するのが一般的ですが、これは#3で詳しく扱います。ここではインライン形式だけ知っておけば十分です。
オプショナルプロパティ #
?を付けるとそのプロパティがなくてもOKになります。
const user: { name: string; age?: number } = { name: '太郎' }; // age がなくても OK
readonly #
読み取り専用のプロパティにはreadonlyを付けます。
const user: { readonly id: string; name: string } = { id: 'u-1', name: '太郎' };
user.name = '花子'; // ✓
user.id = 'u-2'; // 🚫 readonlyなので変更不可
readonlyはコンパイル時の保護にすぎず、JavaScriptレベルの本物のreadonly(Object.freeze)ではありません。それでも意図しない修正を捕まえるのにとても便利です。
enum #
複数の名前付き定数の集まりです。
enum Color {
Red,
Green,
Blue,
}
const c: Color = Color.Red;
console.log(c); // 0
enumメンバーはデフォルトで0から始まる整数が自動で割り当てられます。明示的に値を指定することもできます。
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Deleted = 'DELETED',
}
const s: Status = Status.Active; // 'ACTIVE'
enumの代替 — Union of literals #
TypeScriptコミュニティではenumよりもliteral union(#4)を好む傾向があります。
type Status = 'pending' | 'active' | 'deleted';
const s: Status = 'active';こちらのほうが軽く、コンパイル結果もシンプルです(enumはコンパイル時に追加のオブジェクトを生成します)。#4で詳しく扱います。
このシリーズではenumも知っておきつつ、新規コードではliteral unionを優先する姿勢をおすすめします。
any — 型チェックを切る #
any型は「どんな型でも許す」という意味です。
let value: any = '文字列';
value = 42;
value = { name: '太郎' };
value.foo.bar.baz; // 🚫 ランタイムエラーの可能性 — コンパイルは通る
anyで宣言すると、TypeScriptはその変数に対するすべてのチェックを止めます。何でも入れられて、何でも呼び出せて、結果としてJavaScriptとまったく同じように動作します。
anyはTypeScriptの脱出口です。たまにどうしようもないときだけ使い、日常的には避けるのがよいでしょう。使いすぎるとTypeScriptを使う意味がなくなります。
unknown — 安全なany #
anyと同様に「どんな値でも入りうる」を意味しますが、使うときに型チェックを強制するより安全な型です。
let value: unknown = '文字列';
value.toUpperCase(); // 🚫 unknownなのでメソッド呼び出しはできない
if (typeof value === 'string') {
value.toUpperCase(); // ✓ 分岐の中ではstringに絞り込まれる
}外部APIのレスポンスやユーザー入力のように型がわからない値を扱うときは、anyの代わりにunknownを使うと安全です。使う前に必ず型の絞り込み(narrowing、#4)を強制するからです。
void — 戻り値なし #
関数が意味のある値を返さないときに使います。
function log(message: string): void {
console.log(message);
}voidはundefinedに似ていますが意味が違います。undefinedは明示的にundefinedを返すことを意味し、voidは戻り値に関心がないことを意味します。コールバック関数の戻り値の型によく使われます。
never — 絶対に起こらない #
関数が常にthrowするか無限ループで、正常に戻らないときに使う型です。
function fail(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {
// ...
}
}普段は自分で書くことはほとんどなく、型の絞り込みで「この分岐は到達不可能」であることを表すときにたまに登場します。
型アサーション (type assertion) #
TypeScriptが知らない情報を私たちのほうがよく知っているとき、「これはこの型だ」と断言できます。
const input = document.getElementById('email') as HTMLInputElement;
input.value; // ✓ HTMLInputElementにはvalueがある
document.getElementByIdはデフォルトでHTMLElement | nullを返しますが、それがinputタグだとわかっているのでアサーションして絞り込みます。
as 型の形式が標準で、古いスタイルの<HTMLInputElement>...もありますがJSXと衝突するためほとんど使われません。
型アサーションは型チェックを部分的に回避します。anyほど危険ではないものの、誤って使うとランタイムエラーにつながることがあります。本当に私たちのほうがよく知っているという確信があるときだけ使うべきです。
可読性のよい例 #
ここまで扱ったものを総合した小さな例です。
// ユーザーデータの形
const user: {
readonly id: string;
name: string;
email: string;
age?: number;
roles: string[];
} = {
id: 'u-1',
name: '太郎',
email: 'taro@example.com',
roles: ['admin', 'editor'],
};
// 検索結果 — 座標と距離のタプル
const result: [number, number, number] = [37.5, 127.0, 5.2];
const [lat, lng, distance] = result;
// 関数 — void 戻り値
function logUser(u: typeof user): void {
console.log(`${u.name} (${u.email})`);
}
// 知らない外部データ
function processApiResponse(data: unknown): void {
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log('名前:', (data as { name: string }).name);
}
}オブジェクト型がインラインなのでuser部分が少し長いですね。次回(#3)で、interfaceとtypeを使ってこれをきれいに分離する方法を扱います。
自分で試す #
index.tsに次を書いてコンパイルしてみてください。意図的にいくつか間違いを置いています。
let name: string = '太郎';
name = 42; // 1番目のエラー?
const ages: number[] = [30, '三十', 28]; // 2番目のエラー?
const point: [number, number] = [10, 20, 30]; // 3番目のエラー?
const user: { id: string; age?: number } = {
id: 'u-1',
age: undefined, // これはOK?
};
let value: unknown = '文字列';
console.log(value.toUpperCase()); // 4番目のエラー?
npx tscでコンパイルしながら、どの行にどんなエラーが出るかを実際に確かめると頭にしっかり残ります。先に答えを見ずに、自分で予想してから確認してみてください。
まとめ #
今回は日常的に使うことになる基本の型を一気に振り返りました。
- プリミティブ:
string、number、boolean、null、undefined - コレクション:
T[]、Array<T>、タプル[T1, T2] - オブジェクト: インライン
{ ... }(オプショナル?、readonly) - まとまり: enum + literal union の代替案
- 特殊:
any(脱出口)、unknown(安全なany)、void、never - アサーション:
value as 型
オブジェクト型が長くなる問題は次回で解決します。「TypeScript基礎講座 #3 interfaceとtype alias」では、オブジェクトと関数の型の形に名前を付けて再利用する2つの道具 — interfaceとtype aliasを扱います。両者の違いといつどちらを使うかは混乱しがちなテーマなので、1記事でまとめます。