お久しぶりです。松山事務所の石丸です。
近頃実業務でも「Webの技術」でネイティブアプリを開発することが多くなってきました。
TypeScriptやES2015で少しでも保守性の高いコードを書きたいところですが、案件によってはES5.1で書かないといけないこともあります。
というわけで、今回はTypeScriptのコードをリアルタイムでJavaScriptにトランスパイルしてくれる
Playground · TypeScript を使って、
TypeScriptで書いていたアレをJavaScriptではどう書くのか、コードを比較しながら学んでいきたいと思います。
class
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
|
class Fruit {
constructor(public readonly name: string, private color: string) {
}
public getColor(): string {
return this.color;
}
}
const fruit = new Fruit("Apple", "red");
let color = fruit.getColor();
console.log(color);
|
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
|
var Fruit = /** @class */ (function () {
function Fruit(name, color) {
this.name = name;
this.color = color;
}
Fruit.prototype.getColor = function () {
return this.color;
};
return Fruit;
}());
var fruit = new Fruit("Apple", "red");
var color = fruit.getColor();
console.log(color);
|
即時関数でクラスを定義するコードになりました。
protoypeにメソッドを実装するよくあるクラスの実装かと思います。
引数や戻り値の型情報、publicやprivateなどのアクセス修飾子は削除されています。
またconstもletもES5.1にはないのでvarになります。
interface
TypeScript
1
2
3
4
5
6
7
8
9
|
interface Vehicle {
run(): void;
}
class Car implements Vehicle {
public run(): void {
console.log("ブーーーン");
}
}
|
JavaScript
1
2
3
4
5
6
7
8
9
|
var Car = /** @class */ (function () {
function Car() {
}
Car.prototype.run = function () {
console.log("ブーーーン");
};
return Car;
}());
|
跡形もなく消えてなくなります。実行時には必要ないですしね。
interfaceとは関係ないのですが、Carクラスのコンストラクタを省略したため、デフォルトコンストラクタが生成されていました。
namespace
TypeScript
1
2
3
4
5
6
7
8
9
|
namespace Utility {
export const VERSION = 1;
function usefulFunction() {
}
export function veryUsefulFunction() {
}
}
|
JavaScript
1
2
3
4
5
6
7
8
9
|
var Utility;
(function (Utility) {
Utility.VERSION = 1;
function usefulFunction() {
}
function veryUsefulFunction() {
}
Utility.veryUsefulFunction = veryUsefulFunction;
})(Utility || (Utility = {}));
|
namespaceオブジェクトUtility
に対して、exportする変数、関数を追加することで外部に公開しています。
このときの即時関数の呼び出し方ですが、既にUtilityオブジェクトがある場合はそれを、なければ空のオブジェクトを作って渡しています。
こういう||
を使った書き方はJavaScriptのイディオムらしいです。
継承
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class Animal {
protected age: number;
constructor(protected name: string) {
this.age = 0;
}
getName(): string {
return this.name;
}
}
class Friend extends Animal {
constructor(name: string) {
super(name);
}
getName(): string {
return super.getName() + "!";
}
getNameAndAge(): string {
return this.getName() + this.age + "歳!";
}
}
|
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Animal = /** @class */ (function () {
function Animal(name) {
this.name = name;
this.age = 0;
}
Animal.prototype.getName = function () {
return this.name;
};
return Animal;
}());
var Friend = /** @class */ (function (_super) {
__extends(Friend, _super);
function Friend(name) {
return _super.call(this, name) || this;
}
Friend.prototype.getName = function () {
return _super.prototype.getName.call(this) + "!";
};
Friend.prototype.getNameAndAge = function () {
return this.getName() + this.age + "歳!";
};
return Friend;
}(Animal));
|
継承ついでに、親コンストラクタの呼び出し、メソッドのオーバーライドもしてみました。
__extends関数で難しそうに見えますが、これは親クラスのプロパティとプロトタイプをコピーする関数だと理解しました。
派生クラスは即時関数の引数で親クラスを_super
として受け取り、まずは__extends関数で継承を行います。
派生クラスから親クラスへのアクセスは_super
を通し、メソッドはcallメソッドにthisを渡して呼び出します。
抽象クラスの継承
TypeScript
1
2
3
4
5
6
7
|
abstract class Animal {
abstract run(): void;
}
class Friend extends Animal {
run(): void {}
}
|
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var Animal = /** @class */ (function () {
function Animal() {
}
return Animal;
}());
var Friend = /** @class */ (function (_super) {
__extends(Friend, _super);
function Friend() {
return _super !== null && _super.apply(this, arguments) || this;
}
Friend.prototype.run = function () { };
return Friend;
}(Animal));
|
ほぼ継承と同じですが、抽象クラスのabstractメソッドが消えています。
interfaceと同様実行時には必要ないので、これは理解できるのですが、派生クラスのデフォルトコンストラクタの処理が理解できませんでした。
なぜ_superのnullチェックが必要なのか、なぜcallでなくapplyなのか、argumentsを渡す必要があるのか。
誰か教えて。
クラス変数、クラスメソッド
TypeScript
1
2
3
4
5
6
|
class Foo {
public static foo = "moge";
static bar(): void {
}
}
|
JavaScript
1
2
3
4
5
6
7
8
|
var Foo = /** @class */ (function () {
function Foo() {
}
Foo.bar = function () {
};
Foo.foo = "moge";
return Foo;
}());
|
インスタンスを生成せずに使えるアレですが、namespaceと同じですね。
enum
TypeScript
1
2
3
4
5
6
7
8
9
|
enum Animal {
Dog,
Cat
}
enum Food {
Fruit = "fruit",
Vegetable = "vegetable"
}
|
JavaScript
1
2
3
4
5
6
7
8
9
10
11
|
var Animal;
(function (Animal) {
Animal[Animal["Dog"] = 0] = "Dog";
Animal[Animal["Cat"] = 1] = "Cat";
})(Animal || (Animal = {}));
var Food;
(function (Food) {
Food["Fruit"] = "fruit";
Food["Vegetable"] = "vegetable";
})(Food || (Food = {}));
|
列挙子の値指定の有無の両パターン書いてみました。
こちらもnamespaceと同じように、enumオブジェクトに対して列挙子を追加しています。
列挙子の値がないenum AnimalのJavaScriptで
1
2
3
|
Animal[Animal["Dog"] = 0] = "Dog";
Animal[Animal["Cat"] = 1] = "Cat";
|
のような呪文が出てきますが、次のようなコードと等価でした。
1
2
3
4
5
|
Animal["Dog"] = 0;
Animal[0] = "Dog";
Animal["Cat"] = 1
Animal[1] = "Cat";
|
代入式の値は代入した値となるみたいです。
普段意識しませんが、a = b = 0
で a
に 0
が代入されるのはそういうことですかね?
オプション引数とデフォルト引数
TypeScript
1
2
3
4
|
function repeat(text?: string, count: number = 3): string {
if (text === undefined) { return ""; }
return text.repeat(count);
}
|
JavaScript
1
2
3
4
5
6
7
|
function repeat(text, count) {
if (count === void 0) { count = 3; }
if (text === undefined) {
return "";
}
return text.repeat(count);
}
|
オプション引数であることは実行時には関係ないので削除されています。
デフォルト引数の方は、引数が省略されたか判定し、省略されていたらデフォルト値を代入。
といった普段手で書くのと同じなのですが、count === void 0
がわかりませんでした。
MDN void 演算子によると、
void 演算子は与えられた式 (expression) を評価し、undefined を返します。
とのことでした。
undefinedはグローバル変数なので書き換えることが出来るらしく、ライブラリなどでは void 0
と書くらしいです。
アロー関数式
TypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class HelloPrinter {
private text = "hello";
public print1() {
var func = (text: string): void => {
alert(text);
}
func(this.text);
}
public print2() {
var func = (): void => {
alert(this.text);
}
func();
}
}
|
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
var HelloPrinter = /** @class */ (function () {
function HelloPrinter() {
this.text = "hello";
}
HelloPrinter.prototype.print1 = function () {
var func = function (text) {
alert(text);
};
func(this.text);
};
HelloPrinter.prototype.print2 = function () {
var _this = this;
var func = function () {
alert(_this.text);
};
func();
};
return HelloPrinter;
}());
|
ES5.1にはないアロー関数式はfunctionに置き換わりますが、その関数内でthisを参照するかどうかによって
var _this = this;
の1行が追加されます。賢いですね。
これで本物のアロー関数式と同様に外側のthisでbindされます。
まとめ
TypeScriptが出力するJavaScriptはきれいだと聞いていましたが、確かに読みやすかったかと思います。
ES5.1には足りない機能がたくさんあるのですが、それを実現するために見合わないコストをかけるより、
その言語なりの書き方で読みやすいコードを書いていきたいと思います。
JavaScriptプロの方からのツッコミ、アドバイスお待ちしております。