🧩
TypeScript で「コンストラクタで渡さなかった引数だけ後で必須にする」の型制約を書く
実装
/**
* [α] T 型から undefined 不可キーの union を抽出
*/
type RequiredKeys<T> = {
[K in keyof T]-?: Record<any, unknown> extends Pick<T, K>
? never
: K;
}[keyof T];
/**
* [β] α を利用し, Passed 型から Req 型の条件を満たさないものだけを抽出
*/
type MissingKeys<Req, Passed extends Partial<Req>> = {
[K in keyof Pick<
Req,
RequiredKeys<Req>
>]: Passed[K] extends Req[K] ? never : K;
}[keyof Pick<Req, RequiredKeys<Req>>];
/**
* [γ] β を利用し, 「undefined 不可の不足しているサブセット」と「それ以外のサブセット」を抽出
* 後者に対してのみ undefined を許可して型を合成する
*/
type RequireMissingSubset<Req, Passed extends Partial<Req>> = Pick<
Req,
MissingKeys<Req, Passed>
> &
Partial<Omit<Req, MissingKeys<Req, Passed>>>;
/**
* γ を利用し, 可変長リストで引数自体を省略可能か表現する
*/
export type RequireMissing<Req, Passed extends Partial<Req>> =
Record<any, unknown> extends RequireMissingSubset<Req, Passed>
? [options?: RequireMissingSubset<Req, Passed>]
: [options: RequireMissingSubset<Req, Passed>];
使い方
// プロフィール作成に必要な項目
type Profile = {
name: string; // 名前(*必須*)
age: number; // 年齢(*必須*)
url?: string; // URL(任意)
description?: string; // 自己紹介(任意)
}
// ↓ コンストラクタから推論される型パラメータを与える
class ProfileFactory<O extends Partial<Profile>> {
// ↓ すべて Nullable にしておく
private readonly name: string|null;
private readonly age: number|null;
private readonly url: string|null;
private readonly description: string|null;
// ↓ 推論させる対象
constructor(options?: Exact<O, Partial<Profile>>) {
this.name = options?.name ?? null;
this.age = options?.age ?? null;
this.url = options?.url ?? null;
this.description = options?.description ?? null;
}
// ↓ 作成時に渡されなかった必須オプションを RequireMissing を用いたオーバーロードで要求
public create(...args: RequireMissing<Profile, O>): Profile;
// ↓ 実際は省略可能な Partial<Profile> 型として受け取る
public create(options?: Partial<Profile>): Profile {
return {
name: options?.name ?? this.name,
age: options?.age ?? this.age,
url: options?.url ?? this.url,
description: options?.description ?? this.description,
} as Profile; // ←アサーションが必要
}
}
// 必須パラメータはファクトリで埋められている
const factory1 = new ProfileFactory({
name: 'Bob',
age: 20,
});
// 空で作れる
factory1.create({});
// 引数自体を省略しても作れる
factory1.create();
// 部分的に必須パラメータをオーバーライドしても作れる
factory1.create({
name: 'Bob',
url: 'https://example.com/',
});
// 全部指定しても作れる
factory1.create({
name: 'Bob',
age: 20,
url: 'https://example.com/',
description: 'hello',
});
// 必須パラメータがファクトリで不足
const factory2 = new ProfileFactory({
age: 20,
});
// 足りないものを補えば作れる
factory2.create({
name: 'Bob',
});
// 部分的にオプションパラメータをオーバーライドしても作れる
factory2.create({
name: 'Bob',
url: 'https://example.com/',
});
// 全部指定しても作れる
factory2.create({
name: 'Bob',
age: 20,
url: 'https://example.com/',
description: 'hello',
});
// 空では不可
factory2.create({});
// 引数自体を省略すると不可
factory2.create();
// ファクトリに何もパラメータが与えられていない
const factory3 = new ProfileFactory();
// 必須パラメータをすべて指定すれば作れる
factory3.create({
name: 'Bob',
age: 20,
});
// 全部指定しても作れる
factory3.create({
name: 'Bob',
age: 20,
url: 'https://example.com/',
description: 'hello',
});
// 必須パラメータが足りなければ不可
factory3.create({
name: 'Bob',
url: 'https://example.com/',
});
// 空では不可
factory3.create({});
// 引数自体を省略すると不可
factory3.create();
// 余分なパラメータを与えるとコンストラクタでエラー
const factory4 = new ProfileFactory({
name: 'Bob',
age: 20,
foo: 123,
});
// 余分なパラメータは作成時にもエラー
const factory5 = new ProfileFactory({
name: 'Bob',
age: 20,
});
factory5.create({
foo: 123,
});
Special Thanks: @uhyo さん (改良に協力していただきました)
Discussion
『TypeScript で「コンストラクタで渡さなかった引数だけ後で必須にする」の型制約を書く』で引数自体を省略するパターンを認める方法
↑で改良パターン考え中
Done ✔