函式程式設計師的 TypeScript

TypeScript 最初是為了將傳統的物件導向類型帶入 JavaScript,讓 Microsoft 的程式設計師可以將傳統的物件導向程式帶入網路。隨著發展,TypeScript 的類型系統已演變為建模原生 JavaScript 撰寫的程式碼。 resulting system is powerful, interesting and messy.

本簡介專為想要學習 TypeScript 的 Haskell 或 ML 程式設計師設計。它說明 TypeScript 的類型系統與 Haskell 的類型系統有何不同。它也說明 TypeScript 類型系統中因建模 JavaScript 程式碼而產生的獨特功能。

此簡介不涵蓋物件導向程式設計。實際上,TypeScript 中的物件導向程式與其他具備物件導向功能的熱門語言類似。

先備條件

在這個簡介中,我假設你知道下列事項

  • 如何使用 JavaScript 編寫程式,好處多多。
  • C 系語言的類型語法。

如果你需要學習 JavaScript 的好處,請閱讀 JavaScript:好處多多。如果你知道如何使用呼叫傳值、詞彙範圍語言撰寫程式,其中包含大量可變性,但其他部分不多,則可以略過此書。R4RS Scheme 是個好範例。

C++ 程式語言 是學習 C 式類型語法的理想場所。與 C++ 不同,TypeScript 使用後綴類型,如下所示:x: string,而非 string x

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 最底層類型。
物件字面 例如 { 屬性: 類型 }
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 中元組與清單無關。

boxed 類型

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

單位型別

單位型別是基本型別的子型別,只包含一個基本值。例如,字串 "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[]

請注意,推論會以任何順序運作,但 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 知道該函式是全域的。如果某些變體未涵蓋,則 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>) {}

無點程式設計

無點程式設計(大量使用柯里化和函數組合)在 JavaScript 中是可行的,但可能會很冗長。在 TypeScript 中,類型推論通常會對無點程式失敗,因此你最終會指定類型參數而不是值參數。結果非常冗長,因此通常最好避免無點程式設計。

模組系統

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-assertion,它作用於陣列和物件文字

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

但是,這些選項都不是預設值,因此它們不會一致地用於 TypeScript 程式碼中。

後續步驟

本文件是您在日常程式碼中會使用的語法和類型的概觀。從這裡開始,您應該

TypeScript 文件是一個開源專案。透過 傳送 Pull Request ❤ 協助我們改善這些頁面

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

最後更新:2024 年 3 月 21 日