ページ

2023-12-19

例でわかるTypeScript Utility Types

これはTypeScript Advent Calendar 202319日目の記事です。例とともにUtility Typesの紹介をします。

Utility Typesとは

Utility Typesは、型から型を生成するためのTypeScript組み込みの型です。ライブラリを書いているような方ならば普段から使っていると思いますが、Utility Typesが何かわからない方もPartial<Foo>やRecord<string, string>なんかは見たことがあるのではないでしょうか。

どんなものがあるか?というのはドキュメントを見てもらえれば良いので、 詳細はそちらに譲りますが、今日はどういうときに便利なのか?という視点でUtility Typesの主要なものを分類しつついくつか紹介します。

TypeScriptが提供する型: 作るもの

Record<Keys, Type>

Recordは、Keysをキーとするプロパティを持ち、そのプロパティの値の型はTypeであるという型を生成する型です。これは主にデータを格納するためのオブジェクトの型を定義するときに使います。

type FooRecord = Record<"prop1" | "prop2", string>;
// → { prop1: string, prop2: string }

上記のようなケースだとベタに定義したほうが良いのでありがたみはないですが、keyの集合がすでに定義されている場合は便利です。

interface BarData {
    prop1: string;
    prop2: number;
    prop3: Date;
}

type BarDataErrors = Record<keyof BarData, string>
//  → { prop1: string, prop2: string, prop3: string}
stringのように広い型を指定することもできます。 
type StringRecord = Record<string, string>;

もともとRecordは

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

と単純なのでこれを書いてもいいのですが、意図を明示できる上に簡潔に記述することができます。

TypeScriptが提供する型: 主にプロパティを加工するためのもの

Pick<Type, Keys> / Omit<Type, Keys>

PickはTypeのプロパティをKeysに絞った型を作ります。OmitはTypeのプロパティからKeysを外した型を作ります。ホワイトリストとブラックリストですね。

これらの使い方は幅広いのですが、既存の関数のラッパーを作るときに便利です。

例えば、ButtonというReactコンポーネントがあり、propとしてsizeを取るとき、このsizeを“large”に固定したLargeButtonというコンポーネントを作りたいようなケースを考えます。このときにLargeButtonのpropsの型をButtonからsizeプロパティを外したものとして作ると、Buttonと同じだがsizeは渡せないということを明確にすることができます。

type LargeButtonProps = Omit<ButtonProps, "size">;

function LargeButton(props: LargeButtonProps) {
    return <Button {...props} size="large" />
}

Extract<Type, Union> / Exclude<UnionType, ExcludedMembers>

ExtractはTypeからUnionに代入可能なものを取り出した型を作り、ExcludeはUnionTypeからExcludedMembersに代入可能なものを外した型を作ります。

これは、discriminated unionとあわせて使います。例えば、type: “success”のときは正常、type: “error”のときはエラーであるResultオブジェクトを考えます。

interface ResultSuccess {
    type: "success";
    data: Record<string, string>;
}

interface ResultError {
    type: "error";
    errorMessage: string;
}

// 既存ライブラリが合わさっている型だけexportしていたり
export type Result = ResultSuccess | ResultError;

このような場合に、ExtractやExcludeを使うと限定した型を得ることができます。

type SuccessType = Extract<Result, {type: "success"}>
// ResultSuccess
type NonSuccessType = Exclude<Result, {type: "success"}>
// ResultError

Partial<Type> / Required<Type>

Partialはプロパティを任意にし、Requiredはプロパティを必須にします。

Partialは例えばプロパティを部分的に受け取ってオブジェクトを更新するような関数を作る場合に使えます。

function updateFoo(foo: Foo, fields: Partial<Foo>) {
    return {...foo, ...fields };
}

もともとすべてのプロパティがrequiredとは限らないので、Partialと比べるとRequiredを素で使うケースは少ないと思いますが、他との組み合わせ、例えばPickで明確にして使う、ということもできます。

interface Foo {
    name?: string;
    age?: number;
    description?: string;
}

type Bar = Required<Pick<Foo, "name" | "age">>;

NonNullable

NonNullableはTypeからnullとundefinedを外します。

type NullableType = string | null;
type NonNullType = NonNullable<NullableType>;
// string

まあこれはわかりやすいですよね。

TypeScriptが提供する型: 関数に関するもの

関数の型に対して何かをする型は、汎用的に関数を取ってそれをどうにかする関数には不可欠なものですが、具体的な関数がわかっている場合にも以下のような使い方をすることができます。

ReturnType

ReturnTypeは関数の型から戻り値の型を得ます。

ReturnTypeは、関数はわかっているが型にアクセスできない、または定義されていないようなとき、例えばfactory的な関数でその型の明示がないようなケースのときに、その値を中継するなどのことをするために型が欲しいときに便利です。

function createFoo() {
    return {
        a: "abc",
        b: 123
    };
}

type CreateFooReturn = ReturnType<typeof createFoo>;
// { a: string, b: number }

Parameters

Parametersは、関数の引数の型を得られます。ラッパー関数を作るような場合に、既存の関数の型をそのままに引数を足したいなどのときに便利です。

function foo(a: string|number) {
    return a;
}

type FooParameters = Parameters<typeof foo>;
function wrappedFoo(a: FooParameters[0], b: string) {
    other(b);
    return foo(a);
}

Awaited

asyncな関数だとreturn typeがPromiseになりますが、そのままでは扱いにくいです。 そんなときにAwaitedを使うと剥がすことができます。

interface Foo {
    a: string;
    b?: number;
}

export async function createFoo(): Promise<Foo> {
    return {
        a: "bar",
        b: 123
    };
}

///

type RequiredFoo = Required<Awaited<ReturnType<typeof createFoo>>>;
// {a: string, b: number}

自分で作る

Utility TypesもTypeScriptで定義された型でしかないので、このような型は自分でも作ることができます。

例えば、部分的に任意にするPartialPartiallyというものを作ると以下のようになります。

// すでに定義されているHoge
interface Hoge {
    name: string;
    description: string;
    startAt: Date;
    finishedAt: Date;
}

type PartialPartially<T, K extends keyof T> = Partial<Pick<T, K>> & Pick<T, keyof Omit<T, K>>;
// 終わってない(finishしていない)かもしれない型を作る
type HogeMayUnfinished = PartialPartial<Hoge, "finishedAt">;

このように、型の操作の仕方と、型の操作そのものを分離しておくと、あとから見たときの悩み度合いが減って良いです。

おわりに

使わないに越したこともないものもありますが、どうしようもないときは知ってるか知らないかで型の扱いの難易度が変わってきます。

Utility Typeを使って、型操作を楽にしていきましょう。