TypeScript型システムの基礎から応用:重要な型と構文解説
はじめに
TypeScriptの豊富な型システムの機能に圧倒される方も多いのではないでしょうか。
string や number といった基本型から、Mapped Types や Conditional Types といった高度な機能まで、型の種類と用途は非常に幅広いです。
この記事では、TypeScript型システムの基礎となる型から実践的な応用機能まで、体系的に整理してご紹介します。
まず基本型とオブジェクト型の定義方法から始まり、ジェネリクスや高度な型操作機能へと段階的に進んでいきます。基礎をしっかり固めることが応用機能の習得を大幅に加速させます。本記事はその順序に沿って構成しています。
TypeScript型システムの基本的な型と構造
TypeScript型システムを理解する上で、まず基本的な型の種類と、オブジェクトの型を定義するための interface や型エイリアスを押さえることが重要です。
以下の図は TypeScript の主要な型定義の手段とその関係を示しています。
flowchart TD
A[TypeScript型定義の手段]
A --> B[基本型]
A --> C[interface]
A --> D[型エイリアス type]
A --> E[Class]
C -->|extends| F[インターフェース継承]
D -->|& 交差型| G[型の拡張]
E -->|abstract / implements| H[抽象クラス]
基本型とオブジェクト型
TypeScript には、JavaScript の値に対応する基本的な型が用意されています。変数宣言時に : 型名 の形式で型を指定します。
// string 型
const stringValue: string = 'Michael Jackson'
// number 型
const numberValue: number = 28
// boolean 型
const booleanValue: boolean = true
// 配列型
const arrayValue: string[] = ['John', 'Paul']
const arrayValue2: Array = ['John', 'Paul']
// 関数型
const sayHello: (name: string) => void = (name: string): void => {
console.log(`Hello, ${name}!`)
}
// オブジェクト型
const person: {
name: string
age: number
} = {
name: 'Michael Jackson',
age: 28,
}
// any型(型チェックを無効化)
let anyValue: any = "any value"
anyValue = 28
any 型はあらゆる型の値を許容しますが、型安全性を損なうため使用は最小限にとどめることをおすすめします。
基本型の種類や環境構築について、より詳しくは以下の記事で解説しています。
interfaceと型エイリアスの使い分け
オブジェクトの型を再利用可能な形で定義するには、interface または型エイリアス(type)を使用します。
interface は extends キーワードで他のインターフェースを継承できます。
interface Person {
name: string
age: number
walk: () => void
}
interface SuperMan extends Person {
fly: () => void
}
型エイリアスは &(交差型)を使って同様の拡張ができます。
type Person = {
name: string
age: number
walk: () => void
}
type SuperMan = Person & {
fly: () => void
}
一般的には、公開 API の型定義には interface を、ユーティリティ型や複合型には型エイリアスを使用するのがベストプラクティスとされています。
interfaceの宣言方法や型チェックの仕組みについて、より詳しくは以下の記事をご覧ください。
ClassとabstractによるTypeScript的クラス設計
TypeScript のクラスでは、フィールドを事前に宣言する必要があります。constructor のパラメータに public や private などのアクセス修飾子を付けることで、フィールド宣言を省略できます。
class Person {
fullName: string
constructor(firstName: string, lastName: string, public age: number) {
this.fullName = `${firstName} ${lastName}`
}
}
const person = new Person('John', 'Lennon', 40)
console.log(person.fullName) // 'John Lennon'
console.log(person.age) // 40
抽象クラス(abstract class)を使うと、サブクラスに実装を強制するメソッドを定義できます。
abstract class Animal {
abstract walk(): void
}
class Human implements Animal {
walk() {}
speak() {}
}
class Dog implements Animal {
walk() {}
bark() {}
}
implements によって Animal を実装したクラスは、walk() メソッドを必ず実装しなければなりません。実装漏れはコンパイル時にエラーとして検出されます。
柔軟な型表現:ユニオン型とジェネリクスの活用
TypeScript型システムの強力な機能のひとつが、複数の型を組み合わせたり、型を動的に指定したりするための仕組みです。ユニオン型、ジェネリクス、タプル型はその代表的な機能です。
以下の図は、ユニオン型と never 型の関係を示しています。
flowchart LR
A["'John' | 'Paul' | 'George' | 'Ringo'"] -->|switch文で分岐| B[各 case で処理]
B -->|すべての case を網羅| C[never型で安全性を担保]
C -->|未処理の値が来た場合| D[コンパイルエラー]
ユニオン型とnever型による型安全な設計
ユニオン型は | で区切ることで、複数の型のいずれかを受け入れる変数や引数を定義できます。
class Car {}
class Bicycle {}
let vehicle: Car | Bicycle
vehicle = new Car()
vehicle = new Bicycle()
never 型はどんな型も代入できない型で、「ここには絶対に到達しない」という意図をコードで表現するために使います。switch 文の default ケースで使用すると、将来の型追加漏れをコンパイル時に検出できます。
const getPort = (memberName: 'John' | 'Paul' | 'George' | 'Ringo') => {
switch (memberName) {
case 'John':
return 'guitar'
case 'Paul':
return 'guitar'
case 'George':
return 'bass'
case 'Ringo':
return 'drums'
default:
const neverValue: never = memberName
throw new Error('不正な値です')
}
}
ジェネリクスとタプル型
ジェネリクスは、型を動的に指定するための機能です。関数の引数のように という形式で型パラメータを受け取り、再利用性の高い型を定義できます。
// API からの返却値の型を動的に作るための型
type ApiReturnType = {
ok: boolean
data: T
}
type FetchUser = ApiReturnType<{ id: number; name: string }>
type FetchPost = ApiReturnType<{ id: number; title: string }>
タプル型は、各インデックスの型が固定された配列型です。as const を使うことで、通常の配列をタプル型として扱えます。
// 配列(可変長、number[] 型)
const tupleNumbers = [1, 2, 3]
// as const でタプル型([1, 2, 3] 型)に変換
const tupleNumbersConst = [1, 2, 3] as const
型アサーションとunknown型の安全な活用
as 型名 のシンタックスで、コンパイラによる型推論を上書きできます。ただし、誤った型を指定するとランタイムエラーの原因になるため、使用は慎重に行ってください。
type Person = {
name: string
age: number
}
const person = JSON.parse('{"name":"Michael Jackson","age":30}') as Person
unknown 型は any 型と同様にあらゆる値を代入できますが、型チェックなしでは使用できません。外部からの入力値など、型が不明なデータには any の代わりに unknown を使用することで、より安全なコードが書けます。
const unknownValue: unknown = 1
unknownValue.name // エラー(Object is of type 'unknown')
TypeScriptの型推論やWideningの仕組みについて詳しく知りたい方は、以下の記事をご覧ください。
高度な型操作:Mapped TypesとConditional Types
TypeScript型システムの応用的な機能として、型を変換・操作するための仕組みが用意されています。typeof、keyof、Mapped Types、Conditional Types がその代表です。
以下の図は、keyof と Mapped Types を組み合わせた型変換の流れを示しています。
flowchart LR
A["interface Dog\n{ name, age, weight }"]
B["keyof Dog\n'name' | 'age' | 'weight'"]
C["type Member\n'John' | 'Paul' | 'George' | 'Ringo'"]
D["Mapped Types\n[key in Member]: { port: string }"]
E["type Band\n{ John: ..., Paul: ..., ... }"]
A -->|keyof| B
C -->|Mapped Types| D
D -->|展開| E
typeofとkeyofによる型の取得
typeof は変数や関数から型情報を取得するシンタックスです。ランタイムの typeof とは異なり、型レベルで機能します。
const sayHello = (name: string) => {
console.log(`Hello, ${name}!`)
}
// (name: string) => void 型を取得
type SayHello = typeof sayHello
keyof はインターフェースまたはオブジェクト型のプロパティ名をユニオン型として取得します。
interface Dog {
name: string
age: number
weight: number
}
type DogKey = keyof Dog // 'name' | 'age' | 'weight'
インデックスシグネチャとMapped Types
インデックスシグネチャは、オブジェクトのキーを string 型または number 型として動的に定義するシンタックスです。
type Band = {
[key: string]: { port: string }
}
Mapped Types は、文字列・数値のユニオン型からオブジェクト型のキーを動的に生成します。
type Member = 'John' | 'Paul' | 'George' | 'Ringo'
type Band = {
[key in Member]: { port: string }
}
// Band は以下と同じ型になります
// type Band = {
// 'John': { port: string },
// 'Paul': { port: string },
// 'George': { port: string },
// 'Ringo': { port: string },
// }
Conditional TypesとAssertion Functions
Conditional Types は、型の条件分岐を表現する機能です。T extends U ? X : Y という形式で、T が U に代入可能であれば X、そうでなければ Y の型になります。
type MessageOf = T extends { message: unknown } ? T['message'] : never
interface Email {
message: string
}
interface Dog {
bark(): void
}
type EmailMessage = MessageOf // string
type DogMessage = MessageOf // never
Assertion Functions は、値の型が条件を満たさない場合にエラーをスローし、条件を満たす場合は型を絞り込む関数です。as による型アサーションと異なり、ランタイムでも安全に型変換できます。
function assertString(value: any): asserts value is string {
if (typeof value !== 'string') {
throw new Error('value is not a string.')
}
}
function processValue(value: unknown) {
assertString(value)
return value // value: string 型として扱えます
}
受け取った値の型が不明な場合は、as よりも Assertion Functions を使用することで、ランタイムの安全性も担保できます。
Utility Typesと列挙型の実践活用
TypeScript には、型変換をシンプルに記述できる組み込みの Utility Types が豊富に用意されています。また、関連する値の集合を表現する列挙型(enum)についても、推奨される代替パターンとあわせてご紹介します。
以下の図は、代表的な Utility Types と元の型との関係を示しています。
flowchart LR
A["type Person\n{ name: string, age: number }"]
B["Partial\n{ name?: string, age?: number }"]
C["Required\nすべて必須"]
D["Omit<Person, 'age'>\n{ name: string }"]
A -->|Partial| B
A -->|Required| C
A -->|Omit| D
Partial・Required・OmitによるUtility Types活用
Partial は型に含まれるすべてのプロパティをオプショナルにします。フォームの一時保存状態など、すべてのフィールドが揃っていなくてもよい場面で活用できます。
type Person = { name: string, age: number }
const person: Partial = {} // { name?: string, age?: number }
Required はすべてのプロパティを必須にします。オプショナルなフィールドを含む型から、必須化した型を作成する際に便利です。
type Person = { name: string, age?: number }
const person: Required = { name: 'John', age: 40 }
Omit<Type, Keys> は指定したプロパティを除外した型を作成します。| で複数のキーを指定できます。
type Person = { name: string, age: number, bloodType: string }
const person1: Omit<Person, 'bloodType'> = { name: 'John', age: 40 }
const person2: Omit<Person, 'age' | 'bloodType'> = { name: 'John' }
他にも多数の Utility Types が用意されています。公式ドキュメントで全種類を確認することをおすすめします。
TypeScript公式 Utility Types ドキュメント
列挙型(enum)と推奨される代替パターン
列挙型(enum)は関連する値の集合を定義できますが、TypeScript の enum はトランスパイル後のコードが複雑になるなどの理由から非推奨とされることがあります。
enum Color {
Red,
Green,
Blue,
}
let color: Color
color = Color.Red
color = 'Purple' // Type '"Purple"' is not assignable to type 'Color'
代替手段として、as const を使ったオブジェクトによるパターンが広く採用されています。
const colorMap = {
Red: 'Red',
Blue: 'Blue',
Green: 'Green',
} as const
type Color = typeof colorMap[keyof typeof colorMap] // 'Red' | 'Green' | 'Blue'
let color: Color
color = colorMap.Green
color = 'Purple' // Type '"Purple"' is not assignable to type 'Color'
as const パターンはトランスパイル後のコードがシンプルで、ツールとの相性も良いため、新規プロジェクトでは積極的に採用することをおすすめします。
終わりに
この記事では、TypeScript型システムの基本型から応用的な型操作機能まで、体系的にご紹介しました。interface や型エイリアスによるオブジェクト型の定義から始まり、ジェネリクス・Mapped Types・Conditional Types・Utility Types といった強力な機能まで、実践的なコード例とともに解説しました。
TypeScript型システムは非常に豊富で、最初はすべてを覚えようとするよりも、実際のプロジェクトで必要になった機能から順に習得していくことをおすすめします。基本型と interface・型エイリアスをしっかりと使いこなせるようになった後、ジェネリクスや Utility Types へと段階的にスキルを広げていくと学習が定着しやすいです。実務での経験から言うと、Mapped Types や Conditional Types は最初は難解に感じますが、具体的なユースケースに出会うと一気に理解が深まります。
ぜひ公式ドキュメントも参照しながら、TypeScript型システムをプロジェクトで積極的に活用してみてください。型の恩恵を受けることで、バグを早期に発見し、より安全で保守性の高いコードが書けるようになります。
TypeScriptとECMAScriptの関係や型システムの背景について理解を深めたい方は、以下の記事もご覧ください。
TypeScript公式ドキュメント
コメントを残す