今年お世話になったTypeScriptの型集
はじめに
この記事はWano Group Advent Calendar 2025の18日目の記事です。
普段自分はTypeScript / React / Next.jsなどを使用して主にフロントエンドの開発をしているのですが、その中で2025年によく使用した型をいくつか挙げていこうと思います。
ちなみに下記のうち🛠️マークがついているものは自前で定義した型ですが、ユーテリティ型を提供するライブラリ等でも同等(or それ以上)の機能を提供しているものもあるとは思います。
できるだけ外部への依存を減らしたい目的もあり、自前で定義できそうなものは必要な分だけ
定義しています。
🛠️ ValueOf / DeepValueOf
任意のオブジェクトの取りうる値のユニオン型を取り出す型です。
再帰的に宣言することで第二、第三階層の値も取り出すことができます。
type-festのようなユーティリティ型を集めたライブラリにも定義されています。
type ValueOf<T> = T[keyof T]
type DeepValueOf<T, Depth extends { depth: 1 | 2 } = { depth: 1 }> =
T extends Record<string, infer U>
? U extends object
? Depth extends { depth: 1 }
? ValueOf<U>
: DeepValueOf<U, { depth: 1 }>
: U
: never;
使い所
ドメインモデルの型を定義するのに区分値が必要な場合、以下のように定義することが多かったです。
- 区分値をkey-valueの定数で定義
- → ValueOfでそのユニオン型を定義
- → 指定する側では定数で指定、受け取り側での引数の型をユニオン型で指定
const RELEASE_TYPES = {
album: "album",
single: "single",
} as const;
type ReleaseType = ValueOf<typeof RELEASE_TYPES>;
🛠️ OneOf(1項目のみ必須のレコード)
レコードのキーの内、1つだけを指定できるレコードを表すユーティリティ型です。
/**
* レコードのキーの内、1つだけを指定できるレコード
* @example
* ```ts
* type A = OneOf<{
* a: string;
* b?: number;
* }>;
* // => { a: string; b?: never; } | { a?: never; b?: number; }
* ```
*/
type OneOf<T extends Record<string, unknown>> = ValueOf<{
[K in keyof T]: { [P in K]: T[P] } & Partial<
Record<Exclude<keyof T, K>, never>
>;
}>;
各キーごとに、1つのキーは必須かつ他のキーの値がnever型になるレコードを作り、その値のユニオン型をValueOfで取り出しています。
使い所
Reactコンポーネントのpropsの定義で、どちらか1つだけを受け取れるようにしたい場合のような排他的な項目を持つ型を定義するときに使えます。
type Props = OneOf<{
href: string;
onClick: () => void;
}>
declare const Something: FC<Props>;
// エラーにならない
<Something href="/" />
// ❌ エラーになる
<Something href="/" onClick={() => {}} />
// ^ Type '{ href: string; onClick: () => void; }' is not assignable to type 'IntrinsicAttributes & Props'.
// Types of property 'onClick' are incompatible.
// Type '() => void' is not assignable to type 'undefined'.ts(2322)
🛠️ OrOf(1項目以上が存在するレコード)
OneOfの場合はいずれか1つの項目が必須でしたが、OrOfはいずれかの項目を1つ以上・複数指定もできるユーティリティ型です。
/**
* レコードのキーの内、1つ以上を指定できるレコード
* @example
* ```ts
* type A = OrOf<{
* a: string;
* b?: number;
* }>;
* // => { a: string; b?: number | undefined; } | { a?: never; b?: number; }
* ```
*/
type OrOf<T extends Record<string, unknown>> = NonNullable<
ValueOf<{
[K in keyof T]: { [P in K]: T[P] } & {
[P in Exclude<keyof T, K>]?: NonNullable<T[P]>;
};
}>
>;
OneOfと作りは似ていますが、OrOfではあるキーとそれ以外のキーが複数同時に存在できるため、ValueOfでユニオン型にする前に内部的にレコードを作るところで任意項目としてマッピングし直している感じになっています。
使い所
いずれかの項目は必須にしたいレコードを定義したいケースがたまにあって、そのとき使いました。
import type { FC, ReactNode } from "react";
type PageInfo = OrOf<{
icon: FC<IconProps>;
label: ReactNode;
}>
// ❌ エラーになる
const info1: PageInfo = {};
// ^ Type {} is not assignable to type
// 以下はいずれもOK
const info2: PageInfo = { icon: SomeIcon };
const info3: PageInfo = { label: "something" };
const info4: PageInfo = { icon: SomeIcon; label: "something" };
🛠️ AllOrNone(全項目が存在するor存在しないレコード)
レコードのキーを全て指定するか、全て指定しないパターンのみを許可するレコードの型です。
/**
* レコードのキーを全て指定するか、全て指定しないパターンのみ許可するレコード
*/
type AllOrNone<T extends Record<string, unknown>> =
| {
[K in keyof T]: T[K] | undefined;
}
| {
[K in keyof T]?: never;
};
型の中身はシンプルで、全て指定するorしない(never型)場合のレコードのユニオン型になっています。
使い所
1つの項目を指定する場合まとめて他の項目も指定させたいようなケースのコンポーネントのpropsの型に使用できます。
import type { FC, ReactNode } from "react";
type Props = {
// ...
} & AllOrNone<{
open: boolean;
onOpen: (open: boolean) => void;
}>;
declare const Something: FC<Props>;
// エラーにならない
<Something
open
onOpen={() => {
/** do something */
}}
/>;
// ❌ エラーになる
<Something open />;
// ^ Type '{ open: true; }' is not assignable to type 'IntrinsicAttributes & Props'.
// Property 'onOpen' is missing in type '{ open: true; }' but required in type '{ open: boolean | undefined; onOpen: ((open: boolean) => void) | undefined; }'.ts(2322)
🛠️ RequiredButPossiblyUndefined(undefinedは許可するが、optionalな項目は許可しない)
optionalな項目について、キー自体の指定は必須にしつつundefinedの指定を許可するようにする型です。
ちょっと大仰な命名ですが、特殊な用途のために使用しています。
type RequiredButPossiblyUndefined<T> = {
[K in keyof Required<T>]: [T[K]] extends [never] ? undefined : T[K];
};
使い所
例えば、ライブラリ等の内部でコントロールできない型にoptionalな項目が含まれている場合、それらの任意項目がすべて網羅的に指定されているかをチェックさせたい場合に使えます。
弊社ではOpenAPIで定義したスキーマからopenapi-generatorを使用してリクエストやレスポンスのスキーマの型を生成させています。
その際、OpenAPI上でrequiredに指定していない項目はoptionalな項目として扱われるのですが、フロントエンドからAPIリクエストを送信する際、リクエストするデータに対してoptionalな項目が含まれる型をアノテーションしていると、項目を指定し忘れていても型上は正しいのでエラーにはなりません。
type UserRequest = {
name?: string;
};
// 指定をし忘れていても気づきづらい
const convertInputToUserRequest = (input: { name?: string }): UserRequest => {
// ...
return {};
};
APIリクエストするデータに変換する内部関数では、戻り値の型にRequiredButPossiblyUndefinedを使用することで、指定可能な項目をすべて網羅的に指定しつつ、各項目の値としてundefinedを許容することができます。
この指定により、OpenAPI上で項目が増えた場合にも変更箇所に気づけるようになります。
type UserRequest = {
name?: string;
};
const convertInputToUserRequest = (input: {
name?: string;
}): RequiredButPossiblyUndefined<UserRequest> => {
// ...
return {};
// ^ Property 'name' is missing in type '{}' but required in type 'RequiredButPossiblyUndefined<UserRequest>'.ts(2741)
};
注意点としては、TypeScript 4.4で追加されたexactOptionalPropertyTypesをtsconfig.jsonに設定している環境の場合、optionalな項目にundefinedを代入することはできません。
しかし、弊社で利用しているopenapi-generatorのtypescript-axios / typescript-fetch ではこのオプションにまだ対応していない(はず)ため、このオプションが有効な環境下でopenapi-generatorを利用する場合、この方法はうまく活用できません。。。
※ OpenAPI上で必須にしつつ、nullable: trueを指定するやり方をとれば解決できそうでもありますが、既存実装の影響範囲を考慮して一旦上記で対処しています。
🟦 NoInfer
NoInferはTypeScript 5.4で追加されたビルトインのUtility Typeです。
リリース当初見たときはあまり具体的な使い道が思い浮かばなかったのですが、最近使い所があるのを知りました。
使い所
例えば、下記のようなReact Hook Form(以下RHF)を利用したReactコンポーネントの場合を考えます。
型引数でRHFで管理するオブジェクトの型を指定できるコンポーネントを定義することで、nameに制約を設けることができますが、この型引数は必須ではありません。
型引数を指定しなくても、nameに指定した値から推論されてしまい、以下の例だと"foo" | `foo.${string}`型に推論されてしまいます。
import type { FC } from "react";
import type { FieldPath, FieldValues, FormProps } from "react-hook-form";
import { useFormContext } from "react-hook-form";
type Props<TFieldValues extends FieldValues> = {
name: FieldPath<TFieldValues>;
formProps?: FormProps<TFieldValues>;
};
const FormInput = <
TFieldValues extends FieldValues,
TTransformedValues extends FieldValues = TFieldValues,
>({
name,
formProps,
}: Props<TFieldValues>): ReturnType<FC<Props<TFieldValues>>> => {
const { register } = useFormContext<
TFieldValues,
unknown,
TTransformedValues
>();
return <input {...register(name, formProps)} />;
};
// 呼び出し側
// エラーにならない
<FormInput name="foo" />
そこで、下記のように型引数TFieldValuesのデフォルト値をneverとして、nameの型にNoInferを使うと、実質的にTFieldValuesの指定を必須にすることができます。
TFieldValuesを明示的に指定しない限り、nameはnever型になり、どの型を指定してもエラーになるためです。
import type { FC } from "react";
import type { FieldPath, FieldValues, FormProps } from "react-hook-form";
import { useFormContext } from "react-hook-form";
type Props<TFieldValues extends FieldValues> = {
- name: FieldPath<TFieldValues>;
+ name: NoInfer<FieldPath<TFieldValues>>;
formProps?: FormProps<TFieldValues>;
};
const FormInput = <
- TFieldValues extends FieldValues,
+ TFieldValues extends FieldValues = never,
TTransformedValues extends FieldValues = TFieldValues,
>({
name,
formProps,
}: Props<TFieldValues>): ReturnType<FC<Props<TFieldValues>>> => {
const { register } = useFormContext<
TFieldValues,
unknown,
TTransformedValues
>();
return <input {...register(name, formProps)} />;
};
// 呼び出し側
// ❌ エラーになる
<FormInput name="foo" />
// ^ Type 'string' is not assignable to type 'never'.ts(2322)
// エラーにならない
<FormInput<{ foo: string }> name="foo" />
RHFで管理する型とnameプロパティを紐づける際、手動で整合性を保つ必要があります。
型とnameのパスがずれていた場合に検知できるよう、RHFを使用するコンポーネントでは型引数を指定すべきだと思っているのですが、このように型引数の指定を強制できるとより安全になるなと思いました。
さいごに
TypeScriptの(完璧ではないかもしれませんが、懐の深い)型システムを活用することで、コードをより安全にすることができるというのを改めて実感しました。
どなたかの参考になれば幸いです!
Discussion