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

20 関数型付け



本章では、TypeScriptにおける関数の静的型付けについて探ります。

  本章では、「関数」は「関数、メソッド、またはコンストラクタ」を意味します

本章では、関数について述べられていることのほとんど(特にパラメータ処理に関するもの)は、メソッドとコンストラクタにも適用されます。

20.1 静的型付けされた関数の定義

20.1.1 関数宣言

これはTypeScriptにおける関数宣言の例です。

function repeat1(str: string, times: number): string { // (A)
  return str.repeat(times);
}
assert.equal(
  repeat1('*', 5), '*****');

20.1.2 アロー関数

`repeat1()` のアロー関数バージョンは次のようになります。

const repeat2 = (str: string, times: number): string => {
  return str.repeat(times);
};

この場合、式ボディも使用できます。

const repeat3 = (str: string, times: number): string =>
  str.repeat(times);

20.2 関数の型

20.2.1 関数型シグネチャ

関数型シグネチャを使用して、関数の型を定義できます。

type Repeat = (str: string, times: number) => string;

この関数型の名前は `Repeat` です。その他にも、次の条件を満たすすべての関数と一致します。

この型は、より多くの関数と一致します。本章の後の方で代入可能性のルールを調べるときに、どの関数と一致するのかを学びます。

20.2.2 コールシグネチャを持つインターフェース

インターフェースを使用して関数型を定義することもできます。

interface Repeat {
  (str: string, times: number): string; // (A)
}

注意

一方では、インターフェースは冗長です。他方では、関数のプロパティを指定できます(まれですが、発生します)。

interface Incrementor1 {
  (x: number): number;
  increment: number;
}

関数シグネチャ型とオブジェクトリテラル型の交差型(`&`)を使用して、プロパティを指定することもできます。

type Incrementor2 =
  (x: number) => number
  & { increment: number }
;

20.2.3 呼び出し可能な値が関数型と一致するかどうかをチェックする

例として、ライブラリが次の関数型をエクスポートするシナリオを考えてみましょう。

type StringPredicate = (str: string) => boolean;

型が `StringPredicate` と互換性のある関数を定義し、それが実際にそうであるかどうかをすぐに確認したいと考えています(初めて使用するときに後でわかるのではなく)。

20.2.3.1 アロー関数のチェック

`const` を使用して変数を宣言する場合、型注釈を使用してチェックを実行できます。

const pred1: StringPredicate = (str) => str.length > 0;

TypeScriptは`StringPredicate`を使用して`str`のパラメータの型を推論できるため、パラメータ`str`の型を指定する必要はありません。

20.2.3.2 関数宣言のチェック(簡単)

関数宣言のチェックはより複雑です。

function pred2(str: string): boolean {
  return str.length > 0;
}

// Assign the function to a type-annotated variable
const pred2ImplementsStringPredicate: StringPredicate = pred2;
20.2.3.3 関数宣言のチェック(高度)

次の解決策はやや高度ですが(完全に理解できなくても心配しないでください)、いくつかの高度な機能を示しています。

function pred3(...[str]: Parameters<StringPredicate>)
  : ReturnType<StringPredicate> {
    return str.length > 0;
  }

20.3 パラメータ

20.3.1 パラメータに型注釈が必要な場合

復習: `--noImplicitAny` がオンの場合(`--strict` はそれをオンにします)、各パラメータの型は推論可能であるか、または明示的に指定する必要があります。

次の例では、TypeScriptは `str` の型を推論できないため、それを指定する必要があります。

function twice(str: string) {
  return str + str;
}

A行では、TypeScriptは`StringMapFunction`型を使用して`str`の型を推論できるため、型注釈を追加する必要はありません。

type StringMapFunction = (str: string) => string;
const twice: StringMapFunction = (str) => str + str; // (A)

ここでは、TypeScriptは `.map()` の型を使用して `str` の型を推論できます。

assert.deepEqual(
  ['a', 'b', 'c'].map((str) => str + str),
  ['aa', 'bb', 'cc']);

これは `.map()` の型です。

interface Array<T> {
  map<U>(
    callbackfn: (value: T, index: number, array: T[]) => U,
    thisArg?: any
  ): U[];
  // ···
}

20.3.2 オプションパラメータ

このセクションでは、パラメータの省略を許可するいくつかの方法を見ていきます。

20.3.2.1 オプションパラメータ: `str?: string`

パラメータ名の後に疑問符を付けると、そのパラメータはオプションになり、関数を呼び出す際に省略できます。

function trim1(str?: string): string {
  // Internal type of str:
  // %inferred-type: string | undefined
  str;

  if (str === undefined) {
    return '';
  }
  return str.trim();
}

// External type of trim1:
// %inferred-type: (str?: string | undefined) => string
trim1;

`trim1()` は次のように呼び出すことができます。

assert.equal(
  trim1('\n  abc \t'), 'abc');

assert.equal(
  trim1(), '');

// `undefined` is equivalent to omitting the parameter
assert.equal(
  trim1(undefined), '');
20.3.2.2 ユニオン型: `str: undefined|string`

`trim1()` のパラメータ `str` は外部的には `string|undefined` 型です。したがって、`trim1()` は次の関数とほぼ同等です。

function trim2(str: undefined|string): string {
  // Internal type of str:
  // %inferred-type: string | undefined
  str;

  if (str === undefined) {
    return '';
  }
  return str.trim();
}

// External type of trim2:
// %inferred-type: (str: string | undefined) => string
trim2;

`trim2()` と `trim1()` の異なる点は、関数呼び出しでパラメータを省略できない点だけです(A行)。つまり、型が `undefined|T` のパラメータを省略する場合は、明示的にする必要があります。

assert.equal(
  trim2('\n  abc \t'), 'abc');

// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
trim2(); // (A)

assert.equal(
  trim2(undefined), ''); // OK!
20.3.2.3 パラメータのデフォルト値: `str = ''`

`str` にパラメータのデフォルト値を指定する場合、TypeScriptが型を推論できるため、型注釈を追加する必要はありません。

function trim3(str = ''): string {
  // Internal type of str:
  // %inferred-type: string
  str;

  return str.trim();
}

// External type of trim2:
// %inferred-type: (str?: string) => string
trim3;

デフォルト値により `undefined` になることがないため、`str` の内部型は `string` です。

`trim3()` を呼び出してみましょう。

assert.equal(
  trim3('\n  abc \t'), 'abc');

// Omitting the parameter triggers the parameter default value:
assert.equal(
  trim3(), '');

// `undefined` is allowed and triggers the parameter default value:
assert.equal(
  trim3(undefined), '');
20.3.2.4 パラメータのデフォルト値と型注釈

型とデフォルト値の両方を指定することもできます。

function trim4(str: string = ''): string {
  return str.trim();
}

20.3.3 レストパラメータ

20.3.3.1 配列型を持つレストパラメータ

レストパラメータは残りのすべての引数を配列に収集します。したがって、その静的型は通常配列です。次の例では、`parts` はレストパラメータです。

function join(separator: string, ...parts: string[]) {
  return parts.join(separator);
}
assert.equal(
  join('-', 'state', 'of', 'the', 'art'),
  'state-of-the-art');
20.3.3.2 タプル型を持つレストパラメータ

次の例は2つの機能を示しています。

function repeat1(...[str, times]: [string, number]): string {
  return str.repeat(times);
}

`repeat1()` は次の関数と同等です。

function repeat2(str: string, times: number): string {
  return str.repeat(times);
}

20.3.4 名前付きパラメータ

名前付きパラメータ は、オブジェクトリテラルを使用して各パラメータに名前を付ける一般的なJavaScriptパターンです。次のようになります。

assert.equal(
  padStart({str: '7', len: 3, fillStr: '0'}),
  '007');

プレーンなJavaScriptでは、関数はデストラクチャリングを使用して名前付きパラメータの値にアクセスできます。しかし、TypeScriptでは、オブジェクトリテラルの型も指定する必要があり、冗長になります。

function padStart({ str, len, fillStr = ' ' } // (A)
  : { str: string, len: number, fillStr: string }) { // (B)
  return str.padStart(len, fillStr);
}

デストラクチャリング(`fillStr` のデフォルト値を含む)はすべてA行で行われ、B行はTypeScript専用です。

インラインオブジェクトリテラル型の代わりに、別の型を定義することもできますが、ほとんどの場合、パラメータは関数ごとにローカルで一意であるため、それを行うのは避けています。関数ヘッダーにあまり多くのものを書きたくない場合は、それでも問題ありません。

20.3.5 this をパラメータとして使う (上級)

通常の関数には常に暗黙のパラメータ `this` があり、オブジェクトのメソッドとして使用できます。場合によっては、`this` の型を指定する必要があります。このユースケースにはTypeScript専用の構文があります。通常の関数の1つのパラメータに `this` という名前を付けることができます。このようなパラメータはコンパイル時のみ存在し、実行時には消えます。

例として、DOMイベントソースのインターフェース(やや簡略化されたバージョン)を考えてみましょう。

interface EventSource {
  addEventListener(
    type: string,
    listener: (this: EventSource, ev: Event) => any,
    options?: boolean | AddEventListenerOptions
  ): void;
  // ···
}

コールバック `listener` の `this` は常に `EventSource` のインスタンスです。

次の例は、TypeScriptが `this` パラメータによって提供される型情報を使用して `.call()` の最初の引数をチェックする方法を示しています(A行とB行)。

function toIsoString(this: Date): string {
    return this.toISOString();
}

// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'Date'. (2345)
assert.throws(() => toIsoString.call('abc')); // (A) error

toIsoString.call(new Date()); // (B) OK

さらに、オブジェクト `obj` のメソッドとして `toIsoString()` を呼び出すことはできません。なぜなら、そのレシーバが `Date` のインスタンスではないからです。

const obj = { toIsoString };
// @ts-expect-error: The 'this' context of type
// '{ toIsoString: (this: Date) => string; }' is not assignable to
// method's 'this' of type 'Date'. [...]
assert.throws(() => obj.toIsoString()); // error
obj.toIsoString.call(new Date()); // OK

20.4 オーバーロード (上級)

単一の型シグネチャでは、関数の動作を十分に説明できない場合があります。

20.4.1 関数宣言のオーバーロード

次の例(A行とB行)で呼び出している関数 `getFullName()` を考えてみましょう。

interface Customer {
  id: string;
  fullName: string;
}
const jane = {id: '1234', fullName: 'Jane Bond'};
const lars = {id: '5678', fullName: 'Lars Croft'};
const idToCustomer = new Map<string, Customer>([
  ['1234', jane],
  ['5678', lars],
]);

assert.equal(
  getFullName(idToCustomer, '1234'), 'Jane Bond'); // (A)

assert.equal(
  getFullName(lars), 'Lars Croft'); // (B)

`getFullName()` はどのように実装しますか?次の実装は、前の例での2つの関数呼び出しに対して機能します。

function getFullName(
  customerOrMap: Customer | Map<string, Customer>,
  id?: string
): string {
  if (customerOrMap instanceof Map) {
    if (id === undefined) throw new Error();
    const customer = customerOrMap.get(id);
    if (customer === undefined) {
      throw new Error('Unknown ID: ' + id);
    }
    customerOrMap = customer;
  } else {
    if (id !== undefined) throw new Error();
  }
  return customerOrMap.fullName;
}

しかし、この型シグネチャでは、コンパイル時には有効だが、ランタイムエラーが発生する関数呼び出しが許可されます。

assert.throws(() => getFullName(idToCustomer)); // missing ID
assert.throws(() => getFullName(lars, '5678')); // ID not allowed

次のコードでは、これらの問題が修正されています。

function getFullName(customerOrMap: Customer): string; // (A)
function getFullName( // (B)
  customerOrMap: Map<string, Customer>, id: string): string;
function getFullName( // (C)
  customerOrMap: Customer | Map<string, Customer>,
  id?: string
): string {
  // ···
}

// @ts-expect-error: Argument of type 'Map<string, Customer>' is not
// assignable to parameter of type 'Customer'. [...]
getFullName(idToCustomer); // missing ID

// @ts-expect-error: Argument of type '{ id: string; fullName: string; }'
// is not assignable to parameter of type 'Map<string, Customer>'.
// [...]
getFullName(lars, '5678'); // ID not allowed

何が起こっているのでしょうか?`getFullName()` の型シグネチャはオーバーロードされています。

私のアドバイスは、オーバーロードが避けられない場合にのみオーバーロードを使用することです。1つの代替手段は、オーバーロードされた関数を異なる名前を持つ複数の関数に分割することです – 例えば

20.4.2 インターフェースによるオーバーロード

インターフェースでは、複数の異なる呼び出しシグネチャを持つことができます。これにより、次の例のように、インターフェースGetFullNameをオーバーロードに使用できます。

interface GetFullName {
  (customerOrMap: Customer): string;
  (customerOrMap: Map<string, Customer>, id: string): string;
}

const getFullName: GetFullName = (
  customerOrMap: Customer | Map<string, Customer>,
  id?: string
): string => {
  if (customerOrMap instanceof Map) {
    if (id === undefined) throw new Error();
    const customer = customerOrMap.get(id);
    if (customer === undefined) {
      throw new Error('Unknown ID: ' + id);
    }
    customerOrMap = customer;
  } else {
    if (id !== undefined) throw new Error();
  }
  return customerOrMap.fullName;
}

20.4.3 文字列パラメータのオーバーロード(イベント処理など)

次の例では、文字列リテラル型('click'など)を使用してオーバーロードします。これにより、パラメータtypeの値に応じて、パラメータlistenerの型を変更できます。

function addEventListener(elem: HTMLElement, type: 'click',
  listener: (event: MouseEvent) => void): void;
function addEventListener(elem: HTMLElement, type: 'keypress',
  listener: (event: KeyboardEvent) => void): void;
function addEventListener(elem: HTMLElement, type: string,  // (A)
  listener: (event: any) => void): void {
    elem.addEventListener(type, listener); // (B)
  }

この場合、(行Aから始まる)実装の型を正しく取得することは比較的困難であるため、本体のステートメント(行B)が機能します。最終手段として、常に型anyを使用できます。

20.4.4 メソッドのオーバーロード

20.4.4.1 具象メソッドのオーバーロード

次の例は、メソッドのオーバーロードを示しています。メソッド.add()はオーバーロードされています。

class StringBuilder {
  #data = '';

  add(num: number): this;
  add(bool: boolean): this;
  add(str: string): this;
  add(value: any): this {
    this.#data += String(value);
    return this;
  }

  toString() {
    return this.#data;
  }
}

const sb = new StringBuilder();
sb
  .add('I can see ')
  .add(3)
  .add(' monkeys!')
;
assert.equal(
  sb.toString(), 'I can see 3 monkeys!')
20.4.4.2 インターフェースメソッドのオーバーロード

Array.from()の型定義は、オーバーロードされたインターフェースメソッドの例です。

interface ArrayConstructor {
  from<T>(arrayLike: ArrayLike<T>): T[];
  from<T, U>(
    arrayLike: ArrayLike<T>,
    mapfn: (v: T, k: number) => U,
    thisArg?: any
  ): U[];
}

20.5 代入可能性(上級)

このセクションでは、代入可能性に関する型互換性ルールについて説明します。型Srcの関数を型Trgの記憶域(変数、オブジェクトのプロパティ、パラメータなど)に転送できますか?

代入可能性を理解することで、次のような質問に答えることができます。

20.5.1 代入可能性のルール

この小節では、代入可能性の一般的なルール(関数のルールを含む)を調べます。次の小節では、これらのルールが関数に対して何を意味するのかを調べます。

Srcは、次の条件のいずれかが真の場合、型Trg代入可能です。

20.5.2 関数の代入ルールによる結果

この小節では、代入ルールが次の2つの関数targetFuncsourceFuncに対して何を意味するのかを調べます。

const targetFunc: Trg = sourceFunc;
20.5.2.1 パラメータと結果の型

const trg1: (x: RegExp) => Object = (x: Object) => /abc/;

次の例は、ターゲットの戻り値型がvoidの場合、ソースの戻り値型は問題にならないことを示しています。なぜでしょうか?voidの結果はTypeScriptでは常に無視されます。

const trg2: () => void = () => new Date();
20.5.2.2 パラメータの数

ソースは、ターゲットよりも多くのパラメータを持つことはできません。

// @ts-expect-error: Type '(x: string) => string' is not assignable to
// type '() => string'. (2322)
const trg3: () => string = (x: string) => 'abc';

ソースは、ターゲットよりも少ないパラメータを持つことができます。

const trg4: (x: string) => string = () => 'abc';

なぜそうなのか?ターゲットはソースに対する期待値を指定しています。パラメータxを受け入れる必要があります。それは実行しますが(ただし、無視します)。この許容性により、

['a', 'b'].map(x => x + x)

.map()のコールバックは、.map()の型シグネチャで言及されている3つのパラメータのうち1つしかありません。

map<U>(
  callback: (value: T, index: number, array: T[]) => U,
  thisArg?: any
): U[];

20.6 この章のさらなる参考文献と情報源