this
をパラメータとして使う (上級)本章では、TypeScriptにおける関数の静的型付けについて探ります。
本章では、「関数」は「関数、メソッド、またはコンストラクタ」を意味します
本章では、関数について述べられていることのほとんど(特にパラメータ処理に関するもの)は、メソッドとコンストラクタにも適用されます。
これはTypeScriptにおける関数宣言の例です。
function repeat1(str: string, times: number): string { // (A)
.repeat(times);
return str
}.equal(
assertrepeat1('*', 5), '*****');
パラメータ: コンパイラオプション `--noImplicitAny` がオンの場合(`--strict` がオンの場合もオンになります)、各パラメータの型は、推論可能であるか、または明示的に指定する必要があります。(推論については後で詳しく説明します。) この場合、推論は不可能なため、`str` と `times` に型注釈があります。
戻り値: 既定では、関数の戻り値の型は推論されます。通常はそれで十分です。この例では、`repeat1()` の戻り値の型が `string` であることを明示的に指定しました(A行の最後の型注釈)。
`repeat1()` のアロー関数バージョンは次のようになります。
= (str: string, times: number): string => {
const repeat2 .repeat(times);
return str; }
この場合、式ボディも使用できます。
= (str: string, times: number): string =>
const repeat3 .repeat(times); str
関数型シグネチャを使用して、関数の型を定義できます。
type Repeat = (str: string, times: number) => string;
この関数型の名前は `Repeat` です。その他にも、次の条件を満たすすべての関数と一致します。
この型は、より多くの関数と一致します。本章の後の方で代入可能性のルールを調べるときに、どの関数と一致するのかを学びます。
インターフェースを使用して関数型を定義することもできます。
interface Repeat {: string, times: number): string; // (A)
(str }
注意
一方では、インターフェースは冗長です。他方では、関数のプロパティを指定できます(まれですが、発生します)。
interface Incrementor1 {: number): number;
(x: number;
increment }
関数シグネチャ型とオブジェクトリテラル型の交差型(`&`)を使用して、プロパティを指定することもできます。
type Incrementor2 =
: number) => number
(x& { increment: number }
;
例として、ライブラリが次の関数型をエクスポートするシナリオを考えてみましょう。
type StringPredicate = (str: string) => boolean;
型が `StringPredicate` と互換性のある関数を定義し、それが実際にそうであるかどうかをすぐに確認したいと考えています(初めて使用するときに後でわかるのではなく)。
`const` を使用して変数を宣言する場合、型注釈を使用してチェックを実行できます。
: StringPredicate = (str) => str.length > 0; const pred1
TypeScriptは`StringPredicate`を使用して`str`のパラメータの型を推論できるため、パラメータ`str`の型を指定する必要はありません。
関数宣言のチェックはより複雑です。
function pred2(str: string): boolean {
.length > 0;
return str
}
// Assign the function to a type-annotated variable
: StringPredicate = pred2; const pred2ImplementsStringPredicate
次の解決策はやや高度ですが(完全に理解できなくても心配しないでください)、いくつかの高度な機能を示しています。
function pred3(...[str]: Parameters<StringPredicate>)
: ReturnType<StringPredicate> {
.length > 0;
return str }
パラメータ: `Parameters<>` を使用して、パラメータ型のタプルを抽出します。3つのドットはレストパラメータを宣言し、すべての引数をタプル/配列に収集します。`[str]` はそのタプルを分解します。(レストパラメータについては、本章の後半で詳しく説明します。)
戻り値: `ReturnType<>` を使用して戻り値の型を抽出します。
復習: `--noImplicitAny` がオンの場合(`--strict` はそれをオンにします)、各パラメータの型は推論可能であるか、または明示的に指定する必要があります。
次の例では、TypeScriptは `str` の型を推論できないため、それを指定する必要があります。
function twice(str: string) {
+ str;
return str }
A行では、TypeScriptは`StringMapFunction`型を使用して`str`の型を推論できるため、型注釈を追加する必要はありません。
type StringMapFunction = (str: string) => string;
: StringMapFunction = (str) => str + str; // (A) const twice
ここでは、TypeScriptは `.map()` の型を使用して `str` の型を推論できます。
.deepEqual(
assert'a', 'b', 'c'].map((str) => str + str),
['aa', 'bb', 'cc']); [
これは `.map()` の型です。
<T> {
interface Arraymap<U>(
: (value: T, index: number, array: T[]) => U,
callbackfn?: any
thisArg: U[];
)// ···
}
このセクションでは、パラメータの省略を許可するいくつかの方法を見ていきます。
パラメータ名の後に疑問符を付けると、そのパラメータはオプションになり、関数を呼び出す際に省略できます。
function trim1(str?: string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim1:
// %inferred-type: (str?: string | undefined) => string
; trim1
`trim1()` は次のように呼び出すことができます。
.equal(
asserttrim1('\n abc \t'), 'abc');
.equal(
asserttrim1(), '');
// `undefined` is equivalent to omitting the parameter
.equal(
asserttrim1(undefined), '');
`trim1()` のパラメータ `str` は外部的には `string|undefined` 型です。したがって、`trim1()` は次の関数とほぼ同等です。
function trim2(str: undefined|string): string {
// Internal type of str:
// %inferred-type: string | undefined
;
str
if (str === undefined) {
'';
return
}.trim();
return str
}
// External type of trim2:
// %inferred-type: (str: string | undefined) => string
; trim2
`trim2()` と `trim1()` の異なる点は、関数呼び出しでパラメータを省略できない点だけです(A行)。つまり、型が `undefined|T` のパラメータを省略する場合は、明示的にする必要があります。
.equal(
asserttrim2('\n abc \t'), 'abc');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
trim2(); // (A)
.equal(
asserttrim2(undefined), ''); // OK!
`str` にパラメータのデフォルト値を指定する場合、TypeScriptが型を推論できるため、型注釈を追加する必要はありません。
function trim3(str = ''): string {
// Internal type of str:
// %inferred-type: string
;
str
.trim();
return str
}
// External type of trim2:
// %inferred-type: (str?: string) => string
; trim3
デフォルト値により `undefined` になることがないため、`str` の内部型は `string` です。
`trim3()` を呼び出してみましょう。
.equal(
asserttrim3('\n abc \t'), 'abc');
// Omitting the parameter triggers the parameter default value:
.equal(
asserttrim3(), '');
// `undefined` is allowed and triggers the parameter default value:
.equal(
asserttrim3(undefined), '');
型とデフォルト値の両方を指定することもできます。
function trim4(str: string = ''): string {
.trim();
return str }
レストパラメータは残りのすべての引数を配列に収集します。したがって、その静的型は通常配列です。次の例では、`parts` はレストパラメータです。
function join(separator: string, ...parts: string[]) {
.join(separator);
return parts
}.equal(
assertjoin('-', 'state', 'of', 'the', 'art'),
'state-of-the-art');
次の例は2つの機能を示しています。
function repeat1(...[str, times]: [string, number]): string {
.repeat(times);
return str }
`repeat1()` は次の関数と同等です。
function repeat2(str: string, times: number): string {
.repeat(times);
return str }
名前付きパラメータ は、オブジェクトリテラルを使用して各パラメータに名前を付ける一般的なJavaScriptパターンです。次のようになります。
.equal(
assertpadStart({str: '7', len: 3, fillStr: '0'}),
'007');
プレーンなJavaScriptでは、関数はデストラクチャリングを使用して名前付きパラメータの値にアクセスできます。しかし、TypeScriptでは、オブジェクトリテラルの型も指定する必要があり、冗長になります。
function padStart({ str, len, fillStr = ' ' } // (A)
: { str: string, len: number, fillStr: string }) { // (B)
.padStart(len, fillStr);
return str }
デストラクチャリング(`fillStr` のデフォルト値を含む)はすべてA行で行われ、B行はTypeScript専用です。
インラインオブジェクトリテラル型の代わりに、別の型を定義することもできますが、ほとんどの場合、パラメータは関数ごとにローカルで一意であるため、それを行うのは避けています。関数ヘッダーにあまり多くのものを書きたくない場合は、それでも問題ありません。
this
をパラメータとして使う (上級)通常の関数には常に暗黙のパラメータ `this` があり、オブジェクトのメソッドとして使用できます。場合によっては、`this` の型を指定する必要があります。このユースケースにはTypeScript専用の構文があります。通常の関数の1つのパラメータに `this` という名前を付けることができます。このようなパラメータはコンパイル時のみ存在し、実行時には消えます。
例として、DOMイベントソースのインターフェース(やや簡略化されたバージョン)を考えてみましょう。
interface EventSource {addEventListener(
: string,
type: (this: EventSource, ev: Event) => any,
listener?: boolean | AddEventListenerOptions
options: void;
)// ···
}
コールバック `listener` の `this` は常に `EventSource` のインスタンスです。
次の例は、TypeScriptが `this` パラメータによって提供される型情報を使用して `.call()` の最初の引数をチェックする方法を示しています(A行とB行)。
function toIsoString(this: Date): string {
.toISOString();
return this
}
// @ts-expect-error: Argument of type '"abc"' is not assignable to
// parameter of type 'Date'. (2345)
.throws(() => toIsoString.call('abc')); // (A) error
assert
.call(new Date()); // (B) OK toIsoString
さらに、オブジェクト `obj` のメソッドとして `toIsoString()` を呼び出すことはできません。なぜなら、そのレシーバが `Date` のインスタンスではないからです。
= { toIsoString };
const obj // @ts-expect-error: The 'this' context of type
// '{ toIsoString: (this: Date) => string; }' is not assignable to
// method's 'this' of type 'Date'. [...]
.throws(() => obj.toIsoString()); // error
assert.toIsoString.call(new Date()); // OK obj
単一の型シグネチャでは、関数の動作を十分に説明できない場合があります。
次の例(A行とB行)で呼び出している関数 `getFullName()` を考えてみましょう。
interface Customer {: string;
id: string;
fullName
}= {id: '1234', fullName: 'Jane Bond'};
const jane = {id: '5678', fullName: 'Lars Croft'};
const lars = new Map<string, Customer>([
const idToCustomer '1234', jane],
['5678', lars],
[;
])
.equal(
assertgetFullName(idToCustomer, '1234'), 'Jane Bond'); // (A)
.equal(
assertgetFullName(lars), 'Lars Croft'); // (B)
`getFullName()` はどのように実装しますか?次の実装は、前の例での2つの関数呼び出しに対して機能します。
function getFullName(
: Customer | Map<string, Customer>,
customerOrMap?: string
id: string {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
しかし、この型シグネチャでは、コンパイル時には有効だが、ランタイムエラーが発生する関数呼び出しが許可されます。
.throws(() => getFullName(idToCustomer)); // missing ID
assert.throws(() => getFullName(lars, '5678')); // ID not allowed assert
次のコードでは、これらの問題が修正されています。
function getFullName(customerOrMap: Customer): string; // (A)
function getFullName( // (B)
: Map<string, Customer>, id: string): string;
customerOrMapfunction getFullName( // (C)
: Customer | Map<string, Customer>,
customerOrMap?: string
id: 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()` の型シグネチャはオーバーロードされています。
getFullName()
に使用できる2つの型シグネチャ(本体のない関数ヘッダ)があります。実際の実装の型シグネチャは使用できません!私のアドバイスは、オーバーロードが避けられない場合にのみオーバーロードを使用することです。1つの代替手段は、オーバーロードされた関数を異なる名前を持つ複数の関数に分割することです – 例えば
getFullName()
getFullNameViaMap()
インターフェースでは、複数の異なる呼び出しシグネチャを持つことができます。これにより、次の例のように、インターフェースGetFullName
をオーバーロードに使用できます。
interface GetFullName {: Customer): string;
(customerOrMap: Map<string, Customer>, id: string): string;
(customerOrMap
}
: GetFullName = (
const getFullName: Customer | Map<string, Customer>,
customerOrMap?: string
id: string => {
)if (customerOrMap instanceof Map) {
if (id === undefined) throw new Error();
= customerOrMap.get(id);
const customer if (customer === undefined) {
new Error('Unknown ID: ' + id);
throw
}= customer;
customerOrMap
} else {if (id !== undefined) throw new Error();
}.fullName;
return customerOrMap }
次の例では、文字列リテラル型('click'
など)を使用してオーバーロードします。これにより、パラメータtype
の値に応じて、パラメータlistener
の型を変更できます。
function addEventListener(elem: HTMLElement, type: 'click',
: (event: MouseEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: 'keypress',
: (event: KeyboardEvent) => void): void;
listenerfunction addEventListener(elem: HTMLElement, type: string, // (A)
: (event: any) => void): void {
listener.addEventListener(type, listener); // (B)
elem }
この場合、(行Aから始まる)実装の型を正しく取得することは比較的困難であるため、本体のステートメント(行B)が機能します。最終手段として、常に型any
を使用できます。
次の例は、メソッドのオーバーロードを示しています。メソッド.add()
はオーバーロードされています。
class StringBuilder {= '';
#data
add(num: number): this;
add(bool: boolean): this;
add(str: string): this;
add(value: any): this {
.#data += String(value);
this;
return this
}
toString() {
.#data;
return this
}
}
= new StringBuilder();
const sb
sb.add('I can see ')
.add(3)
.add(' monkeys!')
;
.equal(
assert.toString(), 'I can see 3 monkeys!') sb
Array.from()
の型定義は、オーバーロードされたインターフェースメソッドの例です。
interface ArrayConstructor {from<T>(arrayLike: ArrayLike<T>): T[];
from<T, U>(
: ArrayLike<T>,
arrayLike: (v: T, k: number) => U,
mapfn?: any
thisArg: U[];
) }
最初のシグネチャでは、返される配列の要素型はパラメータと同じです。
2番目のシグネチャでは、返される配列の要素は、mapfn
の結果と同じ型になります。このバージョンのArray.from()
は、Array.prototype.map()
に似ています。
このセクションでは、代入可能性に関する型互換性ルールについて説明します。型Src
の関数を型Trg
の記憶域(変数、オブジェクトのプロパティ、パラメータなど)に転送できますか?
代入可能性を理解することで、次のような質問に答えることができます。
この小節では、代入可能性の一般的なルール(関数のルールを含む)を調べます。次の小節では、これらのルールが関数に対して何を意味するのかを調べます。
型Src
は、次の条件のいずれかが真の場合、型Trg
に代入可能です。
Src
とTrg
は同一の型です。Src
またはTrg
はany
型です。Src
は文字列リテラル型であり、Trg
はプリミティブ型Stringです。Src
はユニオン型であり、Src
の各構成要素型はTrg
に代入可能です。Src
とTrg
は関数型であり、Trg
はrestパラメータを持つ、またはSrc
の必須パラメータの数がTrg
のパラメータの総数以下です。Trg
の各パラメータ型はSrc
の対応するパラメータ型に代入可能です。Trg
の戻り値型はvoid
であるか、Src
の戻り値型はTrg
の戻り値型に代入可能です。この小節では、代入ルールが次の2つの関数targetFunc
とsourceFunc
に対して何を意味するのかを調べます。
: Trg = sourceFunc; const targetFunc
例
: (x: RegExp) => Object = (x: Object) => /abc/; const trg1
次の例は、ターゲットの戻り値型がvoid
の場合、ソースの戻り値型は問題にならないことを示しています。なぜでしょうか?void
の結果はTypeScriptでは常に無視されます。
: () => void = () => new Date(); const trg2
ソースは、ターゲットよりも多くのパラメータを持つことはできません。
// @ts-expect-error: Type '(x: string) => string' is not assignable to
// type '() => string'. (2322)
: () => string = (x: string) => 'abc'; const trg3
ソースは、ターゲットよりも少ないパラメータを持つことができます。
: (x: string) => string = () => 'abc'; const trg4
なぜそうなのか?ターゲットはソースに対する期待値を指定しています。パラメータx
を受け入れる必要があります。それは実行しますが(ただし、無視します)。この許容性により、
'a', 'b'].map(x => x + x) [
.map()
のコールバックは、.map()
の型シグネチャで言及されている3つのパラメータのうち1つしかありません。
map<U>(
: (value: T, index: number, array: T[]) => U,
callback?: any
thisArg: U[]; )