関数型プログラマーのための TypeScript

TypeScript は、Microsoft のプログラマーが従来のオブジェクト指向プログラムを Web に持ち込めるように、従来のオブジェクト指向型を JavaScript に導入する試みとして始まりました。開発が進むにつれて、TypeScript の型システムは、ネイティブな JavaScript プログラマーによって記述されたコードをモデル化するように進化してきました。その結果、強力で興味深く、複雑なシステムになっています。

この入門編は、TypeScript を学びたい Haskell または ML プログラマー向けに設計されています。ここでは、TypeScript の型システムが Haskell の型システムとどのように異なるかを説明します。また、JavaScript コードのモデル化から生じる TypeScript の型システムの独自機能についても説明します。

この入門編では、オブジェクト指向プログラミングについては扱いません。実際には、TypeScript でのオブジェクト指向プログラムは、OO 機能を備えた他の一般的な言語でのプログラムと似ています。

前提条件

この入門編では、以下のことを知っていることを前提としています。

  • JavaScript の良い部分でのプログラミング方法。
  • C 系言語の型構文。

JavaScript の良い部分を学ぶ必要がある場合は、JavaScript: The Good Parts を読んでください。多くの可変性があり、それほど多くの機能を持たない、call-by-value のレキシカルスコープ言語でプログラムを作成する方法を知っていれば、この本をスキップできるかもしれません。R4RS Scheme が良い例です。

The C++ Programming Language は、C スタイルの型構文について学ぶのに適した場所です。C++ とは異なり、TypeScript は string x の代わりに x: string のように、後置型を使用します。

Haskell にはない概念

組み込み型

JavaScript は 8 つの組み込み型を定義しています

説明
Number 倍精度 IEEE 754 浮動小数点数。
String 不変の UTF-16 文字列。
BigInt 任意の精度形式の整数。
Boolean truefalse
Symbol 通常キーとして使用される一意の値。
Null ユニット型と同等。
Undefined これもユニット型と同等。
Object レコードに似ています。

詳細については MDN ページを参照してください。.

TypeScript には、組み込み型に対応するプリミティブ型があります

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

その他の重要なTypeScriptの型

説明
unknown トップ型。
never ボトム型。
オブジェクトリテラル 例:{ property: Type }
void ドキュメント化された戻り値がない関数用
T[] 可変配列、Array<T>とも記述します。
[T, T] タプル。固定長だが可変。
(t: T) => U 関数

注記

  1. 関数の構文にはパラメータ名が含まれます。これは慣れるのが非常に難しいです!

    ts
    let fst: (a: any, b: any) => any = (a, b) => a;
    // or more precisely:
    let fst: <T, U>(a: T, b: U) => T = (a, b) => a;
  2. オブジェクトリテラルの型構文は、オブジェクトリテラルの値構文をほぼそのまま反映しています。

    ts
    let o: { n: number; xs: object[] } = { n: 1, xs: [] };
  3. [T, T]T[]のサブタイプです。これはHaskellとは異なり、Haskellではタプルはリストとは関係ありません。

ボックス化された型

JavaScriptには、プログラマーがそれらの型に関連付けるメソッドを含む、プリミティブ型のボックス化された同等のものがあります。TypeScriptは、たとえば、プリミティブ型numberとボックス化された型Numberの違いを反映しています。ボックス化された型のメソッドはプリミティブを返すため、ボックス化された型はめったに必要ありません。

ts
(1).toExponential();
// equivalent to
Number.prototype.toExponential.call(1);

数値リテラルでメソッドを呼び出すには、パーサーを補助するために括弧で囲む必要があることに注意してください。

段階的な型付け

TypeScriptは、式の型が何であるかを判断できない場合は常に、型anyを使用します。Dynamicと比較すると、anyを型と呼ぶのは誇張表現です。単に、それが現れる場所では常に型チェッカーをオフにするだけです。たとえば、値を何らかの方法でマークすることなく、任意の値をany[]にプッシュできます。

ts
// with "noImplicitAny": false in tsconfig.json, anys: any[]
const anys = [];
anys.push(1);
anys.push("oh no");
anys.push({ anything: "goes" });
Try

また、型anyの式はどこでも使用できます。

ts
anys.map(anys[1]); // oh no, "oh no" is not a function

anyは伝染性もあります。型anyの式で変数を初期化すると、変数も型anyになります。

ts
let sepsis = anys[0] + anys[1]; // this could mean anything

TypeScriptがanyを生成するときにエラーを取得するには、tsconfig.json"noImplicitAny": trueまたは"strict": trueを使用します。

構造的な型付け

構造的な型付けは、ほとんどの関数型プログラマーにとって馴染みのある概念ですが、HaskellやほとんどのMLは構造的に型付けされていません。その基本的な形式は非常に簡単です。

ts
// @strict: false
let o = { x: "hi", extra: 1 }; // ok
let o2: { x: string } = o; // ok

ここで、オブジェクトリテラル{ x: "hi", extra: 1 }には、対応するリテラル型{ x: string, extra: number }があります。その型は、必要なすべてのプロパティを持ち、それらのプロパティに割り当て可能な型があるため、{ x: string }に割り当て可能です。追加のプロパティは割り当てを妨げることはなく、単に{ x: string }のサブタイプにするだけです。

名前付き型は、単に型に名前を付けるだけです。割り当て可能性の目的では、下の型エイリアスOneとインターフェース型Twoの間に違いはありません。どちらもプロパティp: stringを持っています。(ただし、型エイリアスは、再帰的な定義と型パラメーターに関してはインターフェースとは異なる動作をします。)

ts
type One = { p: string };
interface Two {
p: string;
}
class Three {
p = "Hello";
}
 
let x: One = { p: "hi" };
let two: Two = x;
two = new Three();
Try

ユニオン

TypeScriptでは、ユニオン型はタグ付けされていません。言い換えれば、Haskellのdataのような判別ユニオンではありません。ただし、組み込みのタグやその他のプロパティを使用して、ユニオン内の型を判別できることがよくあります。

ts
function start(
arg: string | string[] | (() => string) | { s: string }
): string {
// this is super common in JavaScript
if (typeof arg === "string") {
return commonCase(arg);
} else if (Array.isArray(arg)) {
return arg.map(commonCase).join(",");
} else if (typeof arg === "function") {
return commonCase(arg());
} else {
return commonCase(arg.s);
}
 
function commonCase(s: string): string {
// finally, just convert a string to another string
return s;
}
}
Try

stringArray、およびFunctionには、組み込みの型述語があり、elseブランチにオブジェクト型を便利に残しています。ただし、実行時に区別するのが難しいユニオンを生成することは可能です。新しいコードでは、判別されたユニオンのみを作成するのが最善です。

以下の型には組み込みの述語があります。

述語
string typeof s === "string"
number typeof n === "number"
bigint typeof m === "bigint"
boolean typeof b === "boolean"
symbol typeof g === "symbol"
undefined typeof undefined === "undefined"
関数 typeof f === "function"
配列 Array.isArray(a)
object typeof o === "object"

関数と配列は実行時にはオブジェクトですが、独自の述語を持っていることに注意してください。

インターセクション

TypeScriptには、ユニオンに加えて、インターセクションもあります。

ts
type Combined = { a: number } & { b: string };
type Conflicting = { a: number } & { a: string };
Try

Combinedには、1つのオブジェクトリテラル型として記述された場合と同様に、2つのプロパティabがあります。インターセクションとユニオンは、競合が発生した場合に再帰的になるため、Conflicting.a: number & stringになります。

ユニット型

ユニット型は、正確に1つのプリミティブ値を含むプリミティブ型のサブタイプです。たとえば、文字列"foo"には型"foo"があります。JavaScriptには組み込みの列挙型がないため、代わりに既知の文字列のセットを使用するのが一般的です。文字列リテラル型のユニオンにより、TypeScriptはこのパターンを型付けできます。

ts
declare function pad(s: string, n: number, direction: "left" | "right"): string;
pad("hi", 10, "left");
Try

必要な場合、コンパイラーは、ユニット型をプリミティブ型("foo"からstringなど)に *拡大* (スーパータイプに変換)します。これは可変性を使用する場合に発生し、可変変数の使用を妨げる可能性があります。

ts
let s = "right";
pad("hi", 10, s); // error: 'string' is not assignable to '"left" | "right"'
Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.2345Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.
Try

これがエラーが発生する理由です。

  • "right": "right"
  • s: string。これは、"right"が可変変数への代入時にstringに拡大されるためです。
  • string"left" | "right"に割り当てることができません。

sの型注釈を使用してこれを回避できますが、これにより、型"left" | "right"ではない変数のsへの代入を防ぎます。

ts
let s: "left" | "right" = "right";
pad("hi", 10, s);
Try

Haskellに類似した概念

文脈型付け

TypeScriptには、変数の宣言など、型を推論できる明らかな場所がいくつかあります。

ts
let s = "I'm a string!";
Try

しかし、他のC構文言語を使用したことがある場合は予想しない可能性のあるいくつかの場所でも型を推論します。

ts
declare function map<T, U>(f: (t: T) => U, ts: T[]): U[];
let sns = map((n) => n.toString(), [1, 2, 3]);
Try

ここでは、この例のn: numberも、呼び出し前にTUが推論されていないという事実にもかかわらず、そうです。実際、[1,2,3]を使用してT=numberを推論した後、n => n.toString()の戻り値の型を使用してU=stringを推論し、snsの型をstring[]にします。

推論は任意の順序で機能しますが、インテリセンスは左から右にしか機能しないため、TypeScriptは最初に配列を使用してmapを宣言することを好みます。

ts
declare function map<T, U>(ts: T[], f: (t: T) => U): U[];
Try

文脈型付けは、オブジェクトリテラルを介して再帰的に機能し、それ以外の場合はstringまたはnumberとして推論されるユニット型でも機能します。また、文脈から戻り値の型を推論できます。

ts
declare function run<T>(thunk: (t: T) => void): T;
let i: { inference: string } = run((o) => {
o.inference = "INSERT STATE HERE";
});
Try

oの型は、次の理由により{ inference: string }であると判断されます。

  1. 宣言初期化子は、宣言の型{ inference: string }によって文脈的に型付けされます。
  2. 呼び出しの戻り値の型は、推論に文脈型を使用するため、コンパイラーはT={ inference: string }であると推論します。
  3. アロー関数は、文脈型を使用してパラメーターの型を決定するため、コンパイラーはo: { inference: string }を与えます。

また、入力中にo.を入力すると、実際のプログラムに存在する他のプロパティとともに、プロパティinferenceの補完が表示されます。全体として、この機能により、TypeScriptの推論は統合型推論エンジンのように見える場合がありますが、そうではありません。

型エイリアス

型エイリアスは単なる別名であり、Haskellのtypeと同様です。コンパイラは、ソースコードで使用された場所でエイリアス名を使用しようとしますが、必ずしも成功するとは限りません。

ts
type Size = [number, number];
let x: Size = [101.1, 999.9];
Try

newtypeに最も近いのは、タグ付きインターセクションです。

ts
type FString = string & { __compileTimeOnly: any };

FStringは通常の文字列とまったく同じですが、コンパイラは実際には存在しない__compileTimeOnlyという名前のプロパティを持っていると考えています。これは、FStringは依然としてstringに代入できますが、その逆はできないことを意味します。

判別共用体

dataに最も近いのは、判別プロパティを持つ型の共用体であり、通常、TypeScriptでは判別共用体と呼ばれます。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };

Haskellとは異なり、タグ(判別子)は各オブジェクト型の単なるプロパティです。各バリアントには、異なるユニット型を持つ同一のプロパティがあります。これは依然として通常の共用型です。先頭の|は共用型構文のオプション部分です。通常のJavaScriptコードを使用して、共用体のメンバーを判別できます。

ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
 
function area(s: Shape) {
if (s.kind === "circle") {
return Math.PI * s.radius * s.radius;
} else if (s.kind === "square") {
return s.x * s.x;
} else {
return (s.x * s.y) / 2;
}
}
Try

areaの戻り値の型は、TypeScriptが関数がtotalであることを認識しているため、numberと推論されることに注意してください。一部のバリアントがカバーされていない場合、areaの戻り値の型は代わりにnumber | undefinedになります。

また、Haskellとは異なり、共通のプロパティはどの共用体にも表示されるため、共用体の複数のメンバーを有効に判別できます。

ts
function height(s: Shape) {
if (s.kind === "circle") {
return 2 * s.radius;
} else {
// s.kind: "square" | "triangle"
return s.x;
}
}
Try

型パラメータ

ほとんどのC系言語と同様に、TypeScriptでは型パラメータの宣言が必要です。

ts
function liftArray<T>(t: T): Array<T> {
return [t];
}

大文字と小文字の区別はありませんが、型パラメータは慣例的に単一の大文字です。型パラメータは型に制約することもでき、これは型クラス制約に少し似た動作をします。

ts
function firstish<T extends { length: number }>(t1: T, t2: T): T {
return t1.length > t2.length ? t1 : t2;
}

TypeScriptは通常、引数の型に基づいて呼び出しから型引数を推論できるため、型引数は通常必要ありません。

TypeScriptは構造的であるため、名目システムほど型パラメータを必要としません。具体的には、関数を多相にするために必要ありません。型パラメータは、パラメータを同じ型に制約するなど、型情報を伝播するためにのみ使用する必要があります。

ts
function length<T extends ArrayLike<unknown>>(t: T): number {}
function length(t: ArrayLike<unknown>): number {}

最初のlengthでは、Tは必要ありません。1回しか参照されていないため、戻り値または他のパラメータの型を制約するために使用されていないことに注意してください。

高カインド型

TypeScriptには高カインド型がないため、次のものは有効ではありません。

ts
function length<T extends ArrayLike<unknown>, U>(m: T<U>) {}

ポイントフリープログラミング

ポイントフリープログラミング(カリー化と関数合成を多用する)はJavaScriptで可能ですが、冗長になる可能性があります。TypeScriptでは、ポイントフリープログラムの型推論が失敗することが多いため、値パラメータの代わりに型パラメータを指定することになります。結果として非常に冗長になるため、通常はポイントフリープログラミングを避ける方が良いでしょう。

モジュールシステム

JavaScriptの最新のモジュール構文はHaskellのモジュール構文に少し似ていますが、importまたはexportを含むファイルは暗黙的にモジュールになります。

ts
import { value, Type } from "npm-package";
import { other, Types } from "./local-package";
import * as prefix from "../lib/third-package";

commonjsモジュール(node.jsのモジュールシステムを使用して記述されたモジュール)をインポートすることもできます。

ts
import f = require("single-function-package");

エクスポートリストを使用してエクスポートできます。

ts
export { f };
function f() {
return g();
}
function g() {} // g is not exported

または、各エクスポートを個別にマークすることによってエクスポートできます。

ts
export function f() { return g() }
function g() { }

後者のスタイルの方が一般的ですが、両方とも同じファイルで許可されています。

readonlyconst

JavaScriptでは、可変性がデフォルトですが、constを使用した変数宣言で、参照が不変であることを宣言できます。参照先は依然として可変です。

js
const a = [1, 2, 3];
a.push(102); // ):
a[0] = 101; // D:

TypeScriptには、さらにプロパティ用のreadonly修飾子があります。

ts
interface Rx {
readonly x: number;
}
let rx: Rx = { x: 1 };
rx.x = 12; // error

また、すべてのプロパティをreadonlyにするマップ型Readonly<T>が付属しています。

ts
interface X {
x: number;
}
let rx: Readonly<X> = { x: 1 };
rx.x = 12; // error

そして、副作用のあるメソッドを削除し、配列のインデックスへの書き込みを防止する特定のReadonlyArray<T>型と、この型の特別な構文があります。

ts
let a: ReadonlyArray<number> = [1, 2, 3];
let b: readonly number[] = [1, 2, 3];
a.push(102); // error
b[0] = 101; // error

配列とオブジェクトリテラルで動作するconstアサーションを使用することもできます。

ts
let a = [1, 2, 3] as const;
a.push(102); // error
a[0] = 101; // error

ただし、これらのオプションはどれもデフォルトではないため、TypeScriptコードで一貫して使用されることはありません。

次のステップ

このドキュメントは、日常のコードで使用する構文と型についての概要を説明したものです。ここから、以下を行う必要があります。

TypeScriptのドキュメントはオープンソースプロジェクトです。これらのページの改善にご協力ください。プルリクエストを送信して

このページの共同作成者
OTOrta Therox (15)
MFMartin Fischer (1)
JRSDSJonas Raoni Soares da Silva (1)
RCRyan Cavanaugh (1)
Hhuying (1)
10+

最終更新日: 2024年3月21日