LoginSignup
3
1

型が同じであることをチェックする型

Posted at
1 / 2

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'.

このコードがうまく機能する理由を知るにはコンパイラのソースを読む必要がありそうです...

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1