白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

TypeScriptの型を復習する その3

こんばんは。
ブラックフライデーで何を買うか決めましたか?

今回はUtility Typesを一気に確認していきます。
前回の記事で解説した通り、Utility Typesは型から別の型に変換するための型です。型レベルの関数みたいなイメージですかね。

今回紹介する型はすべて「lib.es5.d.ts」としてTypeScriptの標準ライブラリに定義されています。
できるだけそのまま「lib.es5.d.ts」のそのままの順番で紹介しますが、一部わかりやすさのために順番は入れ替えています。

Partial

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Partialは与えられた型のすべてのプロパティをオプショナルにした型を生成します。

test('Partial型', () => {
    type User = {
        name: string;
        age: number;
    };
    type PartialUser = Partial<User>;
    const user: PartialUser = { name: "Alice" };
    expect(user).toEqual({ name: "Alice" });
});

Required

type Required<T> = {
    [P in keyof T]-?: T[P];
};

RequiredはPartialの逆で、与えられた型のすべてのプロパティを必須にした型を生成します。

test('Required型', () => {
    type User = {
        name?: string;
        age?: number;
    };
    type RequiredUser = Required<User>;
    const user: RequiredUser = { name: "Bob", age: 25 };
    expect(user).toEqual({ name: "Bob", age: 25 });
});

Readonly

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Readonlyは与えられた型のすべてのプロパティを読み取り専用にした型を生成します。

test('Readonly型', () => {
    type User = {
        name: string;
        age: number;
    };
    type ReadonlyUser = Readonly<User>;
    const user: ReadonlyUser = { name: "Charlie", age: 30 };
    // user.name = "Dave"; // エラー: 読み取り専用プロパティ 'name' に代入することはできません。
    expect(user).toEqual({ name: "Charlie", age: 30 });
});

Record

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Recordはユニオン型の各要素をキーとし、指定した型を値とするオブジェクト型を生成します。

test('Record型', () => {
    type UserRoles = 'admin' | 'user';
    type RolePermissions = Record<UserRoles, string[]>;
    const permissions: RolePermissions = {
        admin: ['read', 'write', 'delete'],
        user: ['read', 'write']
    };
    expect(permissions).toEqual({
        admin: ['read', 'write', 'delete'],
        user: ['read', 'write']
    });
});

型から生成する場合にはkeyof演算子と組み合わせて使うことになります。

test('Record型(型から)', () => {
    type User = {
        name: string;
        age: number;
    };
    type UserRecord = Record<keyof User, string>;
    const user: UserRecord = {
        name: "Frank",
        age: "40"
    };
    expect(user).toEqual({ name: "Frank", age: "40" });
});

オブジェクトから生成する場合にはkeyof演算子とtypeof演算子の両方と組み合わせて使うので若干呪文っぽさがでてきます。

test('Record型(オブジェクトから)', () => {
    const user = {
        name: "Grace",
        age: 35
    };
    type UserRecord = Record<keyof typeof user, string | number>;
    const userRecord: UserRecord = {
        name: "Grace",
        age: 35
    };
    expect(userRecord).toEqual({ name: "Grace", age: 35 });
});

ちなみにstringなどのリテラルでない型を指定することもできます。

test('Record型(リテラルでない)', () => {
    type UserRecord = Record<string, string>;
    const user: UserRecord = {
        name: "Heidi",
        age: "28"
    };
    expect(user).toEqual({ name: "Heidi", age: "28" });
});

つまりどういうことかというと、Record型はインデックス型のエイリアスシンタックスシュガー?みたいなものなので、Record<string, string>は{ [key: string]: string }と同じ意味になるということですね。

Exclude

type Exclude<T, U> = T extends U ? never : T;

Excludeはユニオン型から指定した型を除外した型を生成します。

test('Exclude型', () => {
    type UserRoles = 'admin' | 'user';
    type NonAdminRoles = Exclude<UserRoles, 'admin'>;
    const role: NonAdminRoles = 'user';
    expect(role).toBe('user');
});

Extract

type Extract<T, U> = T extends U ? T : never;

Extractはユニオン型から指定した型だけを抽出した型を生成します。

test('Extract型', () => {
    type UserRoles = 'admin' | 'user';
    type AdminRoles = Extract<UserRoles, 'admin'>;
    const role: AdminRoles = 'admin';
    expect(role).toBe('admin');
});

Omit

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Omitは与えられた型から指定したプロパティを除外した型を生成します。Excludeはユニオン型に対して使いましたが、Omitはオブジェクト型に対して使います。

test('Omit型', () => {
    type User = {
        name: string;
        age: number;
        email: string;
    };
    type UserWithoutEmail = Omit<User, 'email'>;
    const user: UserWithoutEmail = { name: "Frank", age: 40 };
    expect(user).toEqual({ name: "Frank", age: 40 });
});

Pick

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Pickは与えられた型から指定したプロパティだけを抽出した型を生成します。こちらもExtractはユニオン型に対して使いましたが、Pickはオブジェクト型に対して使います。

test('Pick型', () => {
    type User = {
        name: string;
        age: number;
        email: string;
    };
    type UserNameAndEmail = Pick<User, 'name' | 'email'>;
    const user: UserNameAndEmail = { name: "Eve", email: "eve@example.com" };
    expect(user).toEqual({ name: "Eve", email: "eve@example.com" });
});

NonNullable

type NonNullable<T> = T & {};

NonNullableは型からnullとundefinedを除外した型を生成します。
Requiredはプロパティレベルでオプショナルを除外しましたが、NonNullableは型レベルでnullとundefinedを除外するのがちょっとややこしいですね。

test('NonNullable型', () => {
    type User = {
        name: string | null;
        age: number | undefined;
    };
    type NonNullableUser = {
        name: NonNullable<User['name']>;
        age: NonNullable<User['age']>;
    };
    type User2 = NonNullable<User>;    // User自体はnullableではないので特に意味はない
    const user: NonNullableUser = { name: "Grace", age: 35 };
    expect(user).toEqual({ name: "Grace", age: 35 });
});

Parameters

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

Parametersは関数型の引数の型をタプル型として取得します。

test('Parameters型', () => {
    type GreetFunction = (name: string, age: number) => string;
    type GreetParameters = Parameters<GreetFunction>;
    const params: GreetParameters = ["Heidi", 28];
    expect(params).toEqual(["Heidi", 28]);
});

ReturnType

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

ReturnTypeは関数型の戻り値の型を取得します。

test('ReturnType型', () => {
    type GreetFunction = (name: string) => string;
    type GreetReturnType = ReturnType<GreetFunction>;
    const greeting: GreetReturnType = "Hello, Ivan!";
    expect(greeting).toBe("Hello, Ivan!");
});

ConstructorParameters

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

ConstructorParametersはクラスのコンストラクタの引数の型をタプル型として取得します。

test('ConstructorParameters型', () => {
    class Person {
        constructor(public name: string, public age: number) {}
    }
    type PersonConstructorParameters = ConstructorParameters<typeof Person>;
    const params: PersonConstructorParameters = ["Judy", 32];
    expect(params).toEqual(["Judy", 32]);
});

InstanceType

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

InstanceTypeはコンストラクタ関数(=クラス)から、その「インスタンスの型」を取り出す型です。
何言ってるんだって感じですが、クラスは型であって値でもあるので、typeof型演算子を使うと値として扱われてコンストラクタ関数の型になるわけです。

test('InstanceType型', () => {
    class Animal {
        constructor(public species: string) {}
    }
    type AnimalConstructorType = typeof Animal; // 値として扱われてコンストラクタ関数の型になる
    type AnimalInstanceType = InstanceType<AnimalConstructorType>;
    const animal: AnimalInstanceType = new Animal("Dog");
    expect(animal.species).toBe("Dog");
});

で、じゃあどういうときに使うかというと、new T()みたいなことをするときに便利らしいです。
C#はwhere T : new()みたいな制約が使えるのでそんなに困ってなかったですが、そういえばJavaでも同じような悩みがあった気がします。

test('new T()的な', () => {
    function createInstance<C extends new (...args: any) => any>(C: C): InstanceType<C> {
      return new C();
    }
    class Dog {}
    class Cat {}
    const dog = createInstance(Dog);    // 型ではなくて値(コンストラクタ関数と解釈される)
    const cat = createInstance(Cat);    // 型ではなくて値(コンストラクタ関数と解釈される)
    expect(dog).toBeInstanceOf(Dog);
    expect(cat).toBeInstanceOf(Cat);
});

ちなみにですが、typeにはtypeof型演算子は使えません。インタフェースにも使えません。
クラスにだけ使えるのでなんか混乱しますね。

const hoge = {};
type HogeType = typeof hoge;
class HogeClass {}
type HogeClassType = typeof HogeClass;
interface HogeInterface {}
// type HogeInterfaceType = typeof HogeInterface; // エラー: 'HogeInterface' は型のみを参照しますが、ここで値として使用されています。
// type HogeTypeType = typeof HogeType; // エラー: 'HogeType' は型のみを参照しますが、ここで値として使用されています。

Parameters、ReturnType、ConstructorParameters、InstanceTypeはちょっとリフレクションっぽいメタプログラミングみを感じます。

Uppercase / Lowercase / Capitalize / Uncapitalize / NoInfer

type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
type NoInfer<T> = intrinsic;

このあたりは名前の通りなんですが(NoInferは置いておいて)、実装を見ると全部「intrinsic」となっていて、TypeScriptのコンパイラ内部で特別扱いされている型です。
「intrinsic」は「組み込みの」とかそんなニュアンスになるかなと思います。TypeScriptってそんなこともできるんですね。

はい、結構たくさんあるのでボリューム多くなってしまいましたね。
次回はConditional Typesです。