型別補充

本章為本系列 TypeScript 基礎教學的最後一篇,整理了一些 TypeScript 的特殊型別:

  • 元組 (Tuple)

  • 列舉 (Enum)

  • Never

  • Unknown

元組

元組讓我們能夠對固定長度的陣列的內容作型別指定,陣列中每個元素的型別可以是不同的:

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK

或是我們也可以分開來賦值:

x[0] = "hello";
x[1] = 10;

相對的,若我們將 hello10 的位置對調,便會發生錯誤:

// Initialize it incorrectly
x = [10, "hello"]; // Error

此外,當我們對元組中的元素賦值或訪問一個已知索引的元素時,會得到正確的型別:

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// OK
console.log(x[0].substring(1));

substring() 用於分割字串,舉例:

const str = 'Mozilla';

console.log(str.substring(1, 3));
// expected output: "oz"

console.log(str.substring(2));
// expected output: "zilla"

根據上面的程式碼可得知: 我們可以輸入 substring(起點,終點) 或是 substring(希望廢棄段落的終點)

-- MDN Web Docs

如果超出一開始宣告的範圍,會發生什麼情況呢?

let x: [string, number];
x = ["hello", 10];
x[2] = {};

如果新增超出一開始預定範圍的元素,需要注意新增元素的型別只能是元組中每個型別的聯合型別:

x[2] = {};// NOPE
x[2] = 12;// OK
x[2] = "Ian";// OK

列舉

列舉用於取值被限定在一定範圍內的地方,最常見的就像是一周有七天,一年有十二個月等等。

enum month {
    JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
}

不過需要注意的是:

定義在 enum 的成員會被賦值為從 0 開始遞增的數字,就像是:

console.log(month["JAN"] === 0); // true
console.log(month["FEB"] === 1); // true
console.log(month["JUL"] === 6); // true

相對的,列舉值與列舉名也是對映的:

console.log(0 === month["JAN"]); // true
  • 知識點: 相等 == 與全等 ===、自動轉型

    請參考本日的延伸閱讀。

根據上面的範例仔細觀察,會發現 JAN 對應到的是 0 而不是 1 ,該怎麼做呢?

enum month {
    JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC
}

我們可以對 enum 中的元素手動賦值,如此一來其他元素也會依循前面的元素遞增:

console.log(month["JAN"] === 1); // true
console.log(month["FEB"] === 2); // true
console.log(month["JUL"] === 7); // true

Never

Never 型別用於以下情況:

  • 沒有回傳值的函式,像是無窮迴圈。

  • 一天到晚出錯的函式

// Function returning never must not have a reachable end point
function error(message: string): never {
  throw new Error(message);
}

// Inferred return type is never
function fail() {
  return error("Something failed");
}

// Function returning never must not have a reachable end point
function infiniteLoop(): never {
  while (true) {}
}

根據第一項提到的: 沒有回傳值的函式,如果讀者有學好 JavaScript ,第一個會聯想到的應該是 void

不過, NeverVoid 還是有些不同:

Void 代表的是沒有回傳值的函式,而 Never 會用於表示本就不會有回傳值,或是會拋出錯誤的函式:

function errMsg(msg: string): never {
  throw new Error(msg);
}

就上面的程式碼來看,我們可以定義了一個負責發出錯誤的函式,這時候 Never 型別就派上用場了。

此外,被宣告為 Never 型別的變數僅能被賦值給另外一個 Never :

let value :never; 
value = 123;// NOPE
value = ((msg: string)=>{ throw new Error(msg); })('Ian')// OK

Unknown

UnknownAny 型別有一個共同點,就是: 所有類型都可以被歸類成 Unkown, Any

話雖如此, Unknown 還是有其特別之處,畢竟它的誕生就是為了解決 Any 型別不夠嚴謹的問題。

let value: unknown = 3;
value = "Ian";
value = undefined;
value = null;
value = true;

型別為 Unknown 的變數也能夠像型別 Any 的變數一樣被任意賦值。

至於與 Any 的差別到底在哪,就讓我們繼續往下看:

賦值限制

在 TypeScript 中,僅有 AnyUnknown 型別可以被存放任意值,作為更嚴謹的 Unknown 型別,就會有更嚴格的規範。

宣告為 Unknown 的變數僅能在被賦值為 Any 或是 Unknown 型別的變數:

let value: unknown;
let value1: unknown = value;   // OK
let value2: any = value;       // OK
let value3: number = value;   // Nope
let value4: boolean = value;    // Nope
let value5: string = value;    // Nope
// ...

筆者認為,這樣的規定是非常合理的,畢竟若 TypeScript 允許一個不確定的變數能夠被賦值給確定型別的變數,會產生很多執行上的錯誤。

看看 Any

限制操作

宣告為 Unknown 的變數無法進行任意操作,因為對於 TypeScript 來說,它會將該變數認定為內容不明確的變數。為了避免錯誤操作,所以不允許使用者任意操作 Unknown 型別的變數。

至於 Any 型別的變數,在被宣告的那一刻就被 TypeScript 當成放棄治療的孩子了,所以你想怎麼做都行:

let value: unknown;
let value2: any;
value.substring();
value2.substring();

錯誤訊息如下:

Object is of type 'unknown'.

總結

本章將一些之前沒有多加補充的型別一次補上,也正式將 TypeScript 基礎系列告一段落了,明天開始便會進行一系列的 Deno 入門與實戰,筆者真的是既期待又害怕趕不出文章來啊啊阿!

延伸閱讀

同樣的事情在不同人眼中可能會有不同的見解、看法。

在讀完本篇以後,筆者也強烈建議大家去看看以下文章,或許會對型別、變數宣告...等觀念有更深層的看法唷!

Last updated