日常類型

在本章節中,我們將介紹在 JavaScript 程式碼中會找到的一些最常見的類型,並說明在 TypeScript 中描述這些類型的對應方式。這並非詳盡無遺的清單,後續章節將說明更多命名和使用其他類型的途徑。

類型也可以出現在許多其他地方,而不仅仅是類型註解。當我們了解類型本身時,我們還將了解可以在其中參照這些類型以形成新結構的地方。

我們將從檢閱在撰寫 JavaScript 或 TypeScript 程式碼時可能會遇到的最基本和最常見的類型開始。這些類型稍後將形成更複雜類型的核心建構區塊。

基本類型:stringnumberboolean

JavaScript 有三個非常常用的基本類型stringnumberboolean。每個類型在 TypeScript 中都有對應的類型。正如您所預期的那樣,這些名稱與在這些類型的值上使用 JavaScript typeof 算子時所看到的相同

  • string 代表字串值,例如 "Hello, world"
  • number 則用於數字,例如 42。JavaScript 沒有針對整數的特殊執行時期值,因此沒有等同於 intfloat 的類型 - 所有類型都只是 number
  • boolean 則用於兩個值 truefalse

類型名稱 StringNumberBoolean(以大寫字母開頭)是合法的,但指的是一些特殊內建類型,在您的程式碼中很少會出現。永遠使用 stringnumberboolean 作為類型。

陣列

若要指定陣列的類型,例如 [1, 2, 3],可以使用語法 number[];此語法適用於任何類型(例如 string[] 是字串陣列,以此類推)。您也可能會看到寫成 Array<number>,其意思相同。我們將在介紹泛型時進一步瞭解 T<U> 語法。

請注意,[number] 是不同的東西;請參閱 元組部分。

any

TypeScript 也有特殊類型 any,當你不希望特定值導致類型檢查錯誤時,可以使用它。

當值為類型 any 時,你可以存取它的任何屬性(類型也會變成 any),像呼叫函式一樣呼叫它,將它指定給(或從)任何類型的值,或執行任何在語法上合法的操作。

ts
let obj: any = { x: 0 };
// None of the following lines of code will throw compiler errors.
// Using `any` disables all further type checking, and it is assumed
// you know the environment better than TypeScript.
obj.foo();
obj();
obj.bar = 100;
obj = "hello";
const n: number = obj;
Try

當你不想寫出長類型,只是為了讓 TypeScript 相信特定程式碼列沒問題時,any 類型會很有用。

noImplicitAny

當你沒有指定類型,而且 TypeScript 無法從內容推論時,編譯器通常會預設為 any

不過,你通常會想避免這樣做,因為 any 沒有經過類型檢查。使用編譯器標記 noImplicitAny 將任何隱含的 any 標記為錯誤。

變數上的類型註解

當你使用 constvarlet 宣告變數時,你可以選擇性地加入型別註解來明確指定變數的型別

ts
let myName: string = "Alice";
Try

TypeScript 不使用「左邊型別」風格的宣告,例如 int x = 0;。型別註解總是會在被型別化的東西之後

不過,在大部分情況下,這是不需要的。在可能的情況下,TypeScript 會嘗試自動推論你的程式碼中的型別。例如,變數的型別會根據其初始值的型別推論出來

ts
// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";
Try

在大部分情況下,你不需要明確地學習推論規則。如果你剛開始,請嘗試使用比你想像中更少的型別註解 - 你可能會驚訝於你只需要多麼少的註解,TypeScript 就能完全理解正在發生的事情。

函式

函式是 JavaScript 中傳遞資料的主要方式。TypeScript 允許你指定函式的輸入和輸出值的型別。

參數類型註解

宣告函數時,可以在每個參數後加上類型註解,以宣告函數接受的參數類型。參數類型註解會出現在參數名稱之後

ts
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
Try

當參數有類型註解時,會檢查傳遞給該函數的參數

ts
// Would be a runtime error if executed!
greet(42);
Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.
Try

即使參數沒有類型註解,TypeScript 仍會檢查是否傳遞了正確數量的參數。

傳回類型註解

您也可以加入傳回類型註解。傳回類型註解會出現在參數清單之後

ts
function getFavoriteNumber(): number {
return 26;
}
Try

就像變數類型註解一樣,通常不需要傳回類型註解,因為 TypeScript 會根據函數的 return 陳述式推斷函數的傳回類型。上述範例中的類型註解並不會改變任何內容。有些程式碼庫會明確指定傳回類型,以供文件記錄使用、防止意外變更,或只是出於個人偏好。

傳回 Promise 的函式

如果您要註解傳回 Promise 的函式傳回類型,您應該使用 Promise 類型

ts
async function getFavoriteNumber(): Promise<number> {
return 26;
}
Try

匿名函式

匿名函式與函式宣告有點不同。當函式出現在 TypeScript 可以判斷其呼叫方式的地方時,該函式的參數會自動賦予類型。

以下是一個範例

ts
const names = ["Alice", "Bob", "Eve"];
 
// Contextual typing for function - parameter s inferred to have type string
names.forEach(function (s) {
console.log(s.toUpperCase());
});
 
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUpperCase());
});
Try

即使參數 s 沒有類型註解,TypeScript 仍使用 forEach 函式的類型,以及陣列的推論類型,來判斷 s 會具有的類型。

這個程序稱為內容類型,因為函式出現的內容會告知其應具有的類型。

類似於推論規則,您不需要明確學習這如何發生,但了解它確實發生有助於您注意到何時不需要類型註解。稍後,我們將看到更多關於值出現的上下文如何影響其類型的範例。

物件類型

除了基本類型之外,您會遇到的最常見的類型是物件類型。這指的是任何具有屬性的 JavaScript 值,幾乎所有值都是如此!要定義物件類型,我們只需列出其屬性和類型即可。

例如,以下是使用類似點的物件的函式

ts
// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
Try

在此,我們使用具有兩個屬性的類型註解參數 - xy - 兩者都是 number 類型。您可以使用 ,; 來分隔屬性,最後的分隔符號是任選的。

每個屬性的類型部分也是任選的。如果您未指定類型,則假設為 any

選用屬性

物件類型也可以指定部分或全部的屬性為選用。要這樣做,請在屬性名稱後加上?

ts
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
Try

在 JavaScript 中,如果您存取不存在的屬性,您會取得值undefined,而不是執行時期錯誤。因此,當您從選用屬性讀取時,您必須在使用前檢查undefined

ts
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
'obj.last' is possibly 'undefined'.18048'obj.last' is possibly 'undefined'.
if (obj.last !== undefined) {
// OK
console.log(obj.last.toUpperCase());
}
 
// A safe alternative using modern JavaScript syntax:
console.log(obj.last?.toUpperCase());
}
Try

聯合類型

TypeScript 的類型系統讓您可以使用各種運算子,根據現有類型建立新的類型。現在我們知道如何撰寫一些類型,是時候開始以有趣的方式組合它們了。

定義聯合類型

您可能會看到的組合類型的第一種方式是聯合類型。聯合類型是由兩個或多個其他類型形成的類型,代表可能是任何一個這些類型的值。我們將這些類型中的每一個稱為聯合的成員

讓我們撰寫一個可以在字串或數字上運作的函數

ts
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.2345Argument of type '{ myID: number; }' is not assignable to parameter of type 'string | number'.
Try

使用聯合型別

提供一個與聯合型別相符的值很簡單,只要提供一個與聯合成員任何一個相符的型別即可。如果你一個聯合型別的值,你該如何使用它?

TypeScript 只會允許一個運算,如果它對聯合的每個成員都是有效的。例如,如果你有一個聯合 string | number,你不能使用只在 string 上可用的方法

ts
function printId(id: number | string) {
console.log(id.toUpperCase());
Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.2339Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
}
Try

解決方案是使用程式碼縮小聯合,就像你在沒有型別註解的情況下使用 JavaScript 一樣。當 TypeScript 可以根據程式碼結構為一個值推斷出一個更具體的型別時,就會發生縮小

例如,TypeScript 知道只有一個 string 值會有 typeof"string"

ts
function printId(id: number | string) {
if (typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
Try

另一個例子是使用像 Array.isArray 的函數

ts
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
Try

請注意,在 else 分支中,我們不需要做任何特別的事情,如果 x 不是 string[],那麼它一定是 string

有時你會有一個聯合,其中所有成員都有共同點。例如,陣列和字串都有 slice 方法。如果聯合中的每個成員都有共同的屬性,你可以在不縮小的情況下使用該屬性

ts
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
Try

類型聯集看似具有這些類型屬性的交集,這可能會令人困惑。這並非偶然 - 聯集的名稱源自類型理論。聯集 number | string 是透過取用各個類型的聯集而組成。請注意,給定兩個集合,其中包含關於每個集合的對應事實,只有這些事實的交集適用於集合本身的聯集。例如,如果我們有一個房間裡的人都很高且戴著帽子,而另一個房間裡的人會說西班牙語且戴著帽子,在合併這些房間後,我們唯一知道關於每個人的事情就是他們一定戴著帽子。

類型別名

我們一直透過在類型註解中直接撰寫物件類型和聯集類型來使用它們。這很方便,但通常會想要多次使用相同的類型,並以單一名稱來指稱它。

類型別名正是如此 - 任何類型名稱。類型別名的語法為

ts
type Point = {
x: number;
y: number;
};
 
// Exactly the same as the earlier example
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });
Try

您實際上可以使用類型別名為任何類型命名,而不僅僅是物件類型。例如,類型別名可以命名聯合類型

ts
type ID = number | string;
Try

請注意,別名為別名 - 您不能使用類型別名來建立相同類型的不同/相異「版本」。當您使用別名時,它就像您寫入別名類型一樣。換句話說,這段程式碼可能看起來是非法的,但根據 TypeScript 是可以的,因為這兩種類型都是相同類型的別名

ts
type UserInputSanitizedString = string;
 
function sanitizeInput(str: string): UserInputSanitizedString {
return sanitize(str);
}
 
// Create a sanitized input
let userInput = sanitizeInput(getInput());
 
// Can still be re-assigned with a string though
userInput = "new input";
Try

介面

介面宣告是命名物件類型的另一種方式

ts
interface Point {
x: number;
y: number;
}
 
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
 
printCoord({ x: 100, y: 100 });
Try

就像我們在上面使用類型別名一樣,範例就像我們使用匿名物件類型一樣運作。TypeScript 僅關注傳遞給 printCoord 的值的結構 - 它僅關注它具有預期的屬性。僅關注類型的結構和功能,這就是我們稱 TypeScript 為結構化類型類型系統的原因。

Type Aliases 與 Interfaces 之間的差異

Type aliases 與 interfaces 非常相似,而且在許多情況下,你可以自由選擇它們。interface 的幾乎所有功能都可以在 type 中使用,主要的區別在於,與始終可延伸的 interface 相比,type 無法重新開啟以新增新的屬性。

Interface Type

延伸 interface

interface Animal {
  name: string;
}
interface Bear extends Animal { honey: boolean; }
const bear = getBear(); bear.name; bear.honey;

透過交集延伸 type

type Animal = {
  name: string;
}
type Bear = Animal & { honey: boolean; }
const bear = getBear(); bear.name; bear.honey;

為現有的 interface 新增新的欄位

interface Window {
  title: string;
}
interface Window { ts: TypeScriptAPI; }
const src = 'const a = "Hello World"'; window.ts.transpileModule(src, {});

建立 type 之後就無法變更

type Window = {
  title: string;
}
type Window = { ts: TypeScriptAPI; }
// Error: Duplicate identifier 'Window'.

你會在後面的章節中進一步了解這些概念,所以如果你現在還無法理解所有這些概念,請不用擔心。

在大部分情況下,你可以根據個人喜好進行選擇,而 TypeScript 會告訴你是否需要將其作為另一種宣告類型。如果你想要一個啟發式方法,請使用 interface,直到你需要使用 type 中的功能。

類型斷言

有時,您會擁有 TypeScript 無法得知的關於值類型的資訊。

例如,如果您使用 document.getElementById,TypeScript 只知道這會傳回 某種 HTMLElement,但您可能知道您的網頁會永遠有一個具有給定 ID 的 HTMLCanvasElement

在這種情況下,您可以使用 類型斷言 來指定更具體的類型

ts
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
Try

類型斷言就像類型註解,會由編譯器移除,並且不會影響您的程式碼的執行時期行為。

您也可以使用尖括號語法(除非程式碼在 .tsx 檔案中),這兩個語法是等效的

ts
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
Try

提醒:因為類型斷言會在編譯時期移除,所以不會有與類型斷言相關聯的執行時期檢查。如果類型斷言錯誤,不會產生例外或 null

TypeScript 只允許轉換為 更具體較不具體 版本的類型的類型斷言。此規則可防止「不可能」的強制轉換,例如

ts
const x = "hello" as number;
Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.2352Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Try

有時,此規則可能過於保守,並且會禁止可能有效的更複雜強制轉換。如果發生這種情況,您可以使用兩個斷言,首先轉換為 any(或 unknown,我們稍後會介紹),然後轉換為所需的類型

ts
const a = expr as any as T;
Try

字面值類型

除了通用的類型 stringnumber,我們可以在類型位置中參考特定的字串和數字。

思考這一點的一種方式是考慮 JavaScript 如何提供不同的方式來宣告變數。varlet 都允許變更變數中儲存的內容,而 const 則不允許。這反映在 TypeScript 如何為字面值建立類型的方式中。

ts
let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
changingString;
let changingString: string
 
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
constantString;
const constantString: "Hello World"
Try

字面值類型本身並不是很值錢

ts
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.2322Type '"howdy"' is not assignable to type '"hello"'.
Try

只有一個值的變數沒有什麼用處!

但透過將字面值組合成聯集,你可以表達一個更有用的概念 - 例如,只接受特定已知值集合的函式

ts
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
printText("G'day, mate", "centre");
Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.2345Argument of type '"centre"' is not assignable to parameter of type '"left" | "right" | "center"'.
Try

數值字面值類型的工作方式相同

ts
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
Try

當然,你可以將這些與非字面值類型結合

ts
interface Options {
width: number;
}
function configure(x: Options | "auto") {
// ...
}
configure({ width: 100 });
configure("auto");
configure("automatic");
Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.2345Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.
Try

還有一種字面值類型:布林字面值。只有兩種布林字面值類型,正如你可能猜到的,它們是類型 truefalse。類型 boolean 本身實際上只是聯集 true | false 的別名。

字面推論

當您使用物件初始化變數時,TypeScript 會假設該物件的屬性可能會在稍後變更值。例如,如果您撰寫類似以下的程式碼

ts
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
Try

TypeScript 不會假設將 1 指定給先前具有 0 的欄位會產生錯誤。另一種說法是,obj.counter 的型別必須是 number,而不是 0,因為型別用於決定讀取和寫入行為。

字串也適用相同規則

ts
declare function handleRequest(url: string, method: "GET" | "POST"): void;
 
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.2345Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
Try

在上述範例中,req.method 推論為 string,而不是 "GET"。由於可以在建立 req 和呼叫 handleRequest 之間評估程式碼,而這可能會將新的字串 (例如 "GUESS") 指定給 req.method,因此 TypeScript 會將此程式碼視為有錯誤。

有兩種方法可以解決這個問題。

  1. 您可以透過在任一位置新增型別斷言來變更推論

    ts
    // Change 1:
    const req = { url: "https://example.com", method: "GET" as "GET" };
    // Change 2
    handleRequest(req.url, req.method as "GET");
    Try

    變更 1 表示「我打算讓 req.method 永遠具有字面類型 "GET"」,防止之後將 "GUESS" 指派給該欄位。變更 2 表示「我透過其他原因得知 req.method 的值為 "GET"」。

  2. 你可以使用 as const 將整個物件轉換為字面類型

    ts
    const req = { url: "https://example.com", method: "GET" } as const;
    handleRequest(req.url, req.method);
    Try

as const 後綴就像 const,但適用於類型系統,確保所有屬性都指派字面類型,而不是更通用的版本,例如 stringnumber

nullundefined

JavaScript 有兩個原始值用於表示不存在或未初始化的值:nullundefined

TypeScript 有兩個對應的類型,名稱相同。這些類型的行為取決於你是否啟用 strictNullChecks 選項。

strictNullChecks 已關閉

strictNullChecks 關閉 時,可能為 nullundefined 的值仍可正常存取,而值 nullundefined 可指派給任何類型的屬性。這類似於沒有空值檢查的語言(例如 C#、Java)的行為。缺乏對這些值的檢查往往是錯誤的主要來源;我們強烈建議人們在實務上可行的情況下開啟 strictNullChecks

strictNullChecks 開啟

strictNullChecks 開啟 時,如果值為 nullundefined,您需要在對該值使用函式或屬性之前測試這些值。就像在使用選用屬性之前檢查 undefined 一樣,我們可以使用縮小範圍檢查可能為 null 的值

ts
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
Try

非空斷言運算子(後綴 !

TypeScript 也有特殊語法,用於從類型中移除 nullundefined,而不進行任何明確檢查。在任何表達式後寫入 ! 實際上是一種類型斷言,表示該值不是 nullundefined

ts
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
Try

就像其他類型斷言一樣,這不會變更程式碼的執行時期行為,因此只有在您知道值無法nullundefined 時才使用 ! 非常重要。

列舉

列舉是 TypeScript 新增到 JavaScript 的功能,用於描述一個值,該值可能是命名常數中的一組可能值之一。與大多數 TypeScript 功能不同,這不是 JavaScript 的類型層級新增功能,而是新增到語言和執行時期的功能。因此,這是您應該知道存在的功能,但除非您確定,否則可能暫緩使用。您可以在 列舉參考頁面 中進一步了解列舉。

不常見的原語

值得一提的是,JavaScript 中的其他基本類型在類型系統中也有表示。不過我們在此不會深入探討。

bigint

從 ES2020 開始,JavaScript 中有一個用於表示非常大的整數的基本類型 BigInt

ts
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
 
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
Try

您可以在 TypeScript 3.2 發行說明中了解有關 BigInt 的更多資訊。

symbol

JavaScript 中有一個用於透過函數 Symbol() 建立全域唯一參考的基本類型

ts
const firstName = Symbol("name");
const secondName = Symbol("name");
 
if (firstName === secondName) {
This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.2367This comparison appears to be unintentional because the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
Try

您可以在 符號參考頁面 中了解有關它們的更多資訊。

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

此頁面的貢獻者
RCRyan Cavanaugh (56)
OTOrta Therox (22)
UGUtku Gultopu (3)
ABAndrew Branch (2)
DRDaniel Rosenwasser (2)
29+

最後更新:2024 年 3 月 21 日