TypeScript - Custom Types

學習資源:

Codecademy

Enums 枚舉

在做型別註釋的時候,比如使用 Tuples ,變數很多的時,型別註釋也會跟著很多。但大多時候很多都是重複的,所以這時枚舉就派上用場了。

enum Direction {
  North,
  South,
  East,
  West
}

在許多情況下,我們可能想要限制變數的可能值。 例如,上面的程式定義了枚舉方向,代表四個指南針方向:Direction.North, Direction.South, Direction.East, and Direction.West。而不允許使用任何其他值,如Direction.Southeast。 可以看看下面的範例:

let whichWayToArcticOcean: Direction;
whichWayToArcticOcean = Direction.North; // No type error.
whichWayToArcticOcean = Direction.Southeast; // Type error: Southeast is not a valid value for the Direction enum.
whichWayToArcticOcean = West; // Wrong syntax, we must use Direction.West instead.

如上所示,枚舉型別可以像任何其他型別一樣用於型別註釋。

TypeScript 使用數字處理這些枚舉型別。枚舉值會根據其列出的順序 assign 一個數值。第一個值被分配一個數字0,第二個值被分配一個數字1,依此類推......

比方說,若設定 whichWayToArticOcean = Direction.NorthwhichWayToArticOcean == 0 就會是 true。

但我們也可以改變這些數字:

enum Direction {
  North = 7,
  South,
  East,
  West
}

或是:

enum Direction {
  North = 8,
  South = 2,
  East = 6,
  West = 4
}

String Enums vs. Numeric Enums

枚舉除了基於數字外,也可以基於字串。但字串的部分就會需要明確的定義。

enum DirectionNumber { North, South, East, West }
enum DirectionString { North = 'NORTH', South = 'SOUTH', East = 'EAST', West = 'WEST' }

從技術上講,任何字串都可以:North = 'JabberWocky' 是一個有效的值。然而,比較好的寫法可以是(North = 'NORTH'),其中枚舉變數的字串值只是變數名稱的大寫形式。而這樣的方式,錯誤訊息和 logs 可以提供更多資訊。

這邊會建議盡量使用字串枚舉,因為數字枚舉會允許一些行為,這些行為可能會讓我們的程式產生意想不到的錯誤。 例如,數字可以直接 assign 給數值枚舉變數:

let whichWayToAntarctica: DirectionNumber;
whichWayToAntarctica = 1; // Valid TypeScript code.
whichWayToAntarctica = DirectionNumber.South; // Valid, equivalent to the above line.

奇怪的是,即使分配任意數字,如 whichWayToAntarctica = 943205,也不會導致型別錯誤。

字串枚舉就會嚴格許多。使用字串枚舉,變數根本無法 assign 給字串!

let whichWayToAntarctica: DirectionString;
whichWayToAntarctica = '\ (•◡•) / Arbitrary String \ (•◡•) /'; // Type error!
whichWayToAntarctica = 'SOUTH'; // STILL a type error!
whichWayToAntarctica = DirectionString.South; // The only allowable way to do this.

Object Types 物件型別

以下示範 Object 的型別註釋:

let aPerson: {name: string, age: number};

可以注意到 Object 的型別註釋出現在屬性後面。因為變數aPerson尚未被 assign 值。嘗試將值 assign 給沒有指定型別的 name 和 age 屬性的話將導致型別錯誤:

aPerson = {name: 'Aisle Nevertell', age: "wouldn't you like to know"}; // Type error: age property has the wrong type.
aPerson = {name: 'Kushim', yearsOld: 5000}; // Type error: no age property. 
aPerson = {name: 'User McCodecad', age: 22}; // Valid code.

這邊也要注意,像在 Kushim 的情況下,儘管該物件具有正確型別的屬性。但因為沒有正確的 key 值,所以會產生錯誤。

並且 TypeScript 對 Object 屬性的型別沒有限制。它們可以是枚舉、陣列,甚至其他物件型別!

let aCompany: {
  companyName: string, 
  boss: {name: string, age: number}, 
  employees: {name: string, age: number}[], 
  employeeOfTheMonth: {name: string, age: number},  
  moneyEarned: number
};

Type Aliases 型別別名

在程式中自定義型別的其中一個方法是使用型別別名。多數是為了方便而選擇的替代型別名稱。以 type <alias name> = <type> 的格式表示。

type MyString = string;
let myVar: MyString = 'Hi'; // Valid code.

型別別名對於引用重複的複雜型別非常有用,特別是物件型別和元組型別。

let aCompany: { 
  companyName: string, 
  boss: { name: string, age: number }, 
  employees: { name: string, age: number }[], 
  employeeOfTheMonth: { name: string, age: number },  
  moneyEarned: number
};

這裡有很多不必要的重複! 而這時就可以用型別別名來處理:

type Person = { name: string, age: number };
let aCompany: {
  companyName: string, 
  boss: Person, 
  employees: Person[], 
  employeeOfTheMonth: Person,  
  moneyEarned: number
};

TypeScript 別名只不過是名稱。他們對型別的運作方式沒有影響。例如,以下程式不會發生型別錯誤:

type MyString = string; 
type MyOtherString = string;
let firstString: MyString = 'test';
let secondString: MyOtherString = firstString; // Valid code.

Function Types

JavaScript 的一個巧妙之處在於,函式是可以 assign 給變數的。

let myFavoriteFunction = console.log; // Note the lack of parentheses.
myFavoriteFunction('Hello World'); // Prints: Hello World

TypeScript 的一個巧妙之處在於,我們可以精確地控制可 assign 給變數的函式種類。可以使用函式型別來做這件事,這些型別指定了函式的引數型別和返回型別。以下是一個函式型別的範例,它只與接受兩個字串引數並返回一個數字的函式。

type StringsToNumberFunction = (arg0: string, arg1: string) => number;

這種語法就像箭頭函式一樣,只是我們放了返回型別而不是返回值。而在這裡,返回型別是數字。因為這只是一個型別,所以我們根本沒有編寫函式本身。StringsToNumberFunction 型別的變數可以 assign 任何函式:

let myFunc: StringsToNumberFunction;
myFunc = function(firstName: string, lastName: string) {
  return firstName.length + lastName.length;
};

myFunc = function(whatever: string, blah: string) {
  return whatever.length - blah.length;
};
// Neither of these assignments results in a type error.

正如我們上面所看到的,只要它們有正確的型別(字串和字串),我們給函式引數取什麼名字並不重要。 因此,我們在型別註釋中怎麼命名引數並不重要(上圖,我們選擇了 arg0arg1)。

這裡有一些要注意的事,那就是我們絕不能在函式型別註釋中省略引數名稱或引數周圍的括號,即使只有一個引數。此程式將無法執行!

type StringToNumberFunction = (string)=>number; // NO
type StringToNumberFunction = arg: string=>number; // NO NO NO NO

Generic Types 泛型

TypeScript 的泛型是建立具有某些相似性的型別(型別函式等等)集合的方法。這些集合由一個或多個型別變數引數化。

我們已經看過一個使用泛型的範例。記得之前陣列型別的語法Array<T> 嗎? 這是通用的,因為我們可以在 T 中替換任何型別(預定義或自定義)。例如,Array<string> 是一個字串陣列。

泛型賦予我們定義自己 object 型別集合的能力。這裡舉一個例子:

type Family<T> = {
  parents: [T, T], mate: T, children: T[]
};

上面的範例定義了 object 型別的集合,T值都可以是任意型別。所以必須用我們選擇的某種型別替換T,例如字串。然後,Family與將T設定為string 時給出的物件型別完全相同:{parents:[string,string], mate:string, children: string[]}。因此,以下範例就不會有錯誤:

let aStringFamily: Family<string> = {
  parents: ['stern string', 'nice string'],
  mate: 'string next door', 
  children: ['stringy', 'stringo', 'stringina', 'stringolio']
};

使用型別 typeName<T> 泛型型別允許我們在型別註釋中使用 T 作為型別 placeholder。所以當使用泛型時,T 被替換為提供的型別。 (寫 T 只是一個慣例。 我們可以同樣輕鬆地使用S或GenericType。)

Generic Functions 泛型函式

我們還可以使用泛型來建立型別函式的集合。讓我們先用 JavaScript 表示:

function getFilledArray(value, n) {
  return Array(n).fill(value);
}

上面的範例,若呼叫 getFilledArray('cheese', 3) ,就會產生 ['cheese', 'cheese', 'cheese’] 。但這邊就有一個問題,就是我們會需要幫每一個 value 都做型別註釋嗎?不用,這邊就可以使用泛型函式。

function getFilledArray<T>(value: T, n: number): T[] {
  return Array(n).fill(value);
}

以上的方法告訴 TypeScript 讓value 以及返回陣列都有同樣的型別 T