Madogiwa Blog

主に技術系の学習メモに使っていきます。

TypeScript: ジェネリック型を使って配列を扱うときに`extends Type`して`T[]`とするのと`extends Type[]`して`T`とする場合の扱われ方の違いMEMO

下記のような場合に<T extends unknown>(arg: T[]) => T[]と定義した場合だけ、TypeScriptのコンパイル時にエラーが発生して🤔となったけど納得したのでMEMOしておきます📝

type TypeA = <T extends unknown>(arg: T[]) => T[]
type TypeB = <T extends unknown[]>(arg: T) => T
const funcA: a = (...args) => args
const funcB: b = (...args) => args

funcA("aaa", 1) // 型 'number' の引数を型 'string' のパラメーターに割り当てることはできません。ts(2345)
funcB("aaa", 2)

検証した環境は下記の通りです。

$ npx tsc -v
Version 3.9.7

個人的には最初は同じかと思っていたのですが、、、TypeScriptは実際には下記ような形で型を設定していました。

const funcA: <string>(...args: string[]) => string[]
const funcB: <[string, number]>(args_0: string, args_1: number) => [string, number]

エラーが発生していたfuncA...args: string[]となっているためfuncA("aaa", 1)number型の引数1を渡しているため型エラーが発生していたようです。

流れとしてはジェネリック型は呼び出し時に動的に型が決まります。 funcAの場合は<T extends unknown>としており、funcA("aaa", 1)の最初の引数の型がstringのためジェネリックTstringに置き換わり、string[]型が指定されてconst funcA: <string>(...args: string[]) => string[]となっているようです👀

// 最初の定義
const funcA: <T extends unknown>(...args: T[]) => T[]
// funcA("aaa", 1)の呼び出しによって第一引数の型がTに反映される
const funcA: <string>(...args: string[]) => string[]

ジェネリック型のTが配列として定義されていないので最初に合致したstring型に置き換わってしまうのがポイントっぽいですね。

逆にfuncBの場合は<T extends unknown[]>としているので可変長引数がタプルとして扱い、そのままT[string, number]に置き換わっているのでエラーにならないようです👀

// 最初の定義
const funcB: <T extends unknown[]>(arg: T) => T
// funcB("aaa", 1)の呼び出しによって引数のタプルがTに反映される
const funcB: <[string, number]>(args_0: string, args_1: number) => [string, number]

どうやらジェネリック型の宣言で指定したものと合致するように解釈して型を推論してくれているみたいですね。

一応、funcAも下記のように明示的に指定してあげると(string|number)[]と判断してくれてエラーは発生しなくなりました😅

funcA<string | number>("aaa", 1)
const funcA: <string | number>(...args: (string | number)[]) => (string | number)[]

ジェネリック型の型推論難しい・・・😓