Masayan tech blog.

  1. ブログ記事一覧>
  2. TypeScriptの型の変性について

TypeScriptの型の変性について

公開日

環境

  • 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種類がある。

項目

説明

不変(invariance

Tが必要

共変(covariance

Tか、Tのサブタイプが必要

反変(contravariance

Tか、Tのスーパータイプが必要

双変(bivariance

Tか、Tのスーパータイプ、Tのサブタイプが必要

変性は、言語や言語の概念ごとに異なる(例えば、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の型の変性について解説しました。プリミティブ型・オブジェクト・配列・関数のパラメータ・関数の返り値の変性についてその具体的なコードを例示しながら紹介していますので、ぜひ参考にしてみてください。