くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログです

TypeScript の特徴を他の言語と比較してみた

こんにちは。2020年4月に新卒で入社した片山です。

私は学生時代、主に C / C++ や Python を書いていました。TypeScript は くらしのマーケットのバックエンドで使われており、入社して初めて触った言語です。 これまで触った言語のとの違いや、特徴的な型推論について調べてみました。

この内容は 9/18 に宮崎で開催された Webナイト宮崎 Vol.10 ~てげTypeScriptを学びたい~ で「TypeScript を初めて触った感想と型推論について」というタイトルで発表したものを記事として再編成・加筆したものになります。

言語の比較

学生時代触っていた C / C++、Python を TypeScript と比較しました。主観的な部分が含まれていますが、ご容赦ください。

実行速度の比較

Python

Python の標準の実装である CPyhon はインタプリタ言語であり、動的型付けを採用したことでコードを評価するたびに型のチェックが必要になることから、メモリアクセスの効率が悪いです。更に GIL の制約によりマルチスレッドのパフォーマンスも良くありません。しかし、PyPy のような JIT コンパイルを採用している処理系であれば CPython の3倍程度高速に実行することができるようです。

C / C++

最終的には機械語が出力されるため高速に実行できます。 学生時代作成したリバーシの思考ルーチンを Python から C++ に書き直した所約140倍速くなった経験があり、高速な言語といえばこれ、というイメージが強いです。

TypeScript

JavaScript にコンパイルして実行する必要があるので、JavaScript の実行速度になります。 Node.js では JIT Virtual Machine 型の V8 が使われており、高速に実行できます。

型についての比較

Python

整数や文字列、関数を含めたすべてのデータがオブジェクト(object)です。式の評価時に型をチェックする動的型付け言語です。

C / C++

char(8bit整数)、int(32bit整数 1)、float(32bit浮動小数点)と比較的低レベルな静的型付け言語です。型が違っていても暗黙的にキャストされるため、例えば以下のように意図しない動作をしてしまうことがあります。(重要じゃないコードは省略しています) また、高級言語ではあるものの、ポインタ や malloc関数 などの低レベルな処理を書くことができる反面、メモリ安全性を考慮する必要があり注意することが多いです。

char c = 'A'
int a = c;  // 65

double f = 3.14;
int b = f; // 3

int i = 100000;
short s = i;  // -31072

TypeScript

静的型付け、動的型付けのどちらも使うことができます。また、プリミティブ型は以下の種類しかなく、C言語と比べるとシンプルです。整数と小数を区別されておらず、どちらも number型 です 2。また、後述の型推論を使うことで、最低限のアノテーションで型安全なプログラムを記述できます。

  • number
  • bigint
  • string
  • boolean
  • null
  • undefined
  • symbol

パフォーマンスの比較

個人的には実行速度が気になりましたので、以下のような再帰関数で35番目のフィボナッチ数( = 9,227,465 )を求めるプログラムで実行時間を計測してみました。 シンプルなプログラムですが、計算量が文字通り指数的に増大します。複雑な計算がないので計算のコストよりも関数呼び出しのコストが大きく出るかもしれません。

# Python
def f(n):
    if n <= 1:
        return n
    else:
        return f(n - 2) + f(n - 1)

y = f(35)
言語 (実行条件) 実行時間
C 言語 ( GCC -O3 ) 0.027 s
C 言語 ( GCC -O2 ) 0.037 s
C 言語 ( GCC ) 0.074 s
JavaScript (Node.js) 0.097 s
Python ( PyPy ) 0.35 s
Python ( CPython ) 3.5 s

実行環境
OS: Ubuntu 18.04
CPU: AMD Ryzen 5 2600
RAM: 8GB 2667Hz 2枚

実行環境や使用したソフトウェアのバージョンによって結果が変わるので参考程度の実測値です。 JavaScript は GCC でコンパイルした C言語には敵いませんが、PyPy で実行した Python よりも約30倍速かったです。

型推論について

TypeScript で特徴的だと感じたのが型推論の機能です。C言語では必ず型を宣言する必要があり、型が違っていても暗黙的にキャストされるため代入できる場合があります 3。TypeScript では型を指定せずとも 3 を number 型と推論されます。それによって一貫性を守るように強制されるので型によるバグが減ります。

// C言語
char s[] = "ABC";
int i = 3;
i = s;  // -647713340 !?
// TypeScript
const s: string = "ABC";
let i = 3
i = s  // ERROR !!

次に、どのような記述なら正しく推論されるのかを調べてみました。プリミティブ型や、プリミティブ型で初期化したオブジェクトは問題なく型推推論できました。 ただ、宣言時に初期化をしない場合では any型 になってしまいます。初期化無しで変数を宣言するにはアノテーションが必要です。

// TypeScript
const i = 15;        // number
const I = 9007199254740991n; // bigint
const s = “hello”;   // string
const b = true;      // boolean
const n = null;      // null
const u = undefined; // undefined
const sym = Symbol("foo");  // symbol

// { name: string; sales: { jpy: number; usd: number; }[] }
const complex = {
  name: "taro",
  sales: [
      {jpy: 1200, usd: 12},
      {jpy: 810, usd: 8.1},
  ],
};

// 宣言をしたあとに代入
let x;
x = 3;  // any

let y: number;
y = 3;  // number

関数(の返り値)でも型推論は機能しますが、number型同士でのみ許可される * 演算子を戻り値にしたような場合だけです。関数の場合はアノテーションした方が良さそうです。

// TypeScript

// 返り値が推論できる
function mul(a, b){
    return a * b;
}
const y1 = mul(7, 5);  // number

// 返り値が推論できない
function add(a, b){
    return a + b;
}
const y2 = add(7, 5);  // any

既存の JavaScript のライブラリで型安全の恩恵を受ける

アンビエント宣言を記述することで、JavaScript で書かれたライブラリを TypeScript から呼び出す際に型安全の恩恵を受けることができるようになります。例えば以下はコミュニティが管理している Node.js の console.log のアンビエント宣言の抜粋です。(関係のない部分は省略してます)

interface Console {
  Console: NodeJS.ConsoleConstructor;
  log(message?: any, ...optionalParams: any[]): void;
}
<200b>
declare var console: Console;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v8/base.d.ts

まとめ

TypeScript は適切に書くことで、効率よく型安全なプログラムを作ることとができ、パフォーマンスも優れた言語だと感じました。しかし、基本である JavaScript の仕様を完全に把握できておらず、業務で混乱することがありましたので、まだまだ勉強が必要そうです!!


  1. int型のbit数はC言語の規格として定義されていないため、CPUによって64bitや、マイコンでは8bit、16bitが使われることがあります。
  2. 64bitの浮動小数点が使われていますが、整数を格納すると53bit精度になるようです。より大きな整数はbigint型で宣言します。
  3. char*(ポインタ)からintへ暗黙的にキャストされたため、アドレスが代入されています。また、実行ごとにこの値は変化します。