Typescript 👨🚀
Promise
コールバック地獄から脱する
Basic
const promise = new Promise((resolve, reject) => {
resolve(123);
});
promise.then((res) => {
console.log('I get called:', res === 123); // I get called: true
});
promise.catch((err) => {
// これは呼び出されません
});
Chain
Promiseチェーンに関する事実:
- エラーが起きた場合、後続のcatchにジャンプします(そして途中のthenはスキップします)
- 同期処理のエラーについても同様に、最も近い後続のcatchで捕捉されます
Promise.resolve(123)
.then((res) => {
console.log(res); // 123
return 456;
})
.then((res) => {
console.log(res); // 456
return Promise.resolve(123); // resolveされたPromiseを返しています
})
.then((res) => {
console.log(res); // 123 : resolveされた値で`then`が呼び出されます
return 123;
})
チェーンの前の部分のエラー処理を単一のcatchに集約
// rejectされたPromiseを作成する
Promise.reject(new Error('何か悪いことが起きた'))
.then((res) => {
console.log(res); // 呼び出されない
return 456;
})
.then((res) => {
console.log(res); // 呼び出されない
return 123;
})
.then((res) => {
console.log(res); // 呼び出されない
return 123;
})
.catch((err) => {
console.log(err.message); // 何か悪いことが起きた
});
catchは実のところ新しいPromiseを返す(要するに新しいPromiseのチェーンを作成)
// rejectされたPromiseを作成する
Promise.reject(new Error('何か悪いことが起きた'))
.then((res) => {
console.log(res); // 呼び出されない
return 456;
})
.catch((err) => {
console.log(err.message); // 何か悪いことが起きた
return 123;
})
.then((res) => {
console.log(res); // 123
})
並列制御フロー(Parallel control flow)
すべてのタスクが終わったタイミングで何らかの処理を行いたいケースがあるかもしれません。Promiseは静的なPromise.all関数を提供します。この関数は、n個のPromiseがすべて完了するまで待つことができます。n個のPromiseの配列を渡すと、n個の解決された値の配列を返します。
Promise.all
// 何らかのデータをサーバから読み込むことを再現する処理
function loadItem(id: number): Promise<{ id: number }> {
return new Promise((resolve) => {
console.log('loading item', id);
setTimeout(() => { // サーバーからのレスポンス遅延を再現
resolve({ id: id });
}, 1000);
});
}
// 並列処理
Promise.all([loadItem(1), loadItem(2)])
.then((res) => {
[item1, item2] = res;
console.log('done');
}); // 全体で 1秒 かかる
Promise.race
var task1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 1000, 'one');
});
var task2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 2000, 'two');
});
Promise.race([task1, task2]).then(function(value) {
console.log(value); // "one"
// 両方ともresolveされるが、task1の方が早く終わる
});
非同期関数のタブー🙅♂️
- 決してコールバックを2回呼ばないでください。
- 決して Error をthrowしないでください。
eg. コールバックを2回
Bad:
function loadJSON(filename: string, cb: (error: Error) => void) {
fs.readFile(filename, function (err, data) {
if (err) {
cb(err);
}
else {
try {
cb(null, JSON.parse(data));
}
catch (err) {
cb(err);
}
}
});
}
Good:
function loadJSON(filename: string, cb: (error: Error) => void) {
fs.readFile(filename, function (err, data) {
if (err) return cb(err);
// すべての同期処理コードをtry catchブロックに含める
try {
var parsed = JSON.parse(data);
}
catch (err) {
return cb(err);
}
// コールバックを呼び出す時を除く
return cb(null, parsed);
});
}
Error
をthrow
eg. Bad:
import fs = require('fs');
// 最初に思いつくであろう試みですが、正しくありません
function loadJSON(filename: string, cb: (error: Error, data: any) => void) {
fs.readFile(filename, function (err, data) {
if (err) cb(err);
else cb(null, JSON.parse(data)); // ここでスローしてるけどキャッチしてない
});
}
// 正しくないJSONファイルのロード
loadJSON('invalid.json', function (err, data) {
// このコードは永久に実行されません
if (err) console.log('bad.json error', err.message);
else console.log(data);
});
async / await
async
関数の前にasyncキーワードをつけることで、その関数は非Promiseの値を返す時にその値を解決したPromiseを返すようになります。
async function requestAsync(): Promise<number> {
return 1;
}
// asyncを使わずに書いた場合
function request(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
requestAsync().then((result) => {
console.log(result); // -> 1
});
Promiseをそのまま返すことも可能です。二重にPromiseがラップされることはありません。
async function requestAsync(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
requestAsync().then((result) => {
console.log(result); // -> 1
});
await
awaitはPromiseの値が解決されるまで実行を待機して、解決された値を返します。
awaitの注意点:
- awaitはasync関数の中でのみ使える
// 1秒後に値を返す
function request(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve('hello');
}, 1000);
});
}
// この書き方はできない
// const result = await request();
// console.log(result);
async function main() {
const result = await request();
console.log(result);
}
main();
async / await
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
};
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result1+1);
}, 1000);
});
};
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(result2: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result2+2);
}, 1000);
});
};
async function main() {
const result1 = await request1();
const result2 = await request2(result1);
const result3 = await request3(result2);
console.log(result3);
}
main();
関数
名前付き関数(Normal Functions)
function increment(num: number): number {
return num + 1;
}
匿名関数(Anonymous Functions)
無名関数を利用することは、高階関数を利用する際にコードがシンプルになるというメリットにも繋がります。
高階関数とは、関数自体を引数や戻り値として扱うことができる関数のことをいいます。
高階関数では、引数になる関数が1回きりしか使わない関数の場合が多く、そのため無名関数にすることでコードをシンプルにすることができます。
また、無名関数にすることでグローバルスコープ名を定義する必要がないため、結果的に関数名の重複によるエラーを減らすことにもつながります。
const increment = function(num: number): number {
return num + 1;
};
匿名かつアロー関数(Arrow Functions)
const increment = (num: number): number => {
return num + 1;
};
アロー関数
Tip:アロー関数の危険性
実は、thisを呼び出しコンテキストにしたい場合はアロー関数を使うべきではありません。jquery、underscore、mochaのようなライブラリで使用するコールバックのケースです。ドキュメントがthisの機能について言及している場合は、おそらくアロー関数の代わりにfunctionを使うべきでしょう。同様に、あなたがargumentsを使うことを予定している場合は、アロー関数を使用しないでください。