環境
- macOS Monterey 12.6
- Windows 11
- VSCode
- node.js v18.6.0
- npm 8.13.2
- Typescript 4.6.4
スーパータイプとサブタイプ
本記事のテーマである変性について理解するには、まずスーパータイプとサブタイプについて理解しておく必要がある
- サブタイプとは、ある基本型から派生した型のことを指し、スーパータイプはその派生元の基本型を指す
- 具体例は以下の通り
- オブジェクトは配列のスーパータイプ
- number型は 数値1のスーパータイプ
- stringは、number | stringのサブタイプ
- anyは全ての型のスーパータイプ
- つまり、スーパータイプはより広い範囲を許容する型で、サブタイプは相対的に、より狭い範囲を許容する型だといえる
変性(variance)
変性(variance)は任意の型Tに対してどのような性質を持つのかを表したもの。大きく分けると以下4種類がある。
項目 | 説明 |
不変( |
|
共変( |
|
反変( |
|
双変( |
|
変性は、言語や言語の概念ごとに異なる(例えば、Javaのジェネリクスは不変だが、TypeScriptのジェネリクスは共変。TypeScriptの中でも配列という概念では共変だが、関数のパラメータは双変、といった具合に。)
本記事では、TypeScriptの以下の概念における変性について説明する。
- プリミティブの変性
- オブジェクトの変性
- 配列の変性
- ジェネリクスの変性
- 関数の変性
- パラメータの変性
- 返り値の変性
プリミティブの変性
プリミティブは共変である
T自身の代入
Super型の変数を別のSuper型の変数に代入できる
type Super = string | number;
let super1: Super = "a";
const super2: Super = 3;
super1 = super2; // OK
TのサブタイプをTに代入
Super型の変数に、そのサブタイプであるSub型の変数を代入できる
type Super = string | number;
type Sub = string;
let super1: Super = 2;
const sub1: Sub = 'hoge';
super1 = sub1; // OK
TをTのサブタイプに代入
Super型の変数をそのサブタイプであるSub型の変数に代入することはできない
type Super = string | number;
type Sub = string;
const super1: Super = 2;
let sub1: Sub = 'hoge';
sub1 = super1; // 型 'number' を型 'string' に割り当てることはできません。
つまり、T
か、T
のサブタイプが必要なので、共変である
オブジェクトの変性
オブジェクトはプロパティに対して共変
T自身の代入
Super型のオブジェクトを別のSuper型のオブジェクトに代入できる
interface Super {
id: number;
name: string | undefined;
}
let super1: Super = { id: 1, name: 'hoge1' };
const super2: Super = { id: 2, name: undefined };
super1 = super2; // OK
TのサブタイプをTに代入
Super型のオブジェクトに、そのサブタイプであるSub型のオブジェクトを代入できる
interface Super {
id: number;
name: string | undefined;
}
interface Sub extends Super {
id: number;
name: string;
}
let super1: Super = { id: 1, name: undefined };
const sub1: Sub = { id: 2, name: 'hoge1' };
super1 = sub1; //OK
TをTのサブタイプに代入
Super型のオブジェクトをそのサブタイプであるSub型のオブジェクトに代入することはできない
interface Super {
id: number;
name: string | undefined;
}
interface Sub extends Super {
id: number;
name: string;
}
const super1: Super = { id: 1, name: undefined };
let sub1: Sub = { id: 2, name: 'hoge1' };
sub1 = super1; //型 'string | undefined' を型 'string' に割り当てることはできません。
つまり、T
か、T
のサブタイプが必要なので、共変である
配列の変性
- 配列は共変である
- T[]に対しては、T[]だけでなく、Tのサブタイプ[]も割り当て可能
T自身の代入
string[] | number[]
型の配列を別のstring[] | number[]
型の配列に代入できる
let super1: string[] | number[] = [1, 2, 3, 4, 5];
const super2: string[] | number[] = ['11', '12', '13', '14', '15'];
super1 = super2; // OK
TのサブタイプをTに代入
string[] | number[]
型の配列に、そのサブタイプであるstring[]
型の配列を代入できる
let super1: string[] | number[] = [1, 2, 3, 4, 5];
const sub1: string[] = ['hoge', 'foo'];
super1 = sub1; // OK
TをTのサブタイプに代入
string[] | number[]
型の配列をそのサブタイプであるstring[]
型の配列に代入することはできない
const super1: string[] | number[] = [1, 2, 3, 4, 5];
let sub1: string[] = ['hoge', 'foo'];
sub1 = super1; // 型 'number[]' を型 'string[]' に割り当てることはできません。
つまり、T
か、T
のサブタイプが必要なので、共変である
ジェネリクスの変性
ジェネリクスの変性は共変である
T自身の代入
string | undefined
型をジェネリクスに指定した配列を別のstring | undefined
型をジェネリクスに指定した配列に代入できる
type SuperArray<T> = T[];
type SubArray<T> = T[];
let superArray1: SuperArray<string | undefined> = ['a', 'b', 'c', 'd', 'e', 'f'];
const superArray2: SuperArray<string | undefined> = [undefined, 'a', 'b', 'c'];
superArray1 = superArray2; // OK
TのサブタイプをTに代入
string | undefined
型をジェネリクスに指定した配列に、そのサブタイプであるstring
型をジェネリクスに指定した配列を代入できる
type SuperArray<T> = T[];
type SubArray<T> = T[];
let superArray: SuperArray<string | undefined> = ['a', 'b', 'c', undefined];
const subArray: SuperArray<string> = ['a', 'b', 'c'];
superArray = subArray; // OK
TをTのサブタイプに代入
string | undefined
型をジェネリクスに指定した配列を、そのサブタイプであるstring
型をジェネリクスに指定した配列に代入できない
type SuperArray<T> = T[];
type SubArray<T> = T[];
const superArray: SuperArray<string | undefined> = ['a', 'b', 'c', undefined];
let subArray: SuperArray<string> = ['a', 'b', 'c'];
subArray = superArray; // 型 'SuperArray<string | undefined>' を型 'SuperArray<string>' に割り当てることはできません。
つまり、T
か、T
のサブタイプが必要なので、共変である
関数の変性
関数の返り値の変性
関数の返り値は共変である
T自身の代入
string | number
型を返り値とする関数を別のstring | number
型を返り値とする関数に代入できる
type Super = () => string | number;
let super1: Super = () => 1;
const super2: Super = () => 2;
super1 = super2; // OK
TのサブタイプをTに代入
string | number
型を返り値とする関数に、string
型を返り値とする関数を代入できる
type Super = () => string | number;
type Sub = () => string;
let super1: Super = () => 1;
const sub1: Sub = () => '1';
super1 = sub1; // OK
TをTのサブタイプに代入
string[] | number[]
型を返り値とする関数をそのサブタイプであるstring[]
型を返り値とする関数に代入することはできない
type Super = () => string | number;
type Sub = () => string;
const super1: Super = () => 1;
let sub1: Sub = () => '1';
sub1 = super1; // 型 'Super' を型 'Sub' に割り当てることはできません。
つまり、T
か、T
のサブタイプが必要なので、共変である
関数のパラメータの変性
関数のパラメーターは双変である
T自身の代入
string | number
型をパラメータとする関数を別のstring | number
型をパラメータとする関数に代入できる
type SuperFunc = (x: string | number) => void;
let super1: SuperFunc = (x: string | number) => console.log(x);
const super2: SuperFunc = (x: string | number) => console.log(x);
super1 = super2; // OK
TのサブタイプをTに代入
string | number
型をパラメータとする関数に、string
型をパラメータとする関数を代入できる
type SuperFunc = (x: string | number) => void;
type SubFunc = (x: string) => void;
let super1: SuperFunc = (x: string | number) => console.log(x);
const sub1: SubFunc = (x: string) => console.log(x);
super1 = sub1; // OK
TをTのサブタイプに代入
string | number
型をパラメータとする関数をそのサブタイプであるstring
型をパラメータとする関数に代入することができる
type SuperFunc = (x: string | number) => void;
type SubFunc = (x: string) => void;
const super1: SuperFunc = (x: string | number) => console.log(x);
let sub1: SubFunc = (x: string) => console.log(x);
sub1 = super1; // OK
つまり、T
か、T
のスーパータイプ、もしくはT
のサブタイプが必要なので、双変である
ただし、上記内容は、tsconfig.jsonのstrictFunctionTypesの値によって変わるため注意が必要です。(strictをtrueにするとstrictFunctionTypesも有効になります)
{
"compilerOptions": {
"strict": true,
・・・
strictFunctionTypesがtrueであれば、関数のパラメータは反変に、falseであれば双変となります。
- より狭い引数を許容する関数に対してより広い引数を許容する関数を適用することは可能
- なぜなら、sub1()の引数には、undefinedが入る余地がなく、xがundefinedかどうかをチェックする処理は実質上不要となり、問題なく動作する
- ※以下は
strictFunctionTypes
がtrueの環境
type SuperFunc = (x: string | undefined) => void;
type SubFunc = (x: string) => void;
const super1: SuperFunc = (x: string | undefined) => {
if (x === undefined) {
return;
}
x.toString();
};
let sub1: SubFunc = (x: string) => x.toString();
sub1 = super1; // OK
- いっぽう、より広い引数を許容する関数に対してより狭い引数を許容する関数を適用することは不可
- なぜなら、super1()の引数には、undefinedが入る余地があるが、xがundefinedかどうかをチェックする処理はsuper1の内部で保持されているsub1には存在しないため、動作しない
type SuperFunc = (x: string | undefined) => void;
type SubFunc = (x: string) => void;
let super1: SuperFunc = (x: string | undefined) => {
if (x === undefined) {
return;
}
x.toString();
};
const sub1: SubFunc = (x: string) => x.toString();
super1 = sub1; // NG
super1(undefined); // Uncaught TypeError: Cannot read properties of undefined (reading 'toString')
まとめ
いかがでしたでしょうか。本記事では、TypeScriptの型の変性について解説しました。プリミティブ型・オブジェクト・配列・関数のパラメータ・関数の返り値の変性についてその具体的なコードを例示しながら紹介していますので、ぜひ参考にしてみてください。