← melang

TypeScript

melangの各noteで他言語と比較した、TypeScriptの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

コンパイル時に型検査するが、実行時に型情報は残らない。

let age: number = 20;
let age: number = 20;
age = "Alice"; // コンパイルエラー

型検査はコンパイル時のみで、JavaScriptとして実行される段階では型情報は消える。「静的に検査すること」と「実行時にも型情報を持つこと」が別問題であることを示す例。

型注釈の位置

postfix構文。戻り値は推論できるが、引数はstrictモードで実質必須になる。

let age: number = 20;
let age: number = 20;
let name = "Alice"; // 型推論でstring

function greet(name: string) { // 引数はstrictモードで実質必須
    return `Hello, ${name}`; // 戻り値の型は推論される
}

`name: Type`というpostfix構文で、変数・戻り値はコンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が推論できるため省略されることが多い。一方、引数の型は`noImplicitAny`を有効にした標準的な設定では推論できず、省略するとエラーになるため実質的に明示が必須という非対称な設計。

型変換

型検査は変換しない。実行時の変換はJavaScriptに従う。

const n = Number(s);
const s: string = "42";
const n: number = Number(s);
const t: string = n.toString();

Number("abc");  // NaN(型はnumberのまま)
parseInt("abc"); // NaN

const v = s as unknown as number; // 型を偽るだけで値は変換されない

`Number(x)`や`x.toString()`で実行時に変換する。失敗は`NaN`になり、型はnumberのままなので失敗可能性が型に現れない。`as`は型検査器への申告にすぎず、実行時には何も変換しない点も、変換と混同しやすい。

暗黙の型変換

型検査上は暗黙変換を許さないが、実行時はJS由来の変換が残る。

let x: number = 10;
let x: number = 10;
let y: string = x; // 型エラー

let a: number = 10;
let b: string = "20";
a + b; // 型エラー(算術演算子のオペランドはnumber/bigintを要求)

静的な型検査上は数値と文字列などの間の暗黙変換を許さない。ただし型検査を通過してJavaScriptとして実行される段階では、`+`演算子など実行時の暗黙変換自体は残っており、「型システム上の暗黙変換」と「実行時の暗黙変換」が分離しているのが特徴。

Enum / Sum Type

enumよりも、literal unionでdiscriminated unionを表現する。

type Result = { kind: 'success'; user: User } | { kind: 'failure'; message: string };
type Result =
  | { kind: 'success'; user: User }
  | { kind: 'failure'; message: string };

function describe(r: Result): string {
  switch (r.kind) {
    case 'success': return `ok: ${r.user}`;
    case 'failure': return `error: ${r.message}`;
    default:
      const exhaustiveCheck: never = r; // 網羅漏れがあると型エラーになる
      return exhaustiveCheck;
  }
}

`enum`はGoに近い整数/文字列定数寄りで、payload付きのADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。表現にはliteral union(discriminated union)を使う。`switch`自体に網羅性チェックはないが、`never`型への代入という慣用句で擬似的に検証できる。

値の不変性

`const`は束縛だけ。`readonly`は型検査だけで、実行時には効かない。

const x = ... / readonly field: T / as const
const user = { name: "alice", tags: ["a"] };
// user = { name: "bob", tags: [] };   // エラー: 再代入
user.name = "bob";                      // OK: constでも変更できる

type RO = Readonly<{ name: string; tags: readonly string[] }>;
const ro: RO = { name: "alice", tags: ["a"] };
// ro.name = "bob";                     // 型エラー

const loose: { name: string } = ro;     // readonlyを外して代入できる
loose.name = "bob";                     // 型は通り、実行時に変更される

`const`は束縛だけを固定する。`readonly`や`Readonly<T>`は型だけの制限で、実行時には何も防がず、readonlyを外した型へ代入できる場合もある。不変性は型検査の範囲に留まる。

名前付き引数

名前付き引数はなく、オブジェクトを分割代入で受け取るパターンで名前を表現する。

function foo({ width, height }: { width: number; height: number })
function foo({ width, height }: { width: number; height: number }) {}
foo({ width: 10, height: 20 }); // オブジェクトで意味を明示

function bar(width: number, height: number) {}
bar(10, 20); // 通常の引数は位置のみ

言語機能としての名前付き引数はないが、オブジェクトを受け取り分割代入するパターンで、名前付き引数に近い書き心地を実現できる。型注釈と組み合わせれば、キーの必須・省略も表現できる。

関数の引数

プリミティブは値、オブジェクトは参照の値が渡される。

function f(n: number, obj: { v: number })
function f(n: number, obj: { v: number }) {
  n = 1;     // 呼び出し側には影響しない
  obj.v = 1; // 呼び出し側から見える
}

function g(obj: Readonly<{ v: number }>) {
  // obj.v = 1; // 型検査でエラー
}

JavaScriptと同じ挙動で、渡し方を選ぶ構文はない。`Readonly`型で型検査上は書き換えを禁じられるが、実行時の挙動は変わらない。プリミティブとオブジェクトで扱いが変わるため、型を見ないと渡し方が分からない。

if 文/式

`if`はstatement。値の分岐は三項演算子`?:`で書く。

if (cond) { ... } else { ... } / cond ? a : b
const x = cond ? 1 : 2;

if (cond) {
  process();
}

JavaScriptと同じく、`if`はstatementで値を返せない。値の分岐は三項演算子で書き、ネストすると読みにくくなりやすい。

I/O の明示

非同期I/Oは`Promise`型で見えるが、同期I/Oは見えない。

function findUser(id: UserId): Promise<User>
function findUser(id: UserId): Promise<User> { /* ... */ }

function getUser(id: UserId): User {
  findUser(id); // awaitせずに呼べる。戻り値を無視しても型エラーにならない
  ...
}

非同期処理は`Promise<T>`という型で戻り値に現れ、呼び出し側は`await`を書かないと値を取り出せないが、`await`せずに呼び出すこと自体は型エラーにならない。同期的なI/O(`fs.readFileSync`など)は通常の関数と区別されない。

言語名

"Type" + "JavaScript"の合成語で、既存言語との関係を名前に含める。

JavaScriptに型を付けるという設計目的をそのまま名前にした合成語。既存言語の拡張であることが名前から一目で伝わる利点がある一方、"Script"を含むためコマンド名は`tsc`のように略される。