Reincarnated as anyone

フロントエンド学習メモ

【学習メモ】 TypeScript の型入門 その1

はじめに

@uhyo_さんが書いた以下の記事を理解するために、原文を自分でも理解できる表現にリライトしたものになります。

TypeScript の型入門

この記事はどんな記事?

TypeScript(v3.7) の型について初歩から解説した記事。

  • プリミティブ型
  • リテラル
  • オブジェクト型
  • 配列型
  • 関数型
  • void 型
  • any 型
  • クラスの型

プリミティブ型

TypeScript において一番基本となる型。

これは JavaScript のプリミティブの型と対応しており、以下の型が存在する

  • string
  • number
  • boolean
  • symbol
  • bigint
  • null
  • undefined

これらは互いに異なる型であり、以下のように相互に代入はできない。

const a: number = 3;

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

bigint

bigint型は TypeScript 3.2 で導入されたので、それ以降の TypeScript でのみ利用可能。

また、BigInt のトランスパイルはできないため、targettsconfig.jsonなどに設定できるオプション)をesnext(または ES2019 が登場したらes2019またはそれ以降)にしないと利用できない。

strictNullChecksオプション

コンパイラオプションで--strictNullChecksをオンにしていない場合は、以下のようにnullundefinedは他の型の値として扱うことができる。

const a: null = null;

const b: string = a; // strictNullChecksがオンならエラー

逆に言うと、--strictNullChecksをオンにしないとundefinednullが紛れ込む可能性があり危険。

そのため、このオプションは常にオンにするべき。

リテラル

プリミティブ型を細分化した型。

リテラル型には、以下のリテテル型が存在する。

例えば、'foo'というリテラル型は、'foo'という文字列しか許されない型である。

同様に、0というというリテラル型は、0という数値しか許されない型になる

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

文字列のリテラル型はstringの部分型であり、文字列のリテラル型を持つ値は、以下のように string 型として扱うこともできる。

const a: "foo" = "foo";
const b: string = a;

他の型も同様。

const a: 1 = 1;
const b: number = a;

const c: true = true;
const d: boolean = c;

リテラル型と型推論

上記の例では変数に全て型注釈を付けていたが、以下のように型注釈を省略してもちゃんと推論される。

const a = "foo"; // a は'foo'型を持つ
const b: "bar" = a; // エラー: Type '"foo"' is not assignable to type '"bar"'.

変数 a は'foo'が代入されているので、a の型も'foo'型になる。

ただし、これはconstを使って変数を宣言した場合に限る。

letvarで変数が宣言する場合、推論される型はリテラル型ではなく対応するプリミティブ型になる。

let a = "foo"; // a は string 型に推論される
const b: string = a;
const c: "foo" = a; // エラー: Type 'string' is not assignable to type '"foo"'.

上記の例では、aletで宣言されているのでstring型と推論される。

そのため、'foo'型を持つcに代入することはできない。

なお、letで宣言する場合も型注釈をつければリテラル型を持たせることはできる。

let a: "foo" = "foo";
a = "bar"; // エラー: Type '"bar"' is not assignable to type '"foo"'.

オブジェクト型

{}の中にプロパティ名とその型を列挙するものがオブジェクト型。

例えば、以下の型はfooというプロパティがstring型の値を持ちbarというプロパティがnumber型の値を持つようなオブジェクトの型。

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

const a: MyObj = {
  foo: "foo",
  bar: 3
};

上記のinterfaceは、TypeScript 独自の構文であり、オブジェクト型に名前を付けることができる。

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

また、今回はconstに型注釈を付けているが、型注釈をしなくてもオブジェクト型を推論してくれる。

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

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

// bar に string 型を定義しているので、エラー
// エラー:
// Type '{ foo: string; bar: string; }' is not assignable to type 'MyObj'.
//  Types of property 'bar' are incompatible.
//    Type 'string' is not assignable to type 'number'.
const a: MyObj = {
  foo: "foo",
  bar: "BARBARBAR"
};

// bar プロパティがないのでエラー
// エラー:
// Type '{ foo: string; }' is not assignable to type 'MyObj'.
//  Property 'bar' is missing in type '{ foo: string; }'.
const b: MyObj = {
  foo: "foo"
};

このように、TypeScript を利用することで、オブジェクトは自由に書き換えることを制限できる。

また、TypeScript では構造的部分型を採用しているため、次のようなことが可能。

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

interface MyObj2 {
  foo: string;
}

const a: MyObj = { foo: "foo", bar: 3 };
// `MyObj`型の値は`string`型のプロパティ`foo`を持っているため、
// `MyObj2`型の値の要件を満たしていると見なされる。
// このような状態を、`MyObj`は`MyObj2`の部分型であると言う。
const b: MyObj2 = a;

オブジェクトリテラルの特殊な処理

上記のようにMyObj型のaMyObj2型のbに代入をする時はエラーは発生しない。

しかし、以下のように、オブジェクトリテラルを代入する場合、余計なプロパティを持つオブジェクトは弾かれる。

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

interface MyObj2 {
  foo: string;
}

// `bar`プロパティは`MyObj2`型には存在しないので、エラーになる。
// エラー:
// Type '{ foo: string; bar: number; }' is not assignable to type 'MyObj2'.
//  Object literal may only specify known properties, and 'bar' does not exist in type 'MyObj2'.
const b: MyObj2 = { foo: "foo", bar: 3 };

配列型

配列の型を表すためには[]を利用する。

例えば、number[]というのは数値の配列を表す。

const foo: number[] = [0, 1, 2, 3];
foo.push(4);

また、TypeScript にジェネリクスが導入されて以降はArray<number>と書くことも可能(ジェネリクスについては後述)。

関数型

関数型は以下のように表現できる。

// 第 1 引数として`string`型の、第 2 引数として`number`型の引数をとり、
// 返り値として`boolean`型の値を返す関数の型
(foo: string, bar: number) => boolean;

function 宣言などによって作られた関数にも、以下のような関数型が付く。

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

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

関数の部分型関係

関数型に対しては、普通の部分型関係がある。

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

interface MyObj2 {
  foo: string;
}

const a: (obj: MyObj2) => void = () => {};
// `(obj: MyObj2) => void`型の値を`(obj: MyObj) => void`型の値として扱うことが可能
// `MyObj`は`MyObj2`の部分型なので、`MyObj2`を受け取って処理できる関数は`MyObj`を
// 受け取っても当然処理できるだろうということを想定している。
// `a`と`b`の型を逆にするとエラーになる。
const b: (obj: MyObj) => void = a;

また、関数の場合、引数の数に関しても部分型関係が発生する。

const f1: (foo: string) => void = () => {};
// `(foo: string) => void`型の値を`(foo: string, bar: number) => void`型の値として使うことができる。
// すなわち、引数を1つだけ受け取る関数は、引数を2つ受け取る関数として使うことが可能であるということを想定している。
const f2: (foo: string, bar: number) => void = f1;

可変長引数

JavaScript には、以下のような可変長引数という機能がある。

const func = (foo, ...bar) => bar;

console.log(func(1, 2, 3)); // [2, 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 型を利用する。

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

// エラー: A function whose declared type is neither 'void' nor 'any' must return a value.
function bar(): undefined {
  console.log("world");
}

返り値が void 型である関数は値を返さなくてもよくなる。

逆に、それ以外の型の場合(any 型を除く)は必ず返り値を返さなければいけない。

any 型

何でもありな型。

TypeScript の型システムを無視するような型であり、TypeScrtipt を使う意味が薄れるので、やむを得ない場面でのみ利用する。

クラスの型

TypeScript では、クラスを定義すると同時に同名の型が定義される。

// `Foo`という、`Foo`クラスのインスタンスの型も同時に定義される
class Foo {
  method(): void {
    console.log("Hello, world!");
  }
}

const obj: Foo = new Foo();

obj: FooFooは型名のFooであり、new Foo()Fooはクラス(コンストラクタ)のFooである。

TypeScript はあくまで構造的型付けを採用しているので、クラスの構造が同じであれば別の型でも代替できる。

そのため、以下のように型Fooは次のようなオブジェクト型で代替可能。

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

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

// MyFoo も Foo と同じ method という関数型のプロパティを持つため、同じ型とみなす
const obj: MyFoo = new Foo();
const obj2: Foo = obj;