部分型って"拡張型"じゃない?集合論で捉えてみる
TypeScriptの部分型
TypeScriptの部分型は、構造的部分型というものが採用されています。
「この型はある型の部分型である」というように宣言[1]するのではなく、型の構造によって部分型かどうかの判定がされます。
(構造的部分型が採用されていることから、一見コード上で関係のなさそうな型同士に部分型の関係が発生することもあります。)
部分型の定義
型S
が型T
の部分型であるとは、S
型の値がT
型の値でもあることです。
type T = { str: string }
type S = { str: string, num: number }
const tObj: T = {str:'t'}
const sObj: S = {str:'s', num:314}
const t: T = sObj
const s: S = tObj // Property 'num' is missing in type 'T' but required in type 'S'.(2741)
上のコードでは、T
型の変数にS
型のオブジェクトを代入することができています。このようなとき、S
型はT
型の部分型であるといいます。
部分型というより拡張型?
先のコード例で、型T
がstring
型のstr
というプロパティが存在することを要求しているのに対し、型S
は追加でnumber
型のnum
というプロパティの存在も要求していました。
T
型に含まれる必須プロパティはS
型にも必ず含まれているのに対し、S
型の必須プロパティは必ずしもT
型には含まれません。
集合論において、「SがTの部分集合である」とは、「集合Sに含まれる要素がすべて集合Tに含まれること」でしたから、S
型はT
型の部分型というよりは拡張型の方が相応しいのでは?と疑問に感じるかもしれません。
型引数の制約として、型T
が型U
の部分型であることを課すときにT extends U
と書きますが、このextends
からも拡張型の語の方が適しているように思えます。
拡張型の方が相応しいのか、オブジェクトがT
型に属する条件をみていくことで考えていきたいと思います。
上のコード例で言うと、オブジェクトがT
型に属する条件とは、「オブジェクトがstring
型のプロパティstr
をもつこと」でした。
対して、オブジェクトがS
型に属する条件とは、「オブジェクトがstring
型のプロパティstr
をもつこと」に加えて、「オブジェクトがnumber
型のプロパティnum
をもつこと」でした。
これらの条件を見比べてみると、S
型に属するための条件を満たすオブジェクトは必然的にT
型に属する条件を満たしていることがわかります。
このことから、型S
が型T
の拡張型なのではなく、部分型であることがわかります。
(拡張型では?と言う疑念は型をオブジェクトのように捉えて包含関係を考えてしまっていたことに起因していたわけですね。)
集合に対応する型
never
型
空集合 以下の記事でも扱いました、空集合に対応する型としてnever
型というものがあります。
unknown
型
全体集合 unknown
型はどんな値でも代入することができる型です。
このことは、任意の型T
の値がunknown
型の値でもあることを示しており、T
型がunknown
型の部分型であることがわかります。このことは、「任意の集合は全体集合の部分集合である」という全体集合の性質に対応していますね。
加えて、T | unknown
がunknown
型となることや、T & unknown
型がT
型になることがそれぞれ「全体集合との和集合は全体集合であること」、「全体集合との積集合はもとの集合自身であること」という全体集合の性質と対応しています。
type T = {str: string, num: number}
type U = T | unknown //type U = unknown
type V = T & unknown //type V = { str: string; num: number; }
これらのことから、unkonown
型は全体集合に対応していると見なすことができます。
関数型の部分型
関数の型にも部分型の関係が発生することがあります。
引数型が同じ場合の部分型
引数の型が同じ場合には、返り値の型によって部分型の関係が確認できます。
型S
を型T
の部分型としたとき、引数の型をA
とすると、関数型(A) => S
は関数型(A) => T
の部分型となります。
これは、(A) => S
型の関数はS
型の値を返すので、部分型の定義からT
型の値を返す関数と見なすことができるためです。
type A = string | number
type T = {a: A}
type S = {a: A, str: string}
// Fnの型はfunction Fn(arg: A): T
function Fn(arg: A): T{
return { a: arg }
}
//Gnの型はfunction Gn(arg: A): S
function Gn(arg: A): S {
return { str: 'str', a: arg }
}
const fnS: (arg: A) => S = Fn //Type '(arg: A) => T' is not assignable to type '(arg: A) => S'. Property 'str' is missing in type 'T' but required in type 'S'.(2322)
const gnT: (arg: A) => T = Gn
上のコード例から、(arg: A) => T
である関数型の変数に(arg: A) => S
型である関数Gn
を代入できていることがわかり、関数型(A) => S
は関数型(A) => T
の部分型となることがわかります。
引数の型が同じで返り値の型に部分型関係がある場合、関数型についても部分型関係は保存されることがわかります。
返り値の型が同じ場合の部分型
返り値の型が同じ場合にも、引数の型によって部分型の関係が確認できます。
型S
を型T
の部分型としたとき、返り値の型をR
とすると、関数型(T) => R
は関数型(S) => R
の部分型となります。
これは、(T) => R
型の関数の引数T
型にはS
型の値を代入可能であることからわかります。
type R = string | number
type T = {a: R}
type S = {a: R, str: string}
// Fnの型はfunction Fn(arg: T): R
function Fn(arg: T): R{
return arg.a
}
//Gnの型はfunction Gn(arg: S): R
function Gn(arg: S): R{
return arg.a
}
const fnT: (arg: T) => R = Gn // Type '(arg: S) => R' is not assignable to type '(arg: T) => R'. Types of parameters 'arg' and 'arg' are incompatible. Property 'str' is missing in type 'T' but required in type 'S'.(2322)
const gnS: (arg: S) => R = Fn
上のコード例から、(arg: S) => R
である関数型の変数に(arg: T) => R
型である関数Fn
を代入できていることがわかり、(arg: T) => R
の値は(arg: S) => R
の値でもあることがわかります。
部分型の定義から、関数型(T) => R
は関数型(S) => R
の部分型となります。
返り値の型が同じで引数の型に部分型関係がある場合、関数型については部分型関係が逆転していることに注意する必要があります。
共変性と反変性
「引数型が同じ場合の部分型」の項でもみたように、返り値の部分型関係が関数型の部分型関係と同じ向きとなる性質を関数型の返り値は関数型の共変の位置にあると言います。
また、「返り値の型が同じ場合の部分型」の項でみた、引数の部分型関係が関数型の部分型関係と反対の向きとなる性質を関数型の引数は関数型の反変の位置にあると言います。
関数型G
が関数型F
の部分型であるとは、「引数が反変の条件を満たす」「返り値が共変の条件を満たす」(と「Fの引数の個数がGの引数の個数以下」)を満たすことであると表すことができます。
-
宣言によって部分型を決定する方法は公称型や名前的部分型と呼ばれています。 ↩︎
Discussion