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 |
true と false 。 |
Symbol |
通常キーとして使用される一意の値。 |
Null |
ユニット型と同等。 |
Undefined |
これもユニット型と同等。 |
Object |
レコードに似ています。 |
TypeScript には、組み込み型に対応するプリミティブ型があります
number
string
bigint
boolean
symbol
null
undefined
object
その他の重要なTypeScriptの型
型 | 説明 |
---|---|
unknown |
トップ型。 |
never |
ボトム型。 |
オブジェクトリテラル | 例:{ property: Type } |
void |
ドキュメント化された戻り値がない関数用 |
T[] |
可変配列、Array<T> とも記述します。 |
[T, T] |
タプル。固定長だが可変。 |
(t: T) => U |
関数 |
注記
-
関数の構文にはパラメータ名が含まれます。これは慣れるのが非常に難しいです!
tslet fst: (a: any, b: any) => any = (a, b) => a;// or more precisely:let fst: <T, U>(a: T, b: U) => T = (a, b) => a; -
オブジェクトリテラルの型構文は、オブジェクトリテラルの値構文をほぼそのまま反映しています。
tslet o: { n: number; xs: object[] } = { n: 1, xs: [] }; -
[T, T]
はT[]
のサブタイプです。これはHaskellとは異なり、Haskellではタプルはリストとは関係ありません。
ボックス化された型
JavaScriptには、プログラマーがそれらの型に関連付けるメソッドを含む、プリミティブ型のボックス化された同等のものがあります。TypeScriptは、たとえば、プリミティブ型number
とボックス化された型Number
の違いを反映しています。ボックス化された型のメソッドはプリミティブを返すため、ボックス化された型はめったに必要ありません。
ts
(1).toExponential();// equivalent toNumber.prototype.toExponential.call(1);
数値リテラルでメソッドを呼び出すには、パーサーを補助するために括弧で囲む必要があることに注意してください。
段階的な型付け
TypeScriptは、式の型が何であるかを判断できない場合は常に、型any
を使用します。Dynamic
と比較すると、any
を型と呼ぶのは誇張表現です。単に、それが現れる場所では常に型チェッカーをオフにするだけです。たとえば、値を何らかの方法でマークすることなく、任意の値をany[]
にプッシュできます。
tsTry
// with "noImplicitAny": false in tsconfig.json, anys: any[]constanys = [];anys .push (1);anys .push ("oh no");anys .push ({anything : "goes" });
また、型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: falselet o = { x: "hi", extra: 1 }; // oklet o2: { x: string } = o; // ok
ここで、オブジェクトリテラル{ x: "hi", extra: 1 }
には、対応するリテラル型{ x: string, extra: number }
があります。その型は、必要なすべてのプロパティを持ち、それらのプロパティに割り当て可能な型があるため、{ x: string }
に割り当て可能です。追加のプロパティは割り当てを妨げることはなく、単に{ x: string }
のサブタイプにするだけです。
名前付き型は、単に型に名前を付けるだけです。割り当て可能性の目的では、下の型エイリアスOne
とインターフェース型Two
の間に違いはありません。どちらもプロパティp: string
を持っています。(ただし、型エイリアスは、再帰的な定義と型パラメーターに関してはインターフェースとは異なる動作をします。)
tsTry
typeOne = {p : string };interfaceTwo {p : string;}classThree {p = "Hello";}letx :One = {p : "hi" };lettwo :Two =x ;two = newThree ();
ユニオン
TypeScriptでは、ユニオン型はタグ付けされていません。言い換えれば、Haskellのdata
のような判別ユニオンではありません。ただし、組み込みのタグやその他のプロパティを使用して、ユニオン内の型を判別できることがよくあります。
tsTry
functionstart (arg : string | string[] | (() => string) | {s : string }): string {// this is super common in JavaScriptif (typeofarg === "string") {returncommonCase (arg );} else if (Array .isArray (arg )) {returnarg .map (commonCase ).join (",");} else if (typeofarg === "function") {returncommonCase (arg ());} else {returncommonCase (arg .s );}functioncommonCase (s : string): string {// finally, just convert a string to another stringreturns ;}}
string
、Array
、および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には、ユニオンに加えて、インターセクションもあります。
tsTry
typeCombined = {a : number } & {b : string };typeConflicting = {a : number } & {a : string };
Combined
には、1つのオブジェクトリテラル型として記述された場合と同様に、2つのプロパティa
とb
があります。インターセクションとユニオンは、競合が発生した場合に再帰的になるため、Conflicting.a: number & string
になります。
ユニット型
ユニット型は、正確に1つのプリミティブ値を含むプリミティブ型のサブタイプです。たとえば、文字列"foo"
には型"foo"
があります。JavaScriptには組み込みの列挙型がないため、代わりに既知の文字列のセットを使用するのが一般的です。文字列リテラル型のユニオンにより、TypeScriptはこのパターンを型付けできます。
tsTry
declare functionpad (s : string,n : number,direction : "left" | "right"): string;pad ("hi", 10, "left");
必要な場合、コンパイラーは、ユニット型をプリミティブ型("foo"
からstring
など)に *拡大* (スーパータイプに変換)します。これは可変性を使用する場合に発生し、可変変数の使用を妨げる可能性があります。
tsTry
lets = "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"'.pad ("hi", 10,); // error: 'string' is not assignable to '"left" | "right"' s
これがエラーが発生する理由です。
"right": "right"
s: string
。これは、"right"
が可変変数への代入時にstring
に拡大されるためです。string
は"left" | "right"
に割り当てることができません。
s
の型注釈を使用してこれを回避できますが、これにより、型"left" | "right"
ではない変数のs
への代入を防ぎます。
tsTry
lets : "left" | "right" = "right";pad ("hi", 10,s );
Haskellに類似した概念
文脈型付け
TypeScriptには、変数の宣言など、型を推論できる明らかな場所がいくつかあります。
tsTry
lets = "I'm a string!";
しかし、他のC構文言語を使用したことがある場合は予想しない可能性のあるいくつかの場所でも型を推論します。
tsTry
declare functionmap <T ,U >(f : (t :T ) =>U ,ts :T []):U [];letsns =map ((n ) =>n .toString (), [1, 2, 3]);
ここでは、この例のn: number
も、呼び出し前にT
とU
が推論されていないという事実にもかかわらず、そうです。実際、[1,2,3]
を使用してT=number
を推論した後、n => n.toString()
の戻り値の型を使用してU=string
を推論し、sns
の型をstring[]
にします。
推論は任意の順序で機能しますが、インテリセンスは左から右にしか機能しないため、TypeScriptは最初に配列を使用してmap
を宣言することを好みます。
tsTry
declare functionmap <T ,U >(ts :T [],f : (t :T ) =>U ):U [];
文脈型付けは、オブジェクトリテラルを介して再帰的に機能し、それ以外の場合はstring
またはnumber
として推論されるユニット型でも機能します。また、文脈から戻り値の型を推論できます。
tsTry
declare functionrun <T >(thunk : (t :T ) => void):T ;leti : {inference : string } =run ((o ) => {o .inference = "INSERT STATE HERE";});
o
の型は、次の理由により{ inference: string }
であると判断されます。
- 宣言初期化子は、宣言の型
{ inference: string }
によって文脈的に型付けされます。 - 呼び出しの戻り値の型は、推論に文脈型を使用するため、コンパイラーは
T={ inference: string }
であると推論します。 - アロー関数は、文脈型を使用してパラメーターの型を決定するため、コンパイラーは
o: { inference: string }
を与えます。
また、入力中にo.
を入力すると、実際のプログラムに存在する他のプロパティとともに、プロパティinference
の補完が表示されます。全体として、この機能により、TypeScriptの推論は統合型推論エンジンのように見える場合がありますが、そうではありません。
型エイリアス
型エイリアスは単なる別名であり、Haskellのtype
と同様です。コンパイラは、ソースコードで使用された場所でエイリアス名を使用しようとしますが、必ずしも成功するとは限りません。
tsTry
typeSize = [number, number];letx :Size = [101.1, 999.9];
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コードを使用して、共用体のメンバーを判別できます。
tsTry
typeShape =| {kind : "circle";radius : number }| {kind : "square";x : number }| {kind : "triangle";x : number;y : number };functionarea (s :Shape ) {if (s .kind === "circle") {returnMath .PI *s .radius *s .radius ;} else if (s .kind === "square") {returns .x *s .x ;} else {return (s .x *s .y ) / 2;}}
area
の戻り値の型は、TypeScriptが関数がtotalであることを認識しているため、number
と推論されることに注意してください。一部のバリアントがカバーされていない場合、area
の戻り値の型は代わりにnumber | undefined
になります。
また、Haskellとは異なり、共通のプロパティはどの共用体にも表示されるため、共用体の複数のメンバーを有効に判別できます。
tsTry
functionheight (s :Shape ) {if (s .kind === "circle") {return 2 *s .radius ;} else {// s.kind: "square" | "triangle"returns .x ;}}
型パラメータ
ほとんどの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() { }
後者のスタイルの方が一般的ですが、両方とも同じファイルで許可されています。
readonly
とconst
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); // errorb[0] = 101; // error
配列とオブジェクトリテラルで動作するconstアサーションを使用することもできます。
ts
let a = [1, 2, 3] as const;a.push(102); // errora[0] = 101; // error
ただし、これらのオプションはどれもデフォルトではないため、TypeScriptコードで一貫して使用されることはありません。
次のステップ
このドキュメントは、日常のコードで使用する構文と型についての概要を説明したものです。ここから、以下を行う必要があります。
- ハンドブック全体を最初から最後まで読む
- Playgroundの例を調べる