undefined
和 null
不包含在類型中undefined|T
本章說明 TypeScript 的基本概念。
閱讀完本章後,您應該能夠了解以下 TypeScript 程式碼
<T> {
interface Arrayconcat(...items: Array<T[] | T>): T[];
reduce<U>(
: (state: U, element: T, index: number, array: T[]) => U,
callback?: U
firstState: U;
)// ···
}
您可能會覺得這很神秘。我同意您的看法!但(我希望證明)這個語法相對容易學習。一旦您了解它,它就能立即、精確且全面地摘要程式碼的行為,而無需閱讀冗長的英文說明。
有許多方法可以設定 TypeScript 編譯器。其中一組重要的選項控制編譯器徹底檢查 TypeScript 程式碼的方式。最高設定值是透過 --strict
啟用,我建議總是使用它。它讓程式變得稍微難寫,但我們也能獲得靜態型別檢查的全部好處。
關於
--strict
,您現在只需要知道這些
如果您想進一步了解,請繼續閱讀。
將 --strict
設定為 true
,會將以下所有選項設定為 true
--noImplicitAny
:如果 TypeScript 無法推斷出型別,我們必須指定它。這主要適用於函式和方法的參數:有了這個設定,我們必須為它們加上註解。--noImplicitThis
:如果 this
的型別不明確,則會抱怨。--alwaysStrict
:盡可能使用 JavaScript 的嚴格模式。--strictNullChecks
:null
不是任何型別的一部分(除了它自己的型別 null
),如果它是可以接受的值,則必須明確提及。--strictFunctionTypes
:啟用函式型別的更嚴格檢查。--strictPropertyInitialization
:類別定義中的屬性必須初始化,除非它們可以具有 undefined
值。我們將在本書後面介紹更多編譯器選項,屆時我們將使用 TypeScript 建立 npm 套件 和 網路應用程式。TypeScript 手冊有關於它們的 完整文件。
在本章中,型別只是一個值集合。JavaScript 語言(非 TypeScript!)只有八種型別
undefined
的集合null
的集合false
和 true
的集合所有這些型別都是動態的:我們可以在執行階段使用它們。
TypeScript 為 JavaScript 帶來額外一層:靜態型別。這些只存在於編譯或類型檢查原始碼時。每個儲存位置(變數、屬性等)都有預測其動態值的靜態型別。類型檢查確保這些預測成真。
而且有很多東西可以在靜態下(不執行程式碼)檢查。例如,如果函式 toString(num)
的參數 num
有靜態型別 number
,則函式呼叫 toString('abc')
是非法的,因為參數 'abc'
有錯誤的靜態型別。
function toString(num: number): string {
String(num);
return }
在先前的函式宣告中有兩個類型註解
num
:冒號後接 number
toString()
的結果:冒號後接 string
number
和 string
都是指定儲存位置型別的類型表達式。
通常,如果沒有類型註解,TypeScript 可以推論靜態型別。例如,如果我們省略 toString()
的回傳型別,TypeScript 會推論它是 string
// %inferred-type: (num: number) => string
function toString(num: number) {
String(num);
return }
類型推論並非猜測:它遵循明確的規則(類似於算術),用於推導未明確指定類型的類型。在此情況下,回傳陳述套用函式 String()
,將任意值對應到字串,到類型為 number
的值 num
,並回傳結果。這就是推論回傳類型為 string
的原因。
如果位置的類型既未明確指定也無法推論,TypeScript 會對其使用類型 any
。這是所有值的類型,也是萬用字元,表示如果值具有該類型,我們可以執行任何操作。
使用 --strict
時,僅當我們明確使用 any
時才允許使用。換句話說:每個位置都必須具有明確或推論的靜態類型。在以下範例中,參數 num
既沒有明確類型也沒有推論類型,因此我們會收到編譯時期錯誤
// @ts-expect-error: Parameter 'num' implicitly has an 'any' type. (7006)
function toString(num) {
String(num);
return }
類型註解冒號後的類型表達式從簡單到複雜,並以以下方式建立。
基本類型是有效的類型表達式
undefined
、null
boolean
、number
、bigint
、string
symbol
object
.Array
(在 JavaScript 中技術上並非類型)any
(所有值的類型)有許多方法可以組合基本類型以產生新的複合類型。例如,透過類型運算子,其組合類型的方式類似於集合運算子聯集 (∪
) 和交集 (∩
) 組合集合的方式。我們很快就會看到如何執行此操作。
TypeScript 有兩種語言層級
我們可以在語法中看到這兩個層級
: undefined = undefined; const undef
在動態層級,我們使用 JavaScript 宣告變數 undef
,並將其初始化為值 undefined
。
在靜態層級,我們使用 TypeScript 指定變數 undef
具有靜態類型 undefined
。
請注意,相同的語法 undefined
表示不同的意義,具體取決於是在動態層級還是靜態層級使用。
嘗試培養對兩種語言層級的認識
這有助於理解 TypeScript。
使用 type
,我們可以為現有類型建立一個新名稱(別名)
type Age = number;
: Age = 82; const age
陣列在 JavaScript 中扮演兩個角色(其中之一或同時扮演兩個角色)
有兩種方法可以表達陣列 arr
用作清單,其中所有元素都是數字
: number[] = [];
let arr1: Array<number> = []; let arr2
通常,如果有一個賦值,TypeScript 可以推斷變數的類型。在這種情況下,我們實際上必須協助它,因為對於一個空的陣列,它無法確定元素的類型。
稍後我們將回到尖括號符號(Array<number>
)。
如果我們在陣列中儲存一個二維點,那麼我們將該陣列用作元組。如下所示
: [number, number] = [7, 5]; let point
對於陣列作為元組,需要類型註解,因為對於陣列文字,TypeScript 推斷的是清單類型,而不是元組類型
// %inferred-type: number[]
= [7, 5]; let point
元組的另一個範例是 Object.entries(obj)
的結果:一個陣列,其中包含 obj
的每個屬性的 [key, value] 對。
// %inferred-type: [string, number][]
= Object.entries({ a: 1, b: 2 });
const entries
.deepEqual(
assert,
entries'a', 1 ], [ 'b', 2 ]]); [[
推斷的類型是元組陣列。
這是函數類型的範例
: number) => string (num
此類型包含每個接受單一數字類型參數並傳回字串的函數。讓我們在類型註解中使用此類型
: (num: number) => string = // (A)
const toString: number) => String(num); // (B) (num
通常,我們必須為函數指定參數類型。但在這種情況下,B 行中 num
的類型可以從 A 行的函數類型推斷出來,我們可以省略它
: (num: number) => string =
const toString=> String(num); (num)
如果我們省略 toString
的類型註解,TypeScript 會從箭頭函數推斷一個類型
// %inferred-type: (num: number) => string
= (num: number) => String(num); const toString
這次,num
必須有類型註解。
以下範例比較複雜
function stringify123(callback: (num: number) => string) {
callback(123);
return }
我們使用函數類型來描述 stringify123()
的參數 callback
。由於這個類型註解,TypeScript 拒絕以下函數呼叫。
// @ts-expect-error: Argument of type 'NumberConstructor' is not
// assignable to parameter of type '(num: number) => string'.
// Type 'number' is not assignable to type 'string'.(2345)
stringify123(Number);
但它接受這個函數呼叫
.equal(
assertstringify123(String), '123');
TypeScript 通常可以推斷函數的傳回類型,但明確指定傳回類型是允許的,偶爾也很有用(至少不會造成任何危害)。
對於 stringify123()
,指定傳回類型是可選的,如下所示
function stringify123(callback: (num: number) => string): string {
callback(123);
return }
void
void
是函式的一種特殊回傳類型:它告訴 TypeScript 函式總是會回傳 undefined
。
它可以明確地這樣做
function f1(): void {
;
return undefined }
或者它可以隱含地這樣做
function f2(): void {}
不過,這種函式不能明確回傳 undefined
以外的值
function f3(): void {
// @ts-expect-error: Type '"abc"' is not assignable to type 'void'. (2322)
'abc';
return }
識別碼後面的問號表示參數是選用的。例如
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
= String;
callback
}callback(123); // (A)
return }
只有在確定 callback
不是 undefined
(如果省略參數,它就是 undefined
)時,TypeScript 才允許我們在 A 行呼叫函式。
TypeScript 支援 參數預設值
function createPoint(x=0, y=0): [number, number] {
, y];
return [x
}
.deepEqual(
assertcreatePoint(),
0, 0]);
[.deepEqual(
assertcreatePoint(1, 2),
1, 2]); [
預設值讓參數變成選用的。我們通常可以省略類型註解,因為 TypeScript 可以推論出類型。例如,它可以推論出 x
和 y
都是 number
類型。
如果我們想加入類型註解,會如下所示。
function createPoint(x:number = 0, y:number = 0): [number, number] {
, y];
return [x }
我們也可以在 TypeScript 參數定義中使用 剩餘參數。它們的靜態類型必須是陣列(清單或元組)
function joinNumbers(...nums: number[]): string {
.join('-');
return nums
}.equal(
assertjoinNumbers(1, 2, 3),
'1-2-3');
變數所儲存的值(一次一個值)可能是不同類型的成員。在這種情況下,我們需要一個聯合類型。例如,在以下程式碼中,stringOrNumber
是 string
類型或 number
類型
function getScore(stringOrNumber: string|number): number {
if (typeof stringOrNumber === 'string'
&& /^\*{1,5}$/.test(stringOrNumber)) {
return stringOrNumber.length;
else if (typeof stringOrNumber === 'number'
} && stringOrNumber >= 1 && stringOrNumber <= 5) {
return stringOrNumber
else {
} throw new Error('Illegal value: ' + JSON.stringify(stringOrNumber));
}
}
.equal(getScore('*****'), 5);
assert.equal(getScore(3), 3); assert
stringOrNumber
的類型是 string|number
。類型表達式 s|t
的結果是類型 s
和 t
的集合論聯集(解釋為集合)。
undefined
和 null
不包含在類型中在許多程式語言中,null
是所有物件類型的部分。例如,只要變數的類型在 Java 中是 String
,我們就可以將它設定為 null
,而 Java 也不會抱怨。
相反地,在 TypeScript 中,undefined
和 null
由不同的不相交類型處理。如果我們想允許它們,我們需要聯合類型,例如 undefined|string
和 null|string
: null|number = null;
let maybeNumber= 123; maybeNumber
否則,我們會收到錯誤
// @ts-expect-error: Type 'null' is not assignable to type 'number'. (2322)
: number = null;
let maybeNumber= 123; maybeNumber
請注意,TypeScript 沒有強迫我們立即初始化(只要我們在初始化變數之前沒有從變數中讀取資料即可)
: number; // OK
let myNumber= 123; myNumber
回想一下這個先前的函式
function stringify123(callback?: (num: number) => string) {
if (callback === undefined) {
= String;
callback
}callback(123); // (A)
return }
讓我們重新撰寫 stringify123()
,讓參數 callback
不再是選項:如果呼叫者不想要提供函式,他們必須明確傳遞 null
。結果如下所示。
function stringify123(
: null | ((num: number) => string)) {
callback= 123;
const num if (callback === null) { // (A)
= String;
callback
}callback(num); // (B)
return
}
.equal(
assertstringify123(null),
'123');
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
.throws(() => stringify123()); assert
我們必須再次處理 callback
不是函式的情況(A 行),才能在 B 行中進行函式呼叫。如果我們沒有這樣做,TypeScript 會在該行中報告錯誤。
undefined|T
以下三個參數宣告相當類似
x?: number
x = 456
x: undefined | number
如果參數為選擇性,則可以省略。在這種情況下,它的值為 undefined
function f1(x?: number) { return x }
.equal(f1(123), 123); // OK
assert.equal(f1(undefined), undefined); // OK
assert.equal(f1(), undefined); // can omit assert
如果參數具有預設值,則在參數被省略或設定為 undefined
時,會使用該值
function f2(x = 456) { return x }
.equal(f2(123), 123); // OK
assert.equal(f2(undefined), 456); // OK
assert.equal(f2(), 456); // can omit assert
如果參數具有聯集類型,則無法省略,但我們可以將其設定為 undefined
function f3(x: undefined | number) { return x }
.equal(f3(123), 123); // OK
assert.equal(f3(undefined), undefined); // OK
assert
// @ts-expect-error: Expected 1 arguments, but got 0. (2554)
f3(); // can’t omit
與陣列類似,物件在 JavaScript 中扮演兩個角色(偶爾會混淆)
記錄:在開發時間已知的固定數量屬性。每個屬性可以有不同的類型。
字典:任意數量的屬性,其名稱在開發時間未知。所有屬性具有相同的類型。
我們在本章中忽略物件作為字典——它們在 §15.4.5「索引簽章:物件作為字典」 中有說明。順帶一提,Map 通常是字典的更好選擇。
介面描述物件作為記錄。例如
interface Point {: number;
x: number;
y }
我們也可以透過逗號分隔成員
interface Point {: number,
x: number,
y }
TypeScript 型別系統的一大優點是它以結構方式運作,而不是名目方式。也就是說,介面 Point
符合所有具有適當結構的物件
interface Point {: number;
x: number;
y
}function pointToString(pt: Point) {
return `(${pt.x}, ${pt.y})`;
}
.equal(
assertpointToString({x: 5, y: 7}), // compatible structure
'(5, 7)');
相反地,在 Java 的名目型別系統中,我們必須明確宣告每個類別實作哪些介面。因此,類別只能實作在其建立時間存在的介面。
物件文字型別是匿名介面
type Point = {
: number;
x: number;
y; }
物件文字型別的一個好處是它們可以在內嵌中使用
function pointToString(pt: {x: number, y: number}) {
return `(${pt.x}, ${pt.y})`;
}
如果可以省略屬性,我們會在其名稱後加上問號
interface Person {: string;
name?: string;
company }
在以下範例中,john
和 jane
都符合介面 Person
: Person = {
const john: 'John',
name;
}: Person = {
const jane: 'Jane',
name: 'Massive Dynamic',
company; }
介面也可以包含方法
interface Point {: number;
x: number;
ydistance(other: Point): number;
}
就 TypeScript 的類型系統而言,方法定義和值為函式的屬性是等效的
interface HasMethodDef {simpleMethod(flag: boolean): void;
}
interface HasFuncProp {: (flag: boolean) => void;
simpleMethod
}
: HasMethodDef = {
const objWithMethodsimpleMethod(flag: boolean): void {},
;
}: HasFuncProp = objWithMethod;
const objWithMethod2
: HasMethodDef = {
const objWithOrdinaryFunction: function (flag: boolean): void {},
simpleMethod;
}: HasFuncProp = objWithOrdinaryFunction;
const objWithOrdinaryFunction2
: HasMethodDef = {
const objWithArrowFunction: (flag: boolean): void => {},
simpleMethod;
}: HasFuncProp = objWithArrowFunction; const objWithArrowFunction2
我的建議是使用最能表達如何設定屬性的語法。
類似地
常態函式存在於動態層級,是值的工廠,並有代表值的參數。參數宣告在括號中
= (x: number) => x; // definition
const valueFactory = valueFactory(123); // use const myValue
泛型類型存在於靜態層級,是類型的工廠,並有代表類型的參數。參數宣告在尖括號中
type TypeFactory<X> = X; // definition
type MyType = TypeFactory<string>; // use
命名類型參數
在 TypeScript 中,通常使用單一的大寫字元(例如 T
、I
和 O
)作為類型參數。不過,任何合法的 JavaScript 識別碼都允許使用,而且較長的命名通常能讓程式碼更容易理解。
// Factory for types
<Value> {
interface ValueContainer: Value;
value
}
// Creating one type
type StringContainer = ValueContainer<string>;
Value
是類型變數。可以在尖括號中引入一個或多個類型變數。
類別也可以有類型參數
<Elem> {
class SimpleStack: Array<Elem> = [];
#datapush(x: Elem): void {
.#data.push(x);
this
}pop(): Elem {
= this.#data.pop();
const result if (result === undefined) {
new Error();
throw
};
return result
}get length() {
.#data.length;
return this
} }
類別 SimpleStack
有類型參數 Elem
。當我們實例化類別時,我們也提供類型參數的值
= new SimpleStack<string>();
const stringStack .push('first');
stringStack.push('second');
stringStack.equal(stringStack.length, 2);
assert.equal(stringStack.pop(), 'second'); assert
Map 在 TypeScript 中是泛型型別。例如
: Map<boolean,string> = new Map([
const myMap, 'no'],
[false, 'yes'],
[true; ])
由於類型推論(基於 new Map()
的引數),我們可以省略類型參數
// %inferred-type: Map<boolean, string>
= new Map([
const myMap , 'no'],
[false, 'yes'],
[true; ])
函式定義可以像這樣引入類型變數
function identity<Arg>(arg: Arg): Arg {
;
return arg }
我們使用函式如下。
// %inferred-type: number
= identity<number>(123); const num1
由於類型推論,我們可以再次省略類型參數
// %inferred-type: 123
= identity(123); const num2
請注意,TypeScript 推論出類型 123
,這是一個只有一個數字的集合,比類型 number
更具體。
箭頭函式也可以有型別參數
= <Arg>(arg: Arg): Arg => arg; const identity
這是方法的型別參數語法
= {
const obj identity<Arg>(arg: Arg): Arg {
;
return arg,
}; }
function fillArray<T>(len: number, elem: T): T[] {
new Array<T>(len).fill(elem);
return }
型別變數 T
在此程式碼中出現四次
fillArray<T>
導入。因此,它的範圍是函式。elem
的型別註解中。fillArray()
的回傳型別。Array()
的型別引數。我們可以在呼叫 fillArray()
(第 A 行) 時省略型別參數,因為 TypeScript 可以從參數 elem
推斷出 T
// %inferred-type: string[]
= fillArray<string>(3, '*');
const arr1 .deepEqual(
assert, ['*', '*', '*']);
arr1
// %inferred-type: string[]
= fillArray(3, '*'); // (A) const arr2
讓我們使用所學到的知識來了解我們先前看過的一段程式碼
<T> {
interface Arrayconcat(...items: Array<T[] | T>): T[];
reduce<U>(
: (state: U, element: T, index: number, array: T[]) => U,
callback?: U
firstState: U;
)// ···
}
這是元素型別為 T
的陣列介面
方法 .concat()
有零個或多個參數 (透過 rest 參數定義)。每個參數的型別為 T[]|T
。也就是說,它可能是 T
值的陣列或單一的 T
值。
方法 .reduce()
導入它自己的型別變數 U
。U
用於表達以下實體全部具有相同型別的事實
callback()
的參數 state
callback()
的結果.reduce()
的選用參數 firstState
.reduce()
的結果除了 state
之外,callback()
還有以下參數
element
,它與陣列元素具有相同的型別 T
index
;一個數字T
的 array