d.sunnyone.org
sunnyone.org

ページ

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を使って、型操作を楽にしていきましょう。

2023-03-27

PHPerKaigi 2023に参加してきました

YAPC::Kyoto 2023を見て、PHPの世界をまた見てみるのいいかもと思ってPHPerKaigi 2023に行ってきました。結果、今のPHPを知れたという意味でもよかったし、ああいう雰囲気の場所が戻ってきたんだというのをまた体感できたという意味でもよかったです。スタッフの皆様、参加者の皆様お疲れ様でした。

以下、セッションのメモです。

---

勉強になったセッション

勉強になったのはこの「PHPの配列の内部実装について学びたくなった。

PHP4のときにガラケー向けアプリケーションを書いていて、arrayの性能に悩まされたことがあり、あのarrayはなあと思っていたのだけど、今更ではあるもののPHP7で真の配列が導入されたことをここで知れたのでよかった。

印象に残ったセッション

印象に残ったのは「名付けできない画面を作ってはならない - 名前を付けるとは何か

設計の話をするセッションだと思っていたのだけど、もうちょっと広く見ていてなるほどなと思った。気持ちの面も結構大事な話だと思う。

2023-03-21

YAPC::Kyoto 2023に参加してきました

京都観光も兼ねて、YAPC::Kyoto 2023に参加してきました。面白かった。遠征でのカンファレンスの参加は人生初なので、そういう意味でも新鮮な体験だった。

印象に残ったセッション

みんな面白かったのだけど、印象に残ったのはmacopyさんのデプロイ今昔物語だった。


よくわからないホスティング環境を借りてPHP(確か4だったと思う)のプログラムを「ホームページ」に置いてみたりとか、読めないPerlを書いて自分で困ったりしていた時代を思い出して、原点に帰った気分になれてとてもよかった。

遠征での学び

日曜日にYAPCで、京都にせっかく行くのだから土曜日は観光しようと決めていて、土曜日に観光するなら朝から行けたほうがいいよねということで金曜日に行って泊まり、日曜日遅くなるかもしれないから月曜日に帰ろうということで、結果3泊4日になった。歩数計を見ると40km近く歩いていることになっているので、結構満喫したと思う。

旅程に合わせてホテルを変えたので、服は洗濯しちゃえば運ぶの楽じゃないか?と思ってチャレンジしてみたものの、旅から帰ってきたあとコインランドリーに行き来して洗濯するのは結構しんどいというのは学びだった。洗濯と乾燥が一緒ならいいのにな~

荷物まわりはuzullaさんがテクニックをまとめてくれているので、今度参考にして楽をしたい。

俺的!遠方カンファレンスの参加体験向上テク 2023最新版 - uzullaがブログ https://uzulla.hateblo.jp/entry/2023/03/21/132421