TypeScript で以下のようなコードを作成する機会がありました。
// 以下のコードは、Code という型と CodeList という型の配列を定義し、
// setupCodeList という関数に CodeList の値を渡して呼び出す例です。
const CODES = {
Foo: "001",
Bar: "002",
} as const;
type Code = (typeof CODES)[keyof typeof CODES];
type CodeList = { name: string; code: Code }[];
// setupCodeList は、CodeList 型の引数 value を受け取り、
// value を使った処理を行う関数です。
function setupCodeList(value: CodeList) {
// ...
}
const list = [
{
name: "Foo",
code: CODES.Foo,
},
{
name: "Bar",
code: CODES.Bar,
},
];
setupCodeList(list);
ここで、list
中に code
の値の設定漏れがあったときに、型でエラーになるようにしたいです。
const list = [
{
name: "Foo",
code: CODES.Foo,
},
];
// TODO: `code: "002"` が含まれていないため、型エラーにしたい
試したこと 1
以下のようなコードを試しました。
// 漏れがあったときに型エラーにしたい
type Equal<A, B> = A extends B ? (B extends A ? true : false) : false;
let check: Equal<Code, (typeof list)[number]["code"]> = true;
試しに Code の部分を別の型に変えてみると、エラーになってくれました。
// 漏れがあったときに型エラーにしたい
type Equal<A, B> = A extends B ? (B extends A ? true : false) : false;
let check: Equal<string, (typeof list)[number]["code"]> = true;
//-> 以下のエラーが発生した
// Type 'true' is not assignable to type 'false'.(2322)
これでよさそうと思ったのですが、以下を試したところなぜか期待通り動きませんでした...
const list = [
{
name: "Foo",
code: CODES.Foo,
},
];
// 漏れがあったときに型エラーにしたい
type Equal<A, B> = A extends B ? (B extends A ? true : false) : false;
let check: Equal<Code, (typeof list)[number]["code"]> = true;
//-> `code: CODES.Bar` が含まれていないのでエラーになってほしかったが、エラーにならなかった...
うまくいかなかった理由
以下の記事に詳細が書かれていました。
コンパイラは Conditional Types (T1 extends U1 ? X1 : Y1) のうち T1 が型パラメータ単体だった場合は必ず遅延評価する。この処理は Distributive Conditional Types のために存在する。
今回はこの T1 部分に型パラメータ単体を指定しており、Equal<"001", "001" | "002">
が Equal<"001", "001"> | Equal<"001", "002">
のような感じで評価された(?)ためにエラーにならなかったようです。
試したこと 2
上の記事にも書かれていた type-challenges のソースコード を使って書き替えてみました。
コードの意図もより明確になったように感じます。
// 以下のコードは、Code という型と CodeList という型の配列を定義し、
// setupCodeList という関数に CodeList の値を渡して呼び出す例です。
const CODES = {
Foo: "001",
Bar: "002",
} as const;
type Code = (typeof CODES)[keyof typeof CODES];
type CodeList = { name: string; code: Code }[];
// setupCodeList は、CodeList 型の引数 value を受け取り、
// value を使った処理を行う関数です。
function setupCodeList(value: CodeList) {
// ...
}
const list = [
{
name: "Foo",
code: CODES.Foo,
},
{
name: "Bar",
code: CODES.Bar,
},
];
// type-challenges のコードを追加した
// https://github.com/type-challenges/type-challenges/blob/fbd74f4067fb43ebbf020f864ef404e99deb585f/utils/index.d.ts#L1
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
type Check = Expect<Equal<Code, (typeof list)[number]["code"]>>;
setupCodeList(list);
list から CODES.Bar を消してみるとエラーになります。
const list = [
{
name: "Foo",
code: CODES.Foo,
},
];
// type-challenges のコードを追加した
// https://github.com/type-challenges/type-challenges/blob/fbd74f4067fb43ebbf020f864ef404e99deb585f/utils/index.d.ts#L1
type Expect<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y
? 1
: 2
? true
: false;
type Check = Expect<Equal<Code, (typeof list)[number]["code"]>>;
//-> Type 'false' does not satisfy the constraint 'true'.
このコードがうまく機能する理由を知るにはコンパイラのソースを読む必要がありそうです...