もっとTypeScriptの力を引き出そう メモ
はじめに
SOFTWARE DESIGN (ソフトウェア) 2024年05月号の「もっとTypeScriptの力を引き出そう」を読んで、私自身にとって必要そう、理解が足りていなかったところを備忘録的にメモしました。
関数型
TypeScriptの関数型とは、関数の引数の型と戻り値の型を明示的に指定することで、関数の型安全性を高めるための方法です。関数型を使うと、関数が期待通りの引数を受け取り、期待通りの型の値を返すことをコンパイラが保証します。これにより、バグを減らし、コードの可読性と保守性を向上させることができます。
下記は、数値nを引数として受け取り、boolean型の値を返す関数の例です
// アロー関数の例
const isEven = (n: number): boolean => n % 2 === 0;
console.log(isEven(2)); // true
// 通常の関数宣言の例
function isEven2(n: number): boolean {
return n % 2 === 0;
}
console.log(isEven2(3)); // false
// 無名関数(匿名関数)の例
const isEven3 = function(n: number): boolean {
return n % 2 === 0;
}
console.log(isEven3(4)); // true
構造的部分型
構造的部分型は、オブジェクトの型がその構造によって決まるため、オブジェクトが持つプロパティやメソッドのセットが一致すれば、異なる型であっても互換性があるとみなされます。これに対して、名前的部分型(nominal subtyping)は、型の名前や宣言によって型の互換性を判断します。
interface Person {
name: string;
age: number;
}
interface Employee {
name: string;
age: number;
employeeId: number;
}
const person: Person = { name: "Alice", age: 25 };
const employee: Employee = { name: "Bob", age: 30, employeeId: 1234 };
// Employee型はPerson型の構造を持っているため、代入が可能
const anotherPerson: Person = employee;
// 逆はコンパイルエラーになる
// Person型はemployeeIdを持っていないのでemployeeには代入不可
// Property 'employeeId' is missing in type 'Person' but required in type 'Employee'.
const anotherPerson: employee = person;
利点
- 柔軟性:型の互換性が構造によって決まるため、より柔軟なプログラミングが可能になります。特に、異なるAPIやライブラリとの統合時に便利です。
- 再利用性:
共通のプロパティを持つ異なる型間でデータを共有しやすくなります。
リテラル型
TyeScriptでは型を明示しないでも、式からどの型であるかを推論する機能があります。
例
これをconstで変数宣言すると。"John"型となる。"John"型には"John"という文字列しか代入できません。
このように特定の文字列の値を型として扱うことを文字列リテラル型と言います。
オブジェクトの型推論
const user = {
name: 'John',
age: 25,
}
例えば上記のようなオブジェクトは下記のように推論されます。
'John'型や25型ではなく、それぞれnameはstring型、ageはnumber型で推論されます。
このようにプロパティがより広い型に推論されるのをwidening型と言います。
constでオブジェクトを宣言した場合でも、下記のようなプロパティの代入は可能です。
user.name = 'Jack'; // OK
user.age = 30; // OK
as constでオブジェクトのプロパティをリテラル型に推論する
オブジェクトのプロパティの書き換えを制限し、読み取り専用にしたい場合は、as constを使用します。
const user = {
name: 'John Doe',
age: 25,
} as const;
user.name = 'Jack'; // Error
user.age = 30; // Error
user.nameに’John Doe’以外を代入しようとすると、「Cannot assign to 'name' because it is a read-only property」というエラーになります。
型推論は以下のようになります。
さらにas const satisfiesで型を制限することもできます。
type People = {
name: string
age:number
}
const user = {
name: 'John Doe',
age: 25,
} as const satisfies Person ;
Union型
変数が複数の型のうちどれか一つを取ることができる型です。ユニオン型を使用すると、より柔軟で強力な型定義が可能になります。
使用例
// 関数の引数で使用する
function printId(id: number | string) {
console.log(`Your ID is: ${id}`);
}
printId(123); // OK
printId("ABC"); // OK
printId(true); // エラー: Argument of type 'boolean' is not assignable to parameter of type 'number | string'.
型ガード
ユニオン型を使用するときは、特定の型に基づいて処理を分岐させる必要があります。これを型ガードと呼びます。
型ガードの使用例
function printId(id: number | string) {
if (typeof id === "string") {
console.log(`Your ID is a string: ${id.toUpperCase()}`);
} else {
console.log(`Your ID is a number: ${id}`);
}
}
printId(123); // 出力: Your ID is a number: 123
printId("abc"); // 出力: Your ID is a string: ABC
ユニオン型は、インターフェースと組み合わせて使用することもできます。
インターフェースとユニオン型
interface Dog {
kind: "dog";
bark(): void;
}
interface Cat {
kind: "cat";
meow(): void;
}
interface Bird {
kind: "bird";
chirp(): void;
}
type Pet = Dog | Cat | Bird;
網羅性チェック
型ガードの網羅性チェックは、すべての可能な型のケースが適切に処理されるようにするための手法です。特にユニオン型を扱う場合、すべての型をカバーすることを保証することで、予期しない型の処理漏れを防ぎます。このチェックを行うために、never型を活用する方法が一般的です。
never型とは「あり得ない」ことを表す型です
網羅性チェックの例
function speak(pet: Pet) {
switch (pet.kind) {
case "dog":
pet.bark();
break;
case "cat":
pet.meow();
break;
case "bird":
pet.chirp();
break;
default:
// never型を利用してすべてのケースがカバーされていることを確認
const _: never = pet;
}
}
const myDog: Dog = { kind: "dog", bark: () => console.log("Woof!") };
const myCat: Cat = { kind: "cat", meow: () => console.log("Meow!") };
const myBird: Bird = { kind: "bird", chirp: () => console.log("Chirp!") };
speak(myDog); // 出力: Woof!
speak(myCat); // 出力: Meow!
speak(myBird); // 出力: Chirp!
例えば、新しいペットの種類Fishを追加してみます。
interface Fish {
kind: "fish";
swim(): void;
}
const myFish: Fish = { kind: "fish", swim: () => console.log("Swim!") };
type Pet = Dog | Cat | Bird | Fish;
先ほどのnever型でエラーを検知してくれます。
判別可能なユニオン型
まずは判別可能なユニオン型のコード例を書きます。
interface Dog {
kind: "dog";
bark(): void;
}
interface Cat {
kind: "cat";
meow(): void;
}
interface Bird {
kind: "bird";
chirp(): void;
}
type Pet = Dog | Cat | Bird;
function speak(pet: Pet) {
switch (pet.kind) {
case "dog":
pet.bark();
break;
case "cat":
pet.meow();
break;
case "bird":
pet.chirp();
break;
default:
// すべてのケースがカバーされているため、この部分に到達することはない
const _exhaustiveCheck: never = pet;
throw new Error(`Unknown pet: ${_exhaustiveCheck}`);
}
}
const myDog: Dog = { kind: "dog", bark: () => console.log("Woof!") };
const myCat: Cat = { kind: "cat", meow: () => console.log("Meow!") };
const myBird: Bird = { kind: "bird", chirp: () => console.log("Chirp!") };
speak(myDog); // 出力: Woof!
speak(myCat); // 出力: Meow!
speak(myBird); // 出力: Chirp!
関数speakがPet型を受け取った時点では、それがDog型なのかCat型なのか分からない。なのでbarkメソッドやmeowメソッドにアクセスすることができない。
しかしkindプロパティが各ユニオンメンバーを区別するための判別プロパティとして機能します。
成功、失敗を判断
TypeScriptでAPIの戻り値の成功、失敗を判断できるResult型を作成する方法を紹介します。この型を使用することで、APIの戻り値が成功か失敗かを安全に判断できるようになります。
まず、Result型を定義します。成功のケースと失敗のケースをユニオン型で表現し、判別可能なユニオン型として実装します。
// 成功時の型
interface Success<T> {
success: true;
data: T;
}
// 失敗時の型
interface Failure {
success: false;
error: string;
}
// Result型
type Result<T> = Success<T> | Failure;
次に、Result型を使ってAPIの戻り値を処理する例を示します。
API呼び出し関数の作成
まず、モックAPI関数を作成します。この関数は、Result型を返すようにします。
// モックAPI関数
async function fetchData(): Promise<Result<string>> {
try {
// モックデータ(成功ケース)
const data = "API call success!";
return { success: true, data };
} catch (error) {
// モックエラー(失敗ケース)
return { success: false, error: "API call failed." };
}
}
戻り値の処理
API呼び出し結果を処理する関数を作成し、成功ケースと失敗ケースを判別します。
async function handleApiCall() {
const result = await fetchData();
if (result.success) {
// 成功ケース
console.log("Data:", result.data);
} else {
// 失敗ケース
console.log("Error:", result.error);
}
}
// 関数を実行してAPI呼び出し結果を処理
handleApiCall();
Discussion