TypeScriptのジェネリック解析

2023-10-17#coding#TypeScript阅读时间约 5 分钟
This Post is Available In:CNENESJA

ジェネリック入門

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

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 を参照してください。


暂时没有留言,要抢沙发吗?
留言