🐈‍⬛

TypeScript で型引数を部分的に指定したい

2022/02/25に公開

はじめに

TypeScript では関数呼び出し時に 1 つ以上の型引数を明示的に指定する場合、オプショナルでないすべての型引数を指定する必要があります。

function func<T, U>(value1: T, value2: U): [T, U] {
  return [value1, value2];
}

// OK
const result1 = func("foo", 42);
const result2 = func<string, number>("foo", 42);

// ERROR: Expected 2 type arguments, but got 1.
const result3 = func<string>("foo", 42);

// これは OK
function func2<T, U = number>(value1: T, value2: U): [T, U] {
  return [value1, value2];
}
const result4 = func2<string>("foo", 42);

型引数を指定しない場合には string 型と number 型に推論してくれているので、result3 のような場合にも推論されてほしいです。このようなケースでコンパイラに推論してもらう方法を考えます。

解決策

関数をカリー化し(型)引数を 1 つずつ受け取るようにします。

function func<T>(value1: T): <U>(value2: U) => [T, U] {
  return (value2) => [value1, value2];
}

こうすることで、次のように型引数を部分的に指定することができるようになります。

const result1 = func<string>("foo")(42);
// typeof result1 => [string, number]

// OK
const result2 = func("foo")(42);
const result3 = func("foo")<number>(42);
const result4 = func<string>("foo")<number>(42);

// ERROR
const result5 = func<number>("foo")(42);
const result6 = func<string>("foo")<boolean>(42);

活用例

ある型を満たす値を作りたいが、その型にアップキャストされてほしくない場合を考えます。

type User = {
  name: string;
  age: number;
};

// 上記の User 型を満たす値を宣言したいが、User 型にアップキャストされてほしくない
const user: User = {
  name: "foo" as const,
  age: 42,
};

// typeof user => User
// 本当は { name: "foo"; age: number } という型で欲しい

これを回避したい場合、次のようにあらかじめ値を宣言しておき目的の型の変数に代入するなどしてコンパイルエラーを発生させることで型を満たしていることを確認できますが、エラーにすぐ気づけなかったり、宣言時に補完が効かなかったり、コンパイラオプションや ESLint の設定によっては未使用の変数や引数、型の宣言がエラーになったりします。

const user = {
  name: "foo" as const,
  aga: 42, // aga ではなく age だが、ここではエラーにならない
};

// 方法 1
const check: User = user;

// 方法 2
function check<T>(value: T): void {
  return;
}
check<User>(user);

// 方法 3
type Expect<T extends true> = T;
type Check = Expect<typeof user extends User ? true : false>;

次のような方法も考えられますが、これは先述の通りコンパイルエラーになります。

function typing<T, U extends T>(value: U): U {
  return value;
}

// ERROR: Expected 2 type arguments, but got 1.
const user = typing<User>({
  name: "foo" as const,
  age: 42,
});

ここで上記の関数を次のようにカリー化します。

function typing<T>(): <U extends T>(value: U) => U {
  return (value) => value;
}

こうすることで、先述の問題を解決しながら User 型を満たす値を作ることができます。

const user1 = typing<User>()({
  name: "foo" as const,
  age: 42,
});
// typeof user => { name: "foo"; age: number }

// ERROR
const user2 = typing<User>()({
  name: "foo" as const,
  aga: 42, // 'aga' does not exist in type 'User'.
});
const user3 = typing<User>()({
  name: "foo" as const,
  age: true, // Type 'boolean' is not assignable to type 'number'.
});
GitHubで編集を提案

Discussion