TypeScript に取り組む
本書をサポートしてください:購入する または 寄付する
(広告、ブロックしないでください。)

7章 TypeScript の要点



この章では、TypeScript の要点について説明します。

7.1章 学習内容

この章を読んだ後、次の TypeScript コードを理解できるようになります。

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U
  ): U;
  // ···
}

これは難解だと感じるかもしれません。私も同意見です!しかし(証明できると期待していますが)、この構文は比較的簡単に習得できます。そして、一度理解すれば、英語で長い説明を読む必要がなく、コードの動作についての即時的で正確かつ包括的な要約が得られます。

7.2章 型チェックの包括性の指定

TypeScript コンパイラには、多くの構成方法があります。重要なオプションのグループの 1 つは、コンパイラが TypeScript コードをどの程度徹底的にチェックするかを制御します。最大の設定は--strictで有効になり、常に使用することをお勧めします。プログラムの作成は少し難しくなりますが、静的型チェックのメリットを最大限に享受できます。

  今のところ、--strict について知っておくべきことは以上です

詳細を知りたい場合は、読み進めてください。

--stricttrueに設定すると、次のすべてのオプションがtrueに設定されます。

この書籍では、npm パッケージウェブアプリを TypeScript で作成する際に、さらにコンパイラオプションについて説明します。TypeScript ハンドブックには、それらに関する包括的なドキュメントがあります。

7.3章 TypeScript の型

この章では、型は単なる値の集合です。JavaScript 言語(TypeScript ではありません!)には、8 つの型しかありません。

  1. Undefined:唯一の要素undefinedを持つ集合
  2. Null:唯一の要素nullを持つ集合
  3. Boolean:2 つの要素falsetrueを持つ集合
  4. Number:すべての数値の集合
  5. BigInt:すべての任意精度整数集合
  6. String:すべての文字列の集合
  7. Symbol:すべてのシンボルの集合
  8. Object:すべてのオブジェクト(関数と配列を含む)の集合

これらの型はすべて動的です。実行時に使用できます。

TypeScript は JavaScript に追加のレイヤーをもたらします:静的型。これらは、ソースコードのコンパイル時または型チェック時のみ存在します。各格納場所(変数、プロパティなど)には、動的値を予測する静的型があります。型チェックは、これらの予測が実現することを保証します。

そして、(コードを実行せずに)静的にチェックできるものがたくさんあります。たとえば、関数toString(num)のパラメータnumの静的型がnumberの場合、引数'abc'の静的型が間違っているため、関数呼び出しtoString('abc')は無効です。

7.4章 型注釈

function toString(num: number): string {
  return String(num);
}

前の関数宣言には、2 つの型注釈があります。

numberstringはどちらも、格納場所の型を指定する型式です。

7.5章 型推論

多くの場合、型注釈がない場合、TypeScript は静的型を推論できます。たとえば、toString()の戻り値の型を省略した場合、TypeScript はそれがstringであると推論します。

// %inferred-type: (num: number) => string
function toString(num: number) {
  return String(num);
}

型推論は推測ではありません。明示的に指定されていない型の導出には、明確なルール(算術と同様)に従います。この場合、戻り文は、任意の値を文字列にマッピングする関数String()を、型numberの値numに適用し、結果を返します。そのため、推論された戻り値の型はstringです。

場所の型が明示的に指定されておらず、推論もできない場合、TypeScript はその型にanyを使用します。これはすべての値の型であり、ワイルドカードです。つまり、値がその型を持っている場合、すべてを行うことができます。

--strictでは、anyは明示的に使用する場合のみ許可されます。言い換えると:すべての場所に、明示的または推論された静的型が必要です。次の例では、パラメータnumにはどちらもなく、コンパイル時エラーが発生します。

// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
  return String(num);
}

7.6章 型式による型の指定

型注釈のコロンの後の型式は、単純なものから複雑なものまでさまざまで、次のように作成されます。

基本型は有効な型式です。

基本型を組み合わせて新しい複合型を作成する方法はたくさんあります。たとえば、集合演算子和集合)と積集合)が集合を組み合わせるのと同様に、型を組み合わせる型演算子を使用します。その方法をすぐに見ていきます。

7.7章 2 つの言語レベル:動的 vs. 静的

TypeScript には 2 つの言語レベルがあります。

構文でこれらの 2 つのレベルを確認できます。

const undef: undefined = undefined;

同じ構文undefinedは、動的レベルで使用されるか静的レベルで使用されるかによって、異なる意味を持つことに注意してください。

  2 つの言語レベルを意識するようにしてください

これは、TypeScript を理解する上で大いに役立ちます。

7.8章 型エイリアス

typeを使用して、既存の型に新しい名前(エイリアス)を作成できます。

type Age = number;
const age: Age = 82;

7.9章 配列の型付け

配列は JavaScript で 2 つの役割を果たします(どちらか一方または両方)。

7.9.1章 リストとしての配列

配列arrが、すべての要素が数値であるリストとして使用されていることを表現する方法は 2 つあります。

let arr1: number[] = [];
let arr2: Array<number> = [];

通常、TypeScript は代入がある場合、変数の型を推論できます。この場合、空の配列では要素の型を決定できないため、実際には助ける必要があります。

角括弧表記(Array<number>)については、後で詳しく説明します。

7.9.2章 タプルとしての配列

2 次元点を配列に格納する場合は、その配列をタプルとして使用しています。次のようになります。

let point: [number, number] = [7, 5];

配列リテラルの場合、TypeScript はタプル型ではなくリスト型を推論するため、配列としてのタプルには型注釈が必要です。

// %inferred-type: number[]
let point = [7, 5];

タプルのもう 1 つの例は、Object.entries(obj)の結果です。objのプロパティごとに 1 つの [キー、値] ペアを持つ配列です。

// %inferred-type: [string, number][]
const entries = Object.entries({ a: 1, b: 2 });

assert.deepEqual(
  entries,
  [[ 'a', 1 ], [ 'b', 2 ]]);

推論された型は、タプルの配列です。

7.10章 関数型

これは関数型の例です。

(num: number) => string

この型は、型 number の単一パラメータを受け取り、文字列を返すすべての関数を網羅しています。この型を型注釈で使用してみましょう。

const toString: (num: number) => string = // (A)
  (num: number) => String(num); // (B)

通常、関数の場合はパラメータ型を指定する必要があります。しかし、この場合、B 行のnumの型は A 行の関数型から推論できるため、省略できます。

const toString: (num: number) => string =
  (num) => String(num);

toStringの型注釈を省略した場合、TypeScript はアロー関数から型を推論します。

// %inferred-type: (num: number) => string
const toString = (num: number) => String(num);

今回は、numに型注釈が必要です。

7.10.1章 より複雑な例

次の例はさらに複雑です。

function stringify123(callback: (num: number) => string) {
  return callback(123);
}

stringify123()callbackパラメータを記述するために関数型を使用しています。この型注釈により、TypeScript は次の関数呼び出しを拒否します。

// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
//   Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);

しかし、この関数呼び出しは受け入れます。

assert.equal(
  stringify123(String), '123');

7.10.2章 関数宣言の戻り値の型

TypeScript は通常、関数の戻り値の型を推論できますが、明示的に指定することもでき、場合によっては役立ちます(少なくとも害はありません)。

stringify123()の場合、戻り値の型を指定するのはオプションであり、次のようになります。

function stringify123(callback: (num: number) => string): string {
  return callback(123);
}
7.10.2.1 特殊な戻り値型 `void`

`void` は関数の特殊な戻り値型です。TypeScript に対し、その関数は常に `undefined` を返すことを示します。

明示的にそうする場合もあります。

function f1(): void {
  return undefined;
}

暗黙的にそうする場合もあります。

function f2(): void {}

しかし、そのような関数は `undefined` 以外の値を明示的に返すことはできません。

function f3(): void {
  // @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
  return 'abc';
}

7.10.3 オプションパラメータ

識別子の後に続く疑問符は、そのパラメータがオプションであることを意味します。例えば

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

TypeScript は、`callback` が `undefined` でないことを確認した場合にのみ、A 行の関数呼び出しを許可します(パラメータが省略された場合は `undefined` です)。

7.10.3.1 パラメータのデフォルト値

TypeScript はパラメータのデフォルト値をサポートしています。

function createPoint(x=0, y=0): [number, number] {
  return [x, y];
}

assert.deepEqual(
  createPoint(),
  [0, 0]);
assert.deepEqual(
  createPoint(1, 2),
  [1, 2]);

デフォルト値により、パラメータはオプションになります。TypeScript は型を推論できるため、通常は型アノテーションを省略できます。例えば、`x` と `y` の両方が `number` 型であると推論できます。

型アノテーションを追加する場合は、次のようになります。

function createPoint(x:number = 0, y:number = 0): [number, number] {
  return [x, y];
}

7.10.4 レストパラメータ

TypeScript のパラメータ定義ではレストパラメータも使用できます。その静的型は配列(リストまたはタプル)でなければなりません。

function joinNumbers(...nums: number[]): string {
  return nums.join('-');
}
assert.equal(
  joinNumbers(1, 2, 3),
  '1-2-3');

7.11 ユニオン型

変数に保持される値(一度に1つの値)は、異なる型のメンバーである場合があります。その場合、ユニオン型が必要です。例えば、次のコードでは、`stringOrNumber` は `string` 型または `number` 型のいずれかです。

function getScore(stringOrNumber: string|number): number {
  if (typeof stringOrNumber === 'string'
    && /^\*{1,5}$/.test(stringOrNumber)) {
      return stringOrNumber.length;
  } else if (typeof stringOrNumber === 'number'
    && stringOrNumber >= 1 && stringOrNumber <= 5) {
    return stringOrNumber
  } else {
    throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
  }
}

assert.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3);

`stringOrNumber` の型は `string|number` です。型式 `s|t` の結果は、型 `s` と `t` の集合論的和集合です(集合として解釈されます)。

7.11.1 デフォルトでは、`undefined` と `null` は型に含まれません

多くのプログラミング言語では、`null` はすべてのオブジェクト型の一部です。例えば、Java で変数の型が `String` の場合、`null` に設定しても Java はエラーを報告しません。

逆に、TypeScript では、`undefined` と `null` は別個の非交差型として扱われます。許可する必要がある場合は、`undefined|string` や `null|string` などのユニオン型が必要です。

let maybeNumber: null|number = null;
maybeNumber = 123;

そうでない場合、エラーが発生します。

// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
let maybeNumber: number = null;
maybeNumber = 123;

(変数を初期化前に読み取らない限り) TypeScript は即時の初期化を強制しません。

let myNumber: number; // OK
myNumber = 123;

7.11.2 省略を明示的にする

以前のこの関数を思い出してください。

function stringify123(callback?: (num: number) => string) {
  if (callback === undefined) {
    callback = String;
  }
  return callback(123); // (A)
}

`stringify123()` を書き直し、パラメータ `callback` をオプションではなくします。呼び出し側が関数を提供したくない場合は、`null` を明示的に渡す必要があります。結果は次のようになります。

function stringify123(
  callback: null | ((num: number) => string)) {
  const num = 123;
  if (callback === null) { // (A)
    callback = String;
  }
  return callback(num); // (B)
}

assert.equal(
  stringify123(null),
  '123');

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
assert.throws(() => stringify123());

再び、B 行で関数呼び出しを行う前に、`callback` が関数でない場合(A 行)を処理する必要があります。そうしなかった場合、TypeScript はその行でエラーを報告します。

7.12 オプション vs. デフォルト値 vs. `undefined|T`

次の3つのパラメータ宣言は非常に似ています。

パラメータがオプションの場合、省略できます。その場合、値は `undefined` になります。

function f1(x?: number) { return x }

assert.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit

パラメータにデフォルト値がある場合、パラメータが省略されているか `undefined` に設定されている場合、そのデフォルト値が使用されます。

function f2(x = 456) { return x }

assert.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit

パラメータにユニオン型がある場合、省略できませんが、`undefined` に設定できます。

function f3(x: undefined | number) { return x }

assert.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit

7.13 オブジェクトの型付け

配列と同様に、オブジェクトは JavaScript で2つの役割を果たします(場合によっては混在します)。

この章では、辞書としてのオブジェクトは無視します。これは§15.4.5「インデックスシグネチャ: 辞書としてのオブジェクト」で扱われています。ちなみに、辞書には通常、Map の方が適しています。

7.13.1 インターフェースによるレコードとしてのオブジェクトの型付け

インターフェースはレコードとしてのオブジェクトを記述します。例えば

interface Point {
  x: number;
  y: number;
}

コンマでメンバーを区切ることができます。

interface Point {
  x: number,
  y: number,
}

7.13.2 TypeScript の構造的型付け vs. 名義的型付け

TypeScript の型システムの大きな利点の1つは、名義的ではなく構造的に機能することです。つまり、インターフェース `Point` は、適切な構造を持つすべてのオブジェクトに一致します。

interface Point {
  x: number;
  y: number;
}
function pointToString(pt: Point) {
  return `(${pt.x}, ${pt.y})`;
}

assert.equal(
  pointToString({x: 5, y: 7}), // compatible structure
  '(5, 7)');

逆に、Java の名義的型システムでは、各クラスで実装するインターフェースを明示的に宣言する必要があります。そのため、クラスは作成時に存在するインターフェースのみ実装できます。

7.13.3 オブジェクトリテラル型

オブジェクトリテラル型は匿名インターフェースです。

type Point = {
  x: number;
  y: number;
};

オブジェクトリテラル型の利点の1つは、インラインで使用できることです。

function pointToString(pt: {x: number, y: number}) {
  return `(${pt.x}, ${pt.y})`;
}

7.13.4 オプションのプロパティ

プロパティを省略できる場合は、名前の後に疑問符を付けます。

interface Person {
  name: string;
  company?: string;
}

次の例では、`john` と `jane` の両方がインターフェース `Person` に一致します。

const john: Person = {
  name: 'John',
};
const jane: Person = {
  name: 'Jane',
  company: 'Massive Dynamic',
};

7.13.5 メソッド

インターフェースにはメソッドを含めることもできます。

interface Point {
  x: number;
  y: number;
  distance(other: Point): number;
}

TypeScript の型システムに関しては、メソッド定義と値が関数であるプロパティは同等です。

interface HasMethodDef {
  simpleMethod(flag: boolean): void;
}
interface HasFuncProp {
  simpleMethod: (flag: boolean) => void;
}

const objWithMethod: HasMethodDef = {
  simpleMethod(flag: boolean): void {},
};
const objWithMethod2: HasFuncProp = objWithMethod;

const objWithOrdinaryFunction: HasMethodDef = {
  simpleMethod: function (flag: boolean): void {},
};
const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction;

const objWithArrowFunction: HasMethodDef = {
  simpleMethod: (flag: boolean): void => {},
};
const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;

プロパティの設定方法を最も適切に表現する構文を使用することをお勧めします。

7.14 型変数とジェネリック型

TypeScript の2つの言語レベルを思い出してください。

同様に

  型パラメータの命名

TypeScript では、型パラメータには大文字の単一文字(`T`、`I`、`O` など)を使用するのが一般的です。ただし、有効な JavaScript 識別子であれば何でも使用でき、長い名前の方がコードの理解が容易になることがよくあります。

7.14.1 例: 値のコンテナ

// Factory for types
interface ValueContainer<Value> {
  value: Value;
}

// Creating one type
type StringContainer = ValueContainer<string>;

`Value` は型変数です。山括弧で1つ以上の型変数を導入できます。

7.15 例: ジェネリッククラス

クラスにも型パラメータを持たせることができます。

class SimpleStack<Elem> {
  #data: Array<Elem> = [];
  push(x: Elem): void {
    this.#data.push(x);
  }
  pop(): Elem {
    const result = this.#data.pop();
    if (result === undefined) {
        throw new Error();
    }
    return result;
  }
  get length() {
    return this.#data.length;
  }
}

クラス `SimpleStack` には型パラメータ `Elem` があります。クラスをインスタンス化するとき、型パラメータの値も提供します。

const stringStack = new SimpleStack<string>();
stringStack.push('first');
stringStack.push('second');
assert.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second');

7.15.1 例: Map

Map は TypeScript でジェネリックに型付けされています。例えば

const myMap: Map<boolean,string> = new Map([
  [false, 'no'],
  [true, 'yes'],
]);

(`new Map()` の引数に基づく) 型推論のおかげで、型パラメータを省略できます。

// %inferred-type: Map<boolean, string>
const myMap = new Map([
  [false, 'no'],
  [true, 'yes'],
]);

7.15.2 関数とメソッドの型変数

関数定義では、次のように型変数を導入できます。

function identity<Arg>(arg: Arg): Arg {
  return arg;
}

関数は次のように使用します。

// %inferred-type: number
const num1 = identity<number>(123);

型推論により、型パラメータを再び省略できます。

// %inferred-type: 123
const num2 = identity(123);

TypeScript が `123` という型を推論したことに注意してください。これは、1つの数値を持つ集合であり、`number` 型よりも具体的な型です。

7.15.2.1 アロー関数とメソッド

アロー関数にも型パラメータを持たせることができます。

const identity = <Arg>(arg: Arg): Arg => arg;

これはメソッドの型パラメータ構文です。

const obj = {
  identity<Arg>(arg: Arg): Arg {
    return arg;
  },
};

7.15.3 より複雑な関数例

function fillArray<T>(len: number, elem: T): T[] {
  return new Array<T>(len).fill(elem);
}

このコードでは、型変数 `T` が4回出現します。

TypeScript がパラメータ `elem` から `T` を推論できるため、`fillArray()` を呼び出すとき(A 行)には型パラメータを省略できます。

// %inferred-type: string[]
const arr1 = fillArray<string>(3, '*');
assert.deepEqual(
  arr1, ['*', '*', '*']);

// %inferred-type: string[]
const arr2 = fillArray(3, '*'); // (A)

7.16 まとめ: 最初の例を理解する

学んだことを使って、前に見たコードを理解しましょう。

interface Array<T> {
  concat(...items: Array<T[] | T>): T[];
  reduce<U>(
    callback: (state: U, element: T, index: number, array: T[]) => U,
    firstState?: U
  ): U;
  // ···
}

これは、要素の型が `T` である配列のインターフェースです。