TypeScript基礎講座 #2 基本の型

読了 9分

前回はTypeScriptとは何か、なぜ使うのか、最初のコードをコンパイルして実行するところまでを扱いました。今回は毎日使うことになる基本の型を整理します。

プリミティブ型 — string / number / boolean #

JavaScriptのプリミティブ型と一対一で対応します。

プリミティブ型
const name: string = '太郎';
const age: number = 30;
const isAdmin: boolean = false;

TypeScriptのstringnumberbooleanすべて小文字です。JavaのString/Integerのような大文字のクラス型と混同しないでください — TypeScriptのString(大文字)も存在はしますが、ほとんど使うことはなく、通常は小文字のstringを使います。

number は整数も実数も含む #

JavaScriptと同じく、numberは整数と実数を区別しません。

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もそれをそのまま引き継ぎます。

nullとundefined
const a: null = null;
const b: undefined = undefined;

この2つの型は単独で使われることはほとんどなく、通常は他の型とunion(#4)で結合して使います。

nullまたはstring
let user: string | null = null;
user = '太郎';

配列 #

配列の型には2つの記法があります。

配列の型 - 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の関数で複数の値を返すときによく使われます。

useStateのようなパターン
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,
};

このインラインのオブジェクト型が長くなると可読性が落ちます。なのでinterfacetypeで別途定義するのが一般的ですが、これは#3で詳しく扱います。ここではインライン形式だけ知っておけば十分です。

オプショナルプロパティ #

?を付けるとそのプロパティがなくてもOKになります。

オプショナル
const user: { name: string; age?: number } = { name: '太郎' };  // age がなくても OK

readonly #

読み取り専用のプロパティには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
enum Color {
  Red,
  Green,
  Blue,
}

const c: Color = Color.Red;
console.log(c);  // 0

enumメンバーはデフォルトで0から始まる整数が自動で割り当てられます。明示的に値を指定することもできます。

文字列enum
enum Status {
  Pending = 'PENDING',
  Active = 'ACTIVE',
  Deleted = 'DELETED',
}

const s: Status = Status.Active;  // 'ACTIVE'

enumの代替 — Union of literals #

TypeScriptコミュニティではenumよりもliteral union(#4)を好む傾向があります。

enumの代わりにliteral union
type Status = 'pending' | 'active' | 'deleted';

const s: Status = 'active';

こちらのほうが軽く、コンパイル結果もシンプルです(enumはコンパイル時に追加のオブジェクトを生成します)。#4で詳しく扱います。

このシリーズではenumも知っておきつつ、新規コードではliteral unionを優先する姿勢をおすすめします。

any — 型チェックを切る #

any型は「どんな型でも許す」という意味です。

any
let value: any = '文字列';
value = 42;
value = { name: '太郎' };
value.foo.bar.baz;  // 🚫 ランタイムエラーの可能性 — コンパイルは通る

anyで宣言すると、TypeScriptはその変数に対するすべてのチェックを止めます。何でも入れられて、何でも呼び出せて、結果としてJavaScriptとまったく同じように動作します。

anyTypeScriptの脱出口です。たまにどうしようもないときだけ使い、日常的には避けるのがよいでしょう。使いすぎるとTypeScriptを使う意味がなくなります。

unknown — 安全なany #

anyと同様に「どんな値でも入りうる」を意味しますが、使うときに型チェックを強制するより安全な型です。

unknown
let value: unknown = '文字列';

value.toUpperCase();  // 🚫 unknownなのでメソッド呼び出しはできない

if (typeof value === 'string') {
  value.toUpperCase();  // ✓ 分岐の中ではstringに絞り込まれる
}

外部APIのレスポンスやユーザー入力のように型がわからない値を扱うときは、anyの代わりにunknownを使うと安全です。使う前に必ず型の絞り込み(narrowing、#4)を強制するからです。

void — 戻り値なし #

関数が意味のある値を返さないときに使います。

void
function log(message: string): void {
  console.log(message);
}

voidundefinedに似ていますが意味が違います。undefined明示的にundefinedを返すことを意味し、void戻り値に関心がないことを意味します。コールバック関数の戻り値の型によく使われます。

never — 絶対に起こらない #

関数が常にthrowするか無限ループで、正常に戻らないときに使う型です。

never
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)で、interfacetypeを使ってこれをきれいに分離する方法を扱います。

自分で試す #

index.tsに次を書いてコンパイルしてみてください。意図的にいくつか間違いを置いています。

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でコンパイルしながら、どの行にどんなエラーが出るかを実際に確かめると頭にしっかり残ります。先に答えを見ずに、自分で予想してから確認してみてください。

まとめ #

今回は日常的に使うことになる基本の型を一気に振り返りました。

  • プリミティブ: stringnumberbooleannullundefined
  • コレクション: T[]Array<T>、タプル [T1, T2]
  • オブジェクト: インライン { ... } (オプショナル ?readonly)
  • まとまり: enum + literal union の代替案
  • 特殊: any(脱出口)、unknown(安全なany)、voidnever
  • アサーション: value as 型

オブジェクト型が長くなる問題は次回で解決します。「TypeScript基礎講座 #3 interfaceとtype alias」では、オブジェクトと関数の型の形に名前を付けて再利用する2つの道具 — interfacetype aliasを扱います。両者の違いといつどちらを使うかは混乱しがちなテーマなので、1記事でまとめます。

X