面向函式式程式設計師的 TypeScript

TypeScript 的誕生初衷是為 JavaScript 引入傳統的面向物件型別,以便微軟的程式設計師能夠將傳統的面向物件程式帶到 Web 上。隨著發展,TypeScript 的型別系統已演變為能夠為原生 JavaScript 程式碼建模。最終形成的系統既強大、有趣,又顯得有些雜亂。

本入門指南專為想要學習 TypeScript 的 Haskell 或 ML 程式設計師而設計。它描述了 TypeScript 的型別系統與 Haskell 型別系統的區別,並介紹了 TypeScript 型別系統中因模擬 JavaScript 程式碼而產生的獨特特性。

本指南不涵蓋面向物件程式設計。實際上,TypeScript 中的面向物件程式與其他具有面向物件特性的流行語言非常相似。

先決條件

在本入門指南中,我假設您瞭解以下內容:

  • 如何編寫 JavaScript(即 JavaScript 的精華部分)。
  • 類 C 語言的型別語法。

如果您需要學習 JavaScript 的精華部分,請閱讀《JavaScript: The Good Parts》。如果您懂得如何編寫按值呼叫、具有詞法作用域且包含大量可變性(除此之外沒別的)的語言,則可能不需要閱讀這本書。R4RS Scheme 就是一個很好的例子。

《C++ 程式設計語言》是學習 C 風格型別語法的好去處。與 C++ 不同,TypeScript 使用字尾型別,例如:x: string 而不是 string x

Haskell 中不存在的概念

內建型別

JavaScript 定義了 8 種內建型別

型別 說明
Number IEEE 754 雙精度浮點數。
String 不可變的 UTF-16 字串。
BigInt 任意精度格式的整數。
Boolean truefalse
Symbol 通常用作鍵的唯一值。
Null 等同於單元型別 (unit type)。
Undefined 同樣等同於單元型別。
Object 類似於記錄 (record)。

詳情請參閱 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

stringArrayFunction 具有內建的型別謂詞,方便地將物件型別留給 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"
function typeof f === "function"
array Array.isArray(a)
object typeof o === "object"

注意,函式和陣列在執行時是物件,但它們有自己的謂詞。

交叉型別

除了聯合型別,TypeScript 還有交叉型別。

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

Combined 具有兩個屬性 ab,就像將它們寫成一個物件字面量型別一樣。在衝突的情況下,交叉和聯合是遞迴的,所以 Conflicting.a: number & string

字面量型別 (Unit types)

字面量型別是原始型別的子型別,僅包含一個原始值。例如,字串 "foo" 具有型別 "foo"。由於 JavaScript 沒有內建列舉,通常會使用一組已知的字串代替。字串字面量型別的聯合允許 TypeScript 對這種模式進行型別檢查:

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

在需要時,編譯器會進行擴大 (widening) —— 將字面量型別轉換為原始超型別,例如將 "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

這裡,儘管在呼叫之前 TU 尚未被推斷,但在此示例中 n: number。事實上,在 [1,2,3] 被用於推斷 T=number 後,n => n.toString() 的返回型別被用於推斷 U=string,導致 sns 具有 string[] 型別。

請注意,推斷可以以任何順序進行,但 Intellisense(智慧感知)只能從左到右工作,因此 TypeScript 更傾向於先宣告帶有陣列的 map

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

上下文型別也透過物件字面量遞迴工作,並且適用於否則會被推斷為 stringnumber 的字面量型別。它還可以從上下文中推斷返回型別。

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 的返回型別被推斷為 number,因為 TypeScript 知道該函式是完全的 (total)。如果某些變體未被覆蓋,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;請注意,它只被引用了一次,所以它沒有被用來約束返回值或其他引數的型別。

高階型別

TypeScript 沒有高階型別,因此以下寫法是不合法的:

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

Point-free 程式設計

Point-free 程式設計(大量使用柯里化和函式組合)在 JavaScript 中是可能的,但可能會很冗長。在 TypeScript 中,型別推斷對於 point-free 程式往往會失敗,因此您最終需要指定型別引數而不是值引數。結果過於冗長,通常最好避免使用 point-free 程式設計。

模組系統

JavaScript 的現代模組語法與 Haskell 的有點像,除了任何帶有 importexport 的檔案都被隱式視為一個模組。

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<T>,使所有屬性變為 readonly

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 文件是一個開源專案。請提交 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
OTOrta Therox (15)
UJUdoh Jeremiah (1)
MFMartin Fischer (1)
JRSDSJonas Raoni Soares da Silva (1)
RCRyan Cavanaugh (1)
11+

最後更新:2026 年 3 月 27 日