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 |
true 和 false 。 |
Symbol |
通常用作鍵的唯一值。 |
Null |
等同於單位類型。 |
Undefined |
也等同於單位類型。 |
Object |
類似於記錄。 |
TypeScript 為內建類型提供了對應的基本類型
number
string
bigint
boolean
symbol
null
undefined
object
其他重要的 TypeScript 類型
類型 | 說明 |
---|---|
unknown |
最頂層類型。 |
never |
最底層類型。 |
物件字面 | 例如 { 屬性: 類型 } |
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 中元組與清單無關。
boxed 類型
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" |
function | typeof f === "function" |
array | Array.isArray(a) |
object | typeof o === "object" |
請注意,函式和陣列在執行階段是物件,但有自己的謂詞。
交集
除了聯集之外,TypeScript 也有交集
tsTry
typeCombined = {a : number } & {b : string };typeConflicting = {a : number } & {a : string };
Combined
有兩個屬性,a
和 b
,就像它們寫成一個物件文字型別一樣。交集和聯集在發生衝突時會遞迴,因此 Conflicting.a: number & string
。
單位型別
單位型別是基本型別的子型別,只包含一個基本值。例如,字串 "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[]
。
請注意,推論會以任何順序運作,但 IntelliSense 只會由左至右運作,因此 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
的回傳類型推斷為 number
,因為 TypeScript 知道該函式是全域的。如果某些變體未涵蓋,則 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 不是必需的;請注意,它只被參照一次,因此它沒有用於限制回傳值或其他參數的類型。
高階類型
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<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); // errorb[0] = 101; // error
您也可以使用 const-assertion,它作用於陣列和物件文字
ts
let a = [1, 2, 3] as const;a.push(102); // errora[0] = 101; // error
但是,這些選項都不是預設值,因此它們不會一致地用於 TypeScript 程式碼中。
後續步驟
本文件是您在日常程式碼中會使用的語法和類型的概觀。從這裡開始,您應該
- 從頭到尾閱讀完整的指南
- 探索 遊樂場範例