個人的Typescriptの型入門① 〜 型の基本


※ 当ページには【広告/PR】を含む場合があります。
2024/09/08
【Javascript基礎講座】AsyncGeneratorを正しく初期化する
個人的Typescriptの型入門② 〜 オブジェクト型の応用
蛸壺の技術ブログ|個人的Typescriptの型入門① 〜 型の基本
目次

個人的にはほどほどの付き合いをしていると思って使っているTypescriptの型ですが、場合によっては利用するライブラリがバリバリの型実装で書かれているものを読み解く必要があります。

自分ではそんなマニアックかつ高度な型実装を使うこともない場合でも、他人のコードを見たときにある程度分かるようになっておきたいところです。

そこで今回はTypescriptの型の初歩を抑えつつ、例を交えながら段階的にTypescriptの型にまつわる利用法を列挙していきます。

なお、巷ではとても参考になるTypescriptの型に関する記事が多く存在しています。

とりわけこの記事では以下の詳しい解説されているサイトをベースに、著者が防備録として抑えておきたい利用法をピックアップしています。

参考|TypeScriptの型入門

詳しい説明が必要の際には、ご本家のほうでご参照ください。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

型の基礎

復習も兼ねて、型の基礎のほうから徐々にTypescriptの型の使い方を慣らして行きましょう。

なおTypescriptの型チェックは、VSCode等のエディタで用意されているものを扱うことを想定していますが、サッとTypescriptを試したい際には定番の
「Typescript Playground」を使うのがもっとも手っ取り早いでしょう。

Typescript Playground

プリミティブ型

TypescriptでもJavaScriptのプリミティブ型に対応した
string, number, boolean, symbol, bigint, null, undefinedがあり、これらの型は代入不可能です。

            
            const a: number = 3;

//✘ Type 'number' is not assignable to type 'string'.
const b: string = a;
        

strictNullChecksオプション

Typescriptのコンパイルオプション・
--strictNullChecks(tsconfig.jsonではstrictNullChecks)が無効なら、null/undefinedは他の型の値として扱うことができます。

            
            const a: null = null;

//(strictNullChecksがオンの場合)
//✘ Type 'null' is not assignable to type 'string'.
const b: string = a;
        

リテラル型

リテラル型はプリミティブ型からさらに細分化した型です。

文字列リテラル型/数値リテラル型/ブーリアンリテラル型があり、それぞれ'foo'や3やtrueというような名前の型で使えます。

            
            const a: 'foo' = 'foo';

//✘ Type '"foo"' is not assignable to type '"bar"'.
const b: 'bar' = 'foo';
        

リテラル型 --> 上位の型

文字列リテラル型はstring型の部分型であるため、文字列リテラル型をstring型として扱うことができます。

            
            const a: 'foo' = 'foo';
const b: string = a;
        
他のリテラル型も同様です。

            
            const a: 3 = 3;
const b: number = a;
        
            
            const a: true = true;
const b: boolean = a;
        

リテラル型の型推論

const変数に型注釈を省略してもちゃんとリテラル型として推論されます。

            
            //aは'foo'型(文字列リテラル型)と推論
const a = 'foo';

//✘ Type '"foo"' is not assignable to type '"bar"'.
const b: 'bar' = a;
        

let変数の型推論

リテラル型の型推論は
constを使って変数を宣言した場合に限り、let/varを使うとstring型で推論されます。

            
            //aはstring型と推論
let a = 'foo';

const b: string = a;
//✘ Type 'string' is not assignable to type '"foo"'.
const c: 'foo' = a;
        
letで宣言した変数の型をリテラル型にしたい場合には、型注釈が必須です。

            
            //aは'foo'型
let a: 'foo' = 'foo';

//✘Type '"bar"' is not assignable to type '"foo"'.
a = 'bar';
        

オブジェクト型

TypeScriptで、Javascriptオブジェクトを表現するための型がオブジェクト型であり、
{ }の中にプロパティ名とその型を列挙するものに対応します。

例えば、
{foo: string; bar: number}というオブジェクト型は、fooというプロパティがstring型、barというプロパティがnumber型、を値に持ちます。

Typescriptではオブジェクト型の宣言は、
interface構文とtype(型エイリアス)構文の2つが使えます。

            
            interface MyObj {
    foo: string;
    bar: number;
}

type MyObj = {
    foo: string;
    bar: number;
}
        
この例では、{foo: string; bar: number}という型にMyObjという名前を付けています。

オブジェクト型にすることで変数の型注釈として付けることができます。

            
            interface MyObj {
    foo: string;
    bar: number;
}

const a: MyObj = {
    foo: 'foo',
    bar: 3,
};
        
なお、この例で型注釈を付けない場合、オブジェクト型({foo: string; bar: number})を推論してくれます。

オブジェクト型の型エラー

型が合わないオブジェクトを変数に代入したりしようとすると型エラーとなります。

            
            interface MyObj {
    foo: string;
    bar: number;
}

const a: MyObj = {
    foo: 'foo',
    //✘Type 'string' is not assignable to type 'number'.
    bar: 'BARBARBAR',
};

//✘Property 'bar' is missing in type '{ foo: string; }' but required in type 'MyObj'.
const b: MyObj = {
    foo: 'foo',
};
        

オブジェクト型の部分型

TypeScriptの型システムにおいて、構造的な「部分型」を設計することができます。

以下の例で、MyObj型はMyObj2の部分型(派生型)になっています。

            
            interface MyObj {
    foo: string;
    bar: number;
}

interface MyObj2 {
    foo: string;
}

const a: MyObj = {foo: 'foo', bar: 3};
const b: MyObj2 = a;
        
一般に部分型のオブジェクトはその上位の型として扱っても矛盾はありません。

この例では、部分型であるMyObj型の値aを、上位の型であるMyObj2型の変数bに代入することができます。

オブジェクトリテラル

interfacetypeなどを使って型名を宣言せずに、中括弧「{...}」を使い、{foo: string, bar: numbder}などとオブジェクト型を表現することを「オブジェクトリテラル」と呼びます。

            
            let obj1: {id: number, name: string};

const obj2 = {id: 1, name: "hoge"};
        
上の例では、オブジェクトリテラルを使って、obj1は{id: number, name: string}型の変数として宣言できます。

obj2も
{id: number, name: string}型に推論されるため、{id: 1, name: "hoge"}の部分がオブジェクトリテラルと言えます。

このようにオブジェクトリテラルは、即席の型定義を記述する場合に便利な表記法です。

オブジェクトリテラルで部分型を使う注意点

オブジェクトリテラルに対しては適用外ルールがあり、次のような場合には注意が必要です。

            
            interface MyObj {
    foo: string;
    bar: number;
}

interface MyObj2 {
    foo: string;
}

//✗Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
const b: MyObj2 = {foo: 'foo', bar: 3};

//あまり意味は無いが以下ならエラーなし
const b: MyObj2 = {foo: 'foo', bar: 3} as MyObj;
        
一見、{foo: 'foo', bar: 3}のオブジェクトリテラルから推測される{foo: string, bar: number}型は、MyObj2型の部分型を満たしているため、先ほど説明したように上位のMyObj2型に代入できるはずです。

実際は、このオブジェクトリテラルはMyObj2型に代入できずに、エラーになっています。

これは上位型-部分型の関係性を吟味する以前の問題で、「単なる書き間違いじゃない?」とTypeScriptで判断されるからのようです。

同様に、このオブジェクトリテラルの適用外ルールは、関数の引数の場合にも起こります。

            
            interface MyObj2 {
    foo: string;
}

function func(obj: MyObj2): void { }

//✗Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
func({foo: 'foo', bar: 3});

//あまり意味は無いが以下ならエラーなし
func({foo: 'foo', bar: 3} as MyObj2);
        

配列型

配列型を表すためには
[]もしくはArray<>を用います。

            
            const foo: number[] = [0, 1, 2, 3];
//もしくは
//const foo: Array<number> = [0, 1, 2, 3];

foo.push(4);
        

関数型

関数型は例えば
(x: string, y: number)=>booleanのように表現されます。

これは、第1引数としてstring型の、第2引数としてnumber型の引数をとり、返り値としてboolean型の値を返す関数型、を意味しています。

            
            function func(arg: string): number {
    return Number(arg);
}

const f: (foo: string)=>number = func;
        

関数型の引数と部分型

関数型の引数にも、上位型-部分型の関係が考慮される必要があります。

以下の例は、関数型の引数に用いられるオブジェクト型で、MyObjがMyObj2の部分型である場合、その関数型である
(obj: MyObj2)=>void型は、(obj: MyObj)=>void型として扱うことができます。

            
            interface MyObj {
    foo: string;
    bar: number;
}

interface MyObj2 {
    foo: string;
}

const a: (obj: MyObj2)=>void = () => {};
const b: (obj: MyObj)=>void = a;
        
これは、「上位型(MyObj2)を入力して処理できる関数は、部分型(MyObj)を入力しても当然処理できるはず」、という意味のようです。

逆にすると、「部分型を入力して処理できる関数が、上位型を入力しても正しく処理できる保証はない」ので、以下のようにエラーとなります。

            
            interface MyObj {
    foo: string;
    bar: number;
}

interface MyObj2 {
    foo: string;
}

const b: (obj: MyObj)=>void = () => {};

//✗ Type '(obj: MyObj) => void' is not assignable to type '(obj: MyObj2) => void'.
//   Types of parameters 'obj' and 'obj' are incompatible.
//   Property 'bar' is missing in type 'MyObj2' but required in type 'MyObj'.
const a: (obj: MyObj2)=>void = b;
        

関数型の引数の数と部分型

関数型の引数の数に着目すると、先程と同じ理屈で、
(foo: string)=>void型を(foo: string, bar: number)=>void型として使うことができます。

            
            const f1: (foo: string)=>void = () => {};
const f2: (foo: string, bar: number)=>void = f1;
        
ここでのf2からすると、2番目の引数bar: numberを無視すればf1として扱えるとみなせる、という意味です。

これを逆にしてしまうとエラーが出ます。

            
            const f1: (foo: string, bar: number)=>void = ()=>{};

//✗ Type '(foo: string, bar: number) => void' is not assignable to type '(foo: string) => void'.
//   Target signature provides too few arguments. Expected 2 or more, but got 1.
const f2: (foo: string)=>void = f1;
        
引数の数の多い関数型を、それより引数の少ない関数型としては扱えないためです。

ただし、特殊な書き間違い防止ルールで、関数を呼び出す側の余計な引数は無視してもらえないので注意しましょう。

            
            const f1: (foo: string)=>void = () => {};

//✗ Expected 1 arguments, but got 2.
f1('foo', 3);
        

可変長引数

TypeScriptで可変長引数の関数を宣言には配列に対応します。

次の例では、...barに
number[]型が付いているため、2番目以降の引数は全て数値型となります。

            
            const func = (foo: string, ...bar: number[]) => bar;

func('foo');
func('bar', 1, 2, 3);
//✗ Argument of type '"hey"' is not assignable to parameter of type 'number'.
func('baz', 'hey', 2, 3);
        

void型

void型は「何も返さない」ことを表す関数の返り値の型です。

JavaScriptでの何も返さない関数はundefinedを返すので、void型というのはundefinedのみを値として取る型となります。

実際、void型の変数にundefinedを入れても問題ありません。

            
            const a: void = undefined;
        
ただし、その逆のvoid型をundefined型の変数に代入することはできません。

            
            const a: void = undefined;

//✗ Type 'void' is not assignable to type 'undefined'.
const b: undefined = a;
        
通常、void型を変数として利用するケースはほぼ皆無で、「何も返さない関数の返り値の型」として使います。

            
            function foo(): void {
    console.log('hello');
}
        

any型

any型はどんな型とも相互変換可能であり、実質TypeScriptの型システムを無視することに相当します。

            
            const a: any = 3;
const b: string = a;
        
any型を使うともはや「何でもあり」となり、TypeScriptの型システムを使う意味がなくなってしまいます。

クラス宣言とオブジェクト型

JavaScriptにもクラスを定義する
class構文がありますが、TypeScriptでclass構文を使うと、クラス定義と同名のオブジェクト型定義を同時に行うことになります。

            
            class Foo {
    method(): void {
    console.log('Hello, world!');
    }
}

const obj: Foo = new Foo();

const obj2: Foo = {
    method: () => {}
};
        
この例でいうと、Fooはクラス定義であり、同時にオブジェクト型でもあります。

よって、Typescriptにおける
class構文はクラス定義のついでに、推論させるオブジェクト型の定義も行ってくれる機能に過ぎません。

Typescriptで純粋にオブジェクト型定義を行いたい場合、
classの利用は避けて、interfacetypeを使うことがベターです。

            
            interface MyFoo {
    method: () => void;
}

class Foo {
    method(): void {
    console.log('Hello, world!');
    }
}

const obj: MyFoo = new Foo();
const obj2: Foo = obj;
        

extends修飾子

Typescript独自の構文に
「extends」があります。

主にextendsの役割は、「interfaceを用いた型の継承(拡張)」です。

            
            interface User {
    name: string;
}

interface Root extends User {
    isMaster: boolean;
}

//👇Rootは以下と同じ定義

// interface Root {
//     name: string;
//     isMaster: boolean;
// }
        

ジェネリクス

ジェネリクスとは、「型引数」とも言われ、受け取った型を遅延で使用する仕組みを作ります。

            
            type A<T> = T;

const str: A<string> = 'moji';
const num: A<number> = 123;
        

オブジェクト型のジェネリクス

オブジェクト型の名前のあとに
< >で囲った列を与えることで、型の定義の中でそれらの名前を型変数として使うことができます。

以下の例では、2つの型変数
STを持つFoo型、という意味のFoo<S,T>型とみなすことができます。

            
            interface Foo<S, T> {
    foo: S;
    bar: T;
}

const obj: Foo<number, string> = {
    foo: 3,
    bar: 'hi',
};
        
例のように、Foo<number, string>のように使うと、型変数Sにnumber、型変数Tにstringが割り当てられることになります。

クラス定義や関数定義でもジェネリクス

ジェネリクスの構文は、クラス名・関数名のあとに
< >で囲った列を与えることで、クラス定義や関数定義にも利用できます。

            
            class Foo<T> {
    constructor(obj: T) {}
}

function func<T>(obj: T): void { }

const obj = new Foo<string>('foo');

func<number>(3);
        

ジェネリクス付きの関数型

先程の例で、ジェネリクスを使ったfuncの関数型は
<T>(obj: T)=>voidとなります。

関数型の場合、関数が呼び出される際に引数の型が決まるので、型変数もそのまま残しておく必要があるからです。

            
            function func<T>(obj: T): void {}

const f: <T>(obj: T)=>void = func;
        

型引数の推論

ジェネリクス付き関数を呼び出す際の型引数は省略することができます。

この場合、上の例でいうと
func<number>(3)<number>を省略して、func(3)となります。

引数を省略した場合、関数に与えた引数の情報から型引数の型が推論されます。

次の例では
getId(3)で、型引数「T」が、数値リテラル型「3」に推論されることになります。

            
            function getId<T>(value: T): T {
    return value;
}

const value = getId(3);

const num: number = value;
const three: 3 = value;

//✘Type '3' is not assignable to type 'string'.
const str: string = value;
        
getIdの返り値の型は3なので、変数valueの型も3となります。

3型の値は、string型に入れることができないのでエラーになります。

型引数の推論は便利な一方、複雑なケースでは型変数が推論できないこともあるようで、多用はなるべく控えたいところです。

ジェネリック付きの関数型のプロパティ限定

以下のようなジェネリクスを使った関数型では、型変数・Tにnameプロパティが存在するかわからないためエラーになります。

            
            function f<T>(arg: T): string {
    //✗ Property 'name' does not exist on type 'T'.
    return arg.name;
}
        
そこで、型変数Tはnameプロパティを持っているように制限をかけたいときにextendsを使います。

            
            interface User {
    name: string;
    age: number;
};

function f<T extends User>(arg: T): string {
    return arg.name;
}

f({ name: 'aaa', age: 123 });

//✗ Argument of type '{ age: number; }' is not assignable to parameter of type 'User'.
//   Property 'name' is missing in type '{ age: number; }' but required in type 'User'.
f({ age: 123 });
        
型変数TはUserの部分型であることが保証され、nameプロパティを持つことになります。

タプル型

JavaScriptには「タプル」はありませんが、TypeScriptでは「タプル型」が使えます。

タプル型は多様な型の値を要素を持つ固定長の配列で、関数から複数の値を配列に入れてまとめて返すようなユースケースに利用できます。

            
            const foo: [string, number] = ['foo', 5];

const str: string = foo[0];

function makePair(x: string, y: number): [string, number] {
    return [x, y];
}
        
上の例では、タプル型・[string, number]を使っています。

これは長さが2の配列で、1つ目が文字列型、2つ目が数値型が入ったような型を表しています。

タプル型の注意点

TypeScriptがタプルと呼んでいるものは、トランスパイル後に使えば結局はJavascriptの配列です。

次の例は、タプルの2番目の要素を数値型の値から文字列型に値に置き換えるものです。

            
            const tuple: [string, number] = ['foo', 3];

tuple.pop();
tuple.push('Hey!');

const num: number = tuple[1];

//"Hey!"
console.log(num);
        
配列の組込みメソッドでタプル型を破壊する際にエラーでないため、タプル型の2つ目の要素は数値型であるはずなのに、実行すると文字列型が出力されます。

このように現状、TypeScriptのタプル型は、予期しない挙動が起こる可能性があります。

タプル型の利用には危険が潜んでいることを覚えておくと良いでしょう。

要素数ゼロのタプル型

空の配列型を宣言しようとした場合、
any[]unknown[]T[]など色々と考えてしまいます。

タプルを使えば、配列の要素の型を考えることなく「要素数ゼロのタプル型」を作ることができます。

            
            const unit: [] = [];
        
使いどころはあまりないように思います。

可変長タプル型・その1

もはやタプル型とはなんなのかと言いたくなりますが、固定長の配列として表現されるタプル型を可変長にするテクニックも存在します。

            
            type NumAndStrings = [number, ...string[]];

const a1: NumAndStrings = [3, 'foo', 'bar'];
const a2: NumAndStrings = [5];
//✘ Type 'string' is not assignable to type 'number'.
const a3: NumAndStrings = ['foo', 'bar'];
        
最初のいくつかの要素の型が特別扱いして、残りを同種の型で有限数並べたような配列の型となります。

上の例で、変数a3は、最初の要素が数値でないのでエラーとなります。

可変長タプル型・その2

タプル型の内部の配列部分は、柔軟な位置に書くことができます。

例えば、最後の要素だけnumber型で他はstring型の配列は次のように書けます。

            
            type StrsAndNumber = [...string[], number];

const b1: StrsAndNumber = ['foo', 'bar', 'baz', 0];
const b2: StrsAndNumber = [123];
// ✘ Type '[string, string]' is not assignable to type 'StrsAndNumber'.
//    Type at position 1 in source is not compatible with type at position 1 in target.
//    Type 'string' is not assignable to type 'number'.
const b3: StrsAndNumber = ['foo', 'bar'];
        
ただし、スプレッド(...)を使えるのはタプル型のどこかに1回だけです。

以下のような可変長タプル型は許されていません。

            
            //✗ A rest element cannot follow another rest element.
type StrsAndNumber = [...string[], ...number[]];
        

オプショナルな要素を持つタプル型

[string, number?]のようにタプル型の内部の要素に?が付いたタプル型も宣言できます。

この場合、2番目の要素はあっても無くてもいい、という意味になります。

            
            type T = [string, number?];

const t1: T = ['foo'];
const t2: T = ['foo', 3];
        
オプショナルな要素は複数あっても構いませんが、そうでない要素より後に来なければいけません。

例えば以下のタプル型は不可です。

            
            //✗ A required element cannot follow an optional element.
type T = [string?, number];
        

タプル型と関数の可変長引数

タプル型は関数の可変長引数の型を表すのにも使えます。

下の例では、関数の可変長引数argsの型はタプル型
Args(=[string, number, boolean])で与えられます。

            
            type Args = [string, number, boolean];

const func = (...args: Args) => args[1];

//vはnumber型
const v = func('foo', 3, true);
        
引数の配列argsがタプル型Argsを持つようにするためには、関数funcの引数の型は、string、number、boolean、の順で与えなければいけません。

このテクニックを使えば、タプル型で、複数の関数の引数の型をまとめて指定することができます。

オプショナルな要素を持つタプル型を用いたオプショナルな引数を持つ関数型

同様に、オプショナルな要素を持つタプルの型を用いた場合はオプショナルな引数を持つ関数の型ができることになります。

            
            type Args = [string, ...number[]];

const func = (f: string, ...args: Args) => args[0];

const v1 = func('foo', 'bar');
const v2 = func('foo', 'bar', 1, 2, 3);
        

関数呼び出しのスプレッド構文とタプル型

JavaScriptでもスプレッド構文「...」という記法は関数呼び出しのときにも使うことができます。

以下の例では、func(...strings)の意味は、配列stringsの中身をfuncの引数に展開して呼び出すということです。

つまり、funcの最初の引数はstringsの最初の要素になり、2番目の引数は2番目の要素に……となります。

            
            const func = (...args: string[]) => args[0];

const strings: string[] = ['foo', 'bar', 'baz'];

func(...strings);
        
タプル型はここでも使うことができます。

適切なタプル型の配列を...で展開することで、型の合った関数を呼び出すことができるのです。

            
            const func = (str: string, num: number, b: boolean) => args[0] + args[1];

const args: [string, number, boolean] = ['foo', 3, false];

console.log(func(...args));
        

タプル型と可変長引数とジェネリクス

タプル型と可変長引数とジェネリクスを組み合わせることによって、さらに複雑な型表現が可能になります。

タプル型をとるような型変数を用いることで、関数の引数列をジェネリクスで扱うことができるのです。

例として、関数の最初の引数があらかじめ決まっているような新しい関数を作る関数bindを書いてみます。

            
            function bind<T, U extends any[], R>(
    func: (arg1: T, ...rest: U)=>R,
    value: T,
): ((...args: U) => R) {
    return (...args: U) => func(value, ...args);
}

const add = (x: number, y: number) => x + y;

const add1 = bind(add, 1);

console.log(add1(5)); // 6

//✗ Argument of type '"foo"' is not assignable to parameter of type 'number'.
add1('foo');
        
関数bindは2つの引数funcとvalueを取り、新しい関数(...args: U) => func(value, ...args)を返します。

この関数は、受け取った引数列argsに加えて最初の引数としてvalueをfuncに渡して呼び出した返り値をそのまま返す関数です。

ポイントは、まず
U extends any[]の部分です。

これは新しい記法ですが、「型引数Uはany[]の部分型でなければならない」、という意味です。

string[]などの配列型に加えてタプル型も全部any[]の部分型です。

この制限を加えることにより、
...rest: Uのように可変長引数の型としてUを使うことができます。

加えて、
bind(add, 1)の呼び出しでは型変数はそれぞれT = number, U = [number], R = numberと推論されます。

返り値の型は
(...args: U)=>Rすなわち(arg: number)=>numberとなります。

Uがタプル型に推論され、addの引数の情報が失われずにadd1に引き継がれています。

Union型

Union型は別名「合併型」とも呼ばれ、複数の型のどれかに当てはまるような型を表します。

記法としては、複数の型を
「|」でつなぎます。

例えば、
string | number型なら、「文字列または数値のUnion型」となります。

            
            let value: string | number = 'foo';
value = 100;
value = 'bar';

//✘ Type 'true' is not assignable to type 'string | number'.
value = true;
        

オブジェクト型のUnion型

プリミティブ型に限らずオブジェクト型でもUnion型を作ることができます。

            
            interface Hoge {
    foo: string;
    bar: number;
}

interface Piyo {
    foo: number;
    baz: boolean;
}

type HogePiyo = Hoge | Piyo;

const obj: HogePiyo = {
    foo: 'hello',
    bar: 0,
};
        

in演算子を使ったUnion型の絞り込み

Union型に代入された値がどの型になるのか、知りたい場合があります。

この例では「in演算子」を使った判定で、型を検出して適切に絞り込んでくれる機能があります。

            
            interface Hoge {
    foo: string;
    bar: number;
}

interface Piyo {
    foo: number;
    baz: boolean;
}

function useHogePiyo(obj: Hoge | Piyo): void {
    // objはHoge | Piyo型
    if ('bar' in obj) {
        //barプロパティがあるならHoge型
        console.log('Hoge', obj.bar);
    } else {
        //barプロパティがないならPiyo型
        console.log('Piyo', obj.baz);
    }
}
        
「in演算子」を'bar' in objと使うことで、barというプロパティがobjに存在するならtrueを返し、そうでないならfalseを返す、という処理が可能です。

in演算子を用いた型の絞り込みの注意点

in演算子で、次のようなコードでは注意が必要です。

            
            interface Hoge {
    foo: string;
    bar: number;
}
interface Piyo {
    foo: number;
    baz: boolean;
}

function useHogePiyo(obj: Hoge | Piyo): void {
    //objはHoge | Piyo型
    if ('bar' in obj) {
        //barプロパティがある --> objはHoge型
        console.log('Hoge', obj.bar);
    } else {
        //barプロパティがない --> objはPiyo型
        console.log('Piyo', obj.baz);
    }
}

const obj: Hoge | Piyo = {
    foo: 123,
    bar: 'bar',
    baz: true,
};

useHogePiyo(obj);
        
ここでobjに代入されている値は、Piyo型の部分型でかつbar: stringのプロパティが付いたものです。

Piyo型の部分型なので、
Hoge | Piyo型にも代入できます。

ただし、useHogePiyo関数の判定では、
'bar' in objがtrueになるため、Hoge型と見なされてしまうので、期待どおりには型の絞り込みができないことになります。

typeof演算子を用いた型の絞り込み

typeof演算子は与えられた値の型を文字列で返す演算子です。

typeof演算子によって、
typeof <変数>と書くと、その変数のプリミティブ型が得られます。

            
            let foo = 'str';

//FooTypeはstring
type FooType = typeof foo;

const str: FooType = 'abcdef';
        
プリミティブ型のUnion型なら、このtypeof演算子でもっと単純に型の絞り込みができます。

            
            function func(value: string | number): number {
    if ('string' === typeof value) {
        // valueはstring型なのでlengthプロパティを見ることができる
        return value.length;
    } else {
        // valueはnumber型
        return value;
    }
}
        
オブジェクトが絡まないこともあり、in演算子より確実です。

nullチェック

Union型がよく目にするのが、Nullableな値を扱いたい場合です。

例えば、
string | null型(文字列型かnull型)の値はnullかもしれないので、そのまま文字列の操作をすることは危険です。

典型的なやり方で、if文で
value != nullを使うと、適切に値のnullチェックができます。

            
            function func(value: string | null): number {
    if (value != null) {
        //valueはstring型に絞り込まれる
        return value.length;
    } else {
        return 0;
    }
}
        

&&や||でnullチェック

nullチェックと組み合わせて、
&&||で短絡実行するというテクニックも頻出度が高いです。

            
            function func(value: string | null): number {
    return value != null && value.length || 0;
}
        

タグ付きUnion型でオブジェクト型の絞り込み

プリミティブ型ならUnion型の絞り込みは確実性の高い実装ができるものの、オブジェクト型のUnion型は実装に工夫が必要です。

オブジェクト型のUnion型の絞り込みに推奨されているパターンとして、リテラル型とUnion型を組み合わせて、代数的データ型(タグ付きUnion型)を再現する方法があります。

例として、Option型を簡単に実装する例です。

            
            interface Some<T> {
    type: 'Some';
    value: T;
}
interface None {
    type: 'None';
}
type Option<T> = Some<T> | None;

function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
    if (obj.type === 'Some') {
        //objはSome<T>型
        return {
            type: 'Some',
            value: f(obj.value),
        };
    } else {
        //objはNone型
        return {
            type: 'None',
        };
    }
}
        
ここでのOption<T>型は、何か値を保持するSome<T>型と、値のない無いNone型のUnion型として表現されています。

ポイントは、共通プロパティである
typeプロパティで、オブジェクト型を識別する文字列リテラル型('Some'/'None')を指定します。

typeプロパティのリテラル型をタグとして使うことでUnion型の絞り込みを行っています。

次のように
switch文でも同じことができます。

            
            interface Some<T> {
    type: 'Some';
    value: T;
}
interface None {
    type: 'None';
}
type Option<T> = Some<T> | None;

function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
    switch (obj.type) {
    case 'Some':
        return {
            type: 'Some',
            value: f(obj.value),
        };
    case 'None':
        return {
            type: 'None',
        };
    }
}
        
map関数の場合はswitch文を使うほうが拡張に対して強くて安全です。

Union型のプロパティ

異なる型をもつ同名プロパティを持ったオブジェクト型でUnion型を作った場合、そのプロパティもUnion型として推論されます。

どういうことかというと、以下の例では
Hoge | Piyo型で共通した型が異なるfooプロパティはstring | number型となります。

            
            interface Hoge {
    foo: string;
    bar: number;
}

interface Piyo {
    foo: number;
    baz: boolean;
}

type HogePiyo = Hoge | Piyo;

function getFoo(obj: HogePiyo): string | number {
    //obj.fooはstring | number型
    return obj.foo;
}
        
配列の要素もプロパティの一種なので、同じ挙動となります。

            
            const arr: string[] | number[] = [];

    //string[] | number[]型の配列の要素はstring | number型
    const elm = arr[0];
        

never型

never型は「属する値が存在しない型」であり、型システムのもっとも下位にある部分型(任意の型の部分型)です。

よって、どんな値もnever型の変数に入れることはできません。

            
            //✘ Type '0' is not assignable to type 'never'.
const n: never = 0;
        
一方、never型の値はあらゆる型にも入ります。

            
            //never型の値を作る方法が無いのでdeclareで無理やり宣言
declare const n: never;

const foo: string = n;
        

never型のユースケース・その1

never型は「絶対に到達できない値」の概念を表現したもので、(TypeScriptの型システムを欺かない限り)never型の値を作ることは不可能です。

never型は、型システムを考える上で、ひっそりと推論される形で出現します。

            
            interface Some<T> {
    type: 'Some';
    value: T;
}
interface None {
    type: 'None';
}
type Option<T> = Some<T> | None;

function map<T, U>(obj: Option<T>, f: (obj: T)=> U): Option<U> {
    switch (obj.type) {
    case 'Some':
        return {
            type: 'Some',
            value: f(obj.value),
        };
    case 'None':
        return {
            type: 'None',
        };
    default:
        //objはnever型
        return obj;
    }
}
        
上の例では、処理がdefaultに到達することはありえないので、このobjはnever型になります。

never型のユースケース・その2

never型は値を返す可能性が無い関数の返り値にも現れます。

そのそも返り値が無いことを表すのはvoid型ですが、それは関数が正常終了する場合の話です。

関数がなんらかの異常で中断し、値を返すことなく関数を脱出した場合、これはnever型(到達不可能な値)です。

この挙動は、例えば以下のようなコードで確認できます。

            
            function func(): never {
    throw new Error('Hi');
}

const result: never = func();
        

Intersection型

Intersection型は別名「交差型」と呼ばれ、
「&」を使って複数のオブジェクト型の全てのプロパティを持ったオブジェクト型を作ります。

たとえばこの例では、
Hoge & Piyo型はHogeでもありPiyoでもある型を表します。

            
            interface Hoge {
    foo: string;
    bar: number;
}

interface Piyo {
    foo: string;
    baz: boolean;
}

const obj: Hoge & Piyo = {
    foo: 'foo',
    bar: 3,
    baz: true,
};
        

Union型とIntersection型を組みあわせ

例えば、Union型とIntersection型を組みあわせた、
(Hoge | Piyo) & Fuga型は、(Hoge & Fuga) | (Piyo & Fuga)型と同じです。

            
            interface Hoge {
    type: 'hoge';
    foo: string;
}
interface Piyo {
    type: 'piyo';
    bar: number;
}
interface Fuga {
    baz: boolean;
}

type Obj = (Hoge | Piyo) & Fuga;

function func(obj: Obj) {
    //objはFuga型でもありbazを参照可
    console.log(obj.baz);
    if (obj.type === 'hoge') {
        //objはHoge & Fuga型
        console.log(obj.foo);
    } else {
        // objはPiyo & Fuga型
        console.log(obj.bar);
    }
}
        

Union型を持つ関数型

関数型を含むUnion型というものも考えることができます。

            
            type Func = (arg: number) => number;
interface MyObj {
    prop: string;
}

const obj : Func | MyObj = { prop: '' };

//✘ Cannot invoke an expression whose type lacks a call signature.
//    Type 'MyObj' has no compatible call signatures.
obj(123);
        
当然ながら、関数型とそれ以外の型で作ったUnion型は関数として呼ぶことはできません。

構成が全部関数型のUnion型

では、全て関数型のメンバーでUnion型を作ったら、関数として呼び出せるかというと、そうではありません。

            
            type StrFunc = (arg: string) => string;
type NumFunc = (arg: number) => string;

declare const obj : StrFunc | NumFunc;
//✘ Argument of type '123' is not assignable to parameter of type 'string & number'.
//    Type '123' is not assignable to type 'string'.
obj(123);
        
この例では、文字列を受け取って文字列を返す関数型・StrFuncと、数値を受け取って文字列を返す関数型・NumFuncで、StrFunc | NumFunc型の変数objを作っています。

ですが、
StrFunc | NumFunc型の関数を呼ぶことは実質できません。

この場合、objはStrFunc型かもしれないので引数は文字列でないといけない一方、NumFunc型かもしれないので引数は数値でないといけません。

つまり、関数として呼び出せる引数は「文字列かつ数値(
string & number型)」であることが同時に要求されていますが、string & number型の値は存在しないため、この関数を呼ぶことは実質的にできないのです。

このように、関数型のUnion型を考えるとき、結果の関数の引数はもともとの引数同士のIntersection型を持つ必要があります。

ちなみに、
string & number型のようなありえない型もnever型と判断されます。

引数がIntersection型で表現される関数型のUnion型

引数の型が現実的なIntersection型で例を見てみます。

この例ではfuncは
HogeFunc | PiyoFunc型です。

            
            interface Hoge {
    foo: string;
    bar: number;
}

interface Piyo {
    foo: string;
    baz: boolean;
}

type HogeFunc = (arg: Hoge) => number;
type PiyoFunc = (arg: Piyo) => boolean;

declare const func: HogeFunc | PiyoFunc;

//resは number | boolean 型
const res = func({
    foo: 'foo',
    bar: 123,
    baz: false,
});
        

HogeFunc | PiyoFunc型の関数の引数型は、それぞれの関数の引数の型のIntersection型・Hoge & Piyoを矛盾なく定義できるので、ここでのfuncを呼ぶことができます。

なお、resの型はnumber | boolean型になっているのも注目です。

これは、funcの型がHogeFuncの場合は返り値がnumberであり、PiyoFuncの場合は返り値がbooleanであることから説明できます。

Union型にかかる機能制限

関数型のUnion型は処理の扱いが煩雑なためか、現在のところ制限があります。

具体的には関数のオーバーロードがある場合やジェネリクスが関わる場合に関数が呼べなかったり、引数の型が推論できなかったりする場合があります。

            
            const arr: string[] | number[] = [];

//✘ Parameter 'x' implicitly has an 'any' type.
arr.forEach(x => console.log(x));
//✘ Cannot invoke an expression whose type lacks a call signature.
const arr2 = arr.map(x => x);
        

Object型

存在感の薄い準プリミティブ型に
object型があり、「プリミティブ以外の値の型」を意味します。

JavaScriptでのオブジェクトのみを引数に受け取る関数などを表現するための互換性を保つための型です。

例えば、組み込みのObject.createメソッドは、その引数としてオブジェクトまたはnullのみを受け取る関数です。

            
            //✘ Argument of type '3' is not assignable to parameter of type 'object | null'.
Object.create(3);
        

{}型

たまに目にする
{}という型は、見た目通り、「何もプロパティがないオブジェクト型」です。

プロパティが無いといっても、
{foo: string}のような構造的部分型と見なせるオブジェクト型の値も、{}型として扱うことができます。

            
            const obj = { foo: 'foo' };
const obj2: {} = obj;
        
ということは、任意のオブジェクト型を、{}型と表現してもよい気がしてきます。

でも実際には、{}型は、オブジェクト型以外(undefinedとnullは除外)も受け付けてしまうので、「すべてのオブジェクト型を部分型に持つ型」としては使えません。

例えば、以下のように、プリミティブ型に対してもプロパティアクセスができてしまいます。

            
            const o: {} = 3;
        
プリミティブ型を除外できないのは、型システムとしては欠陥となります。

例えば、プリミティブ型のひとつであるstring型に対して、
length: number;というプロパティを持つ、{}型の部分型・Lengthを使うと、そのままstring型のlengthプロパティから文字列の長さを取得できます。

            
            interface Length {
    length: number;
}

const o: Length = 'foobar';
        
このことから、{}というのはundefinedとnull以外は何でも受け入れてしまうようなとても「弱い型」であると解釈できます。

weak typeルール

「弱い型」は、
{}型でも触れたように、どのようなオブジェクト型のプロパティでも受け取ってしまう、緩い型を指して使います。

{}型もさることながら、オプショナルなプロパティしかない型も同様に弱い型となります。

困ったことに、そのような型は関数の引数で、オプションを保持したオブジェクト型として良く利用されるため、「weak type」という位置づけで例外的な処理をとります。

            
            interface Options {
    foo?: string;
    bar?: number;
}

const obj1 = { hoge: 3 };

//✘ Type '{ hoge: number; }' has no properties in common with type 'Options'
const obj2: Options = obj1;

//✘ Type '5' has no properties in common with type 'Options'.
const obj3: Options = 5;
        
Options型のプロパティはすべてオプショナルなので{}型と同等と考えれば、{ hoge: number; }型も部分型なので、Options型として扱えそうですが、obj2はエラーになります。

obj3も
{}型ならプリミティブ型がそのまま入りそうですが、エラーになっています。

「weak typeのルール」は、weak typeが持つプロパティを1つ以上持った型である必要があります。

実際、弱い型はこういったルールで制限しなければ、
{}型と区別がつかないということで理由があるようです。

ただし
{}型はweak typeの例外として、ここでのOptions型の値として扱えます。

また、「weak typeのルール」はオブジェクト型ではない型も弾いてくれます。

unknown型

先程までの話から、
{}型がもっとも弱い型である、と思われたかもしれませんが、後付で導入された「unknown型」が、実質もっとも弱い型として登場しました。

unknown型は、どのような型としても扱うことができる、部分型関係の最上位にある型になります。

この意味ではちょうどnever型の真逆に位置した型です。

            
            const u1: unknown = 3;
const u2: unknown = null;
const u3: unknown = (foo: string)=> true;
        

unknown型とany型の違い

「どんな型でも入る」と聞くと、unknown型はany型と同じように思えます。

実際には、unknown型はany型とは異なり、型安全に扱うことができます。

というのは、unknown型の値はどんな値なのか分からないため、できることが制限されます。

            
            const u: unknown = 3;
//✘ Object is of type 'unknown'.
const sum = u + 5;
//✘ Object is of type 'unknown'.
const p = u.prop;
        
上の例では、unknown型では、数値の足し算もプロパティアクセスも不可です。

any型の場合には、エラーが出ようがお構いなしの操作だったものが、unknown型では出来なくなっていることが大きく異なります。

unknown型の絞り込み

unknown型の型絞り込みは、前述したUnion型と同様の絞り込み方法で可能です。

            
            const u: unknown = 3;

if (typeof u === 'number') {
    // この中ではuはnumber型
    const foo = u + 5;
}
        
また、クラスのオブジェクト型と、instanceof演算子を組み合わせることによる絞り込みもできます。

            
            const u: unknown = 3;

class MyClass {
    public prop: number = 10;
}

if (u instanceof MyClass) {
    //uはMyClass型
    u.prop;
}
        

unknown型とvoid型の意外な関係

unknown型とvoid型は共通する部分が多くあります。

次の例は、一見するとエラーが起こりそうですが、実際は正常に処理されます。

            
            const func: ()=>number = () => 123;

const f: ()=>void = func;
        
ここでのfuncは()=>number型で、数値型の値を返す関数型です。

その型を
()=>void型という関数型で扱っても良いということを示しています。

前述のように、void型は
const v: void = 1;のように直接宣言することはできない独自の構文ルールはあるものの、部分型関係においてはunknown型と同様の振る舞いをします。

void型の関数の返り値は、返り値に興味がないし、何が入っていても構わない、という意味でunknown型相当の返り値になります。

例えば、以下を実行すると、void型の変数valueの中身は123になります。

            
            const func: ()=>number = () => 123;

const f: ()=>void = func;

const value: void = f();

//123
console.log(value);
        
このことで、void型が必ずしもundefined型である必要すらなく、むしろなんでもありのunknown型に親しい型だと分かります。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

まとめ

当初簡単にまとめるつもりが、思い出したいテクニックも多くあり、基礎的な内容を拾っていっても、結構な長丁場になってしましました。

今回は「型の基本」だけをまとめた話でしたが、次回以降の記事Typescriptの実用的なテクニックにもう少し内容を広げていく予定です。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き