skip to content
usubeni fantasy logo Usubeni Fantasy

TypeScriptのジェネリック解析

/ 8 min read

This Post is Available In: CN EN ES JA

ジェネリック入門

ジェネリックは、型を変数として型定義に渡すということを簡単に言えば、関数に引数を渡すのと同じようなものです。例えば:

function identity<Type>(arg: Type): Type {
return arg;
}
let output = identity<string>("myString");

<>Typeを囲むことで、ジェネリック関数に型を渡すことができます。上記の関数は、Type型の引数を受け取り、Type型の結果を返します。

Typeは引数なので、名前も自由ですが、一般的にはTという名前を使用することがよくあります。したがって、以下のように書くこともできます:

function identity<T>(arg: T): T {
return arg;
}

型の推論

ジェネリック関数を使用する際には、Tの型を明示的に指定する必要はありません(実際、通常はそうします)。この場合、TS はTの型を自動的に推論します:

let output = identity("myString");
// outputもstring型です

先ほどの例でも、型<string>を明示的に指定しない場合、TS は"myString"の型をTとして推論するため、関数の戻り値も文字列になります。

型の制約

デフォルトでは、ジェネリックは任意の型になりますが、これでは可読性が低くなりますし、ジェネリックを適用した型に対して操作やメソッドの呼び出しを行う際には、任意の型であるため必ずチェックに通過しません。この問題を解決するために、extendsを使用してジェネリックに制約を設けることができます。

interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // argは必ずlengthプロパティを持つため、型チェックを通過します
return arg;
}

上記のコードでは、<T extends Lengthwise>によってTlengthプロパティを持つ型であることを示しています。lengthプロパティを持つ任意の型は、このジェネリックの要件を満たします。例えば、配列でも可能です。

バインディング能力

公式ウェブサイトによると、ジェネリックは型の再利用に使用されると述べていますが、上記の簡単な説明を見ると、これは非常に効果的であることがわかるでしょう。しかし、ジェネリックは型の再利用だけでなく、他の用途もあります。

私の答えは、型の連動です。T は同じ型定義内で使用される他のジェネリックにバインドすることができます

再度、この例を見てみましょう。実際には、入力の型と出力の型をバインドしています:

function identity<Type>(arg: Type): Type {
return arg;
}

以下は、より明確な「型のバインディング」の例です。

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // 可能
getProperty(x, "m"); // エラー、`Key`は`Type`のキーにバインドされるため、`m`は`Type`のキーではありません

マッピング型

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;

キーがabcであり、値が異なる関数であるオブジェクトがあるとします。このオブジェクトのキーと対応する関数の引数のオブジェクトの型を取得する必要がある場合、どのように実現できるでしょうか?

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
function wrapper<K extends keyof typeof myMap>(key: K, fn: (typeof myMap)[K]) {
return async function (...arg: Parameters<typeof fn>) {
// do something
await Promise.resolve();
fn.apply(null, arg);
};
}
const wrappedMap: {
[K in keyof typeof myMap]: ReturnType<typeof wrapper>;
} = {} as any;
for (const key in myMap) {
const k = key as keyof typeof myMap;
wrappedMap[k] = wrapper(k, myMap[k]);
}

この回答では、ジェネリック K を使用して key のタイプを制限し、それが myMap のキーであることを確認しています。その後、typeof myMap[K] を使用して、myMap 中の対応するキーの関数タイプを取得しています。wrapper 関数では、Parameters<typeof fn> を使用して関数 fn のパラメータータイプを取得しています。最後に、マップ型 [K in keyof typeof myMap]: ReturnType<typeof wrapper> を使用して、wrappedMap のタイプを定義し、そのキーと値が myMap と対応していることを確認しています。TypeScript は wrappedMap のタイプを推測できないため、型チェックを回避するために as any を使用しています。

const myMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
type MyMap = typeof myMap;
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
return async function (...arg: Parameters<T>) {
await Promise.resolve();
(fn as any).apply(null, arg);
};
}
type WrappedMap = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMap> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

現在、WrappedMap の型はラッパーの戻り値の効果を持っていることは確かですが、(fn as any).apply(null, arg) という部分は突然現れたように見えますね。

なぜ fnany にする必要があるのでしょうか?

なぜなら、TS にとって abc は値の引数の型と結びついていないからです。したがって、T を使用して制約をかけても効果がありません。少しわかりにくいかもしれませんが、次の回答 2 を見ればより明確になるかもしれません。

回答 2

const myMap: MyMap = {
a: () => {},
b: (someString: string) => {},
c: (someNumber: number) => {},
};
interface MyMapArgs {
a: [];
b: [someString: string];
c: [someNumber: number];
}
type MyMap = {
[K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void;
};
type MyKey = keyof MyMap;
function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
return async function (...arg: Parameters<F>) {
await Promise.resolve();
fn.apply(null, arg);
};
}
type WrappedMay = {
[K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>;
};
const wrappedMap: Partial<WrappedMay> = {};
for (const key in myMap) {
const k = key as MyKey;
wrappedMap[k] = wrapper(k, myMap[k]);
}

(fn as any) を削除する解決策は、まず関連付けたい要素を別のマップにマッピングし、上記の MyMapArgs のようにします。そして、このマッピングを使用して MyMap を作成します。これにより、TS はついにこれらの 2 つの要素が関連していることを理解します。

P.S. より詳細な情報については、issues#30581pull#47109 を参照してください。

评论组件加载中……