# TypeScript note
###### tags: `typescript`
## TS for JS programmer
### 介紹
TS比起JS來說,TS有賦予型別的功能,可以大幅減少異常行為以及bug的發生。
### Types by Inference 型別推論
TS能夠自動理解JS並且產生型別
例如:建立一個變數並且賦值給他,TS將會把這個值當作其型別。
```typescript
let helloworkd = "Hello Word";
```
TS會將helloworld的型別定義為string。
>[型別推論探討](https://tecky.io/zh_Hant/blog/%e5%ad%b8%e8%a1%93%e6%8e%a2%e8%a8%8e%e7%b3%bb%e5%88%97-%e4%b8%80-%e5%9e%8b%e5%88%a5%e6%8e%a8%e8%ab%96-%e4%b8%80-type-inference-i/)
### Defining Types 定義型別
某些設計模式會導致型別無法自動推論,為了避免狀況發生,TS就是用來解決這問題,TS可以提供方法來定義型別該是如何。
**例如**:
可以使用interface來準確定義物件的介面
```typescript
interface User { --> User 為往後定義物件的基礎介面
name: string; --> name已經定義型別為string
id: number; --> id已經定義型別為number
}
```
使用定義好的物件介面來建立物件
```typescript
const user: User = { --> user這物件就是透過User介面來寫出來
name: "Hayes", --> 為string
id: 0, --> 為number
};
```
如果型別賦予錯誤將會報錯
```typescript
const user1: User = {
name: louis,
id: '0034' --> 誤將id定義為string
}
```

以及如果定義的key不在原始的介面之中也會報錯
```typescript
const user: User = {
userName: 'louis', --> 原始介面中為name,不為userName
id: 12
}
```

#### TS如同JS,可以寫class以及寫物件導向,所以你也可以用介面宣告(inrerface declaration)和classes一起。
例如:
```typescript
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);
```
##### 原生JS就有以下幾種基礎型別:boolean, bigint, null, number, string, symbol, and undefined,在TS中又新增幾種如下:any (任何型別皆可), unknown (確保使用者使用為其定義的型別,且unknown不能賦值給除了any以及unknow以外的型別), never (絕不可能為的型別), and void (回傳值為undefined或是null的).
##### 有兩種可以定義介面跟型別的方式,最好使用interface,除非有特殊需求才使用type
### Union Types 聯合型別
聯合型別(Union Types)表示值可以為多種型別中的一種。
聯合型別使用 | 分隔每個型別
一種常使用的方式為定義為不同的值的組合
```typescript
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
```
也可以像如下一個函示可以接收string或是array
```typescript
function getLength(obj: string | string[]) {
return obj.length;
}
```
### Generics Types 泛型
常見使用方式為array,如果array沒有泛型則其可能包含任f何型別
```typescript
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;
```
### Structural Type System
TS其中一個核心原則為型別確認著重於其中值的shape,這方式又被稱為"duck typing" or "structural typing". 如果在這系統內,兩個物件有相同的shape,則將會被認為為同一種型別。
以下舉例:
```typescript
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// logs "12, 26"
const point = { x: 12, y: 26 };
logPoint(point);
```
以上的point從未被定義為Point型別,但TS比較了point跟Point的shape,是相同的,所以這段code是可以行得通的。
classes和objects同樣遵守了這個shape原則:
```typescript
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const newVPoint = new VirtualPoint(13, 56);
logPoint(newVPoint); // logs "13, 56"
```
---
# Everyday Types
這篇會涵蓋最常見的你會在JS中看到的幾種型別,並且解釋如何轉換成對照的TS。
### string、number、boolean
* string 就像"hello, world"
* number 就像 23,在JS中沒有特別分整數以及浮點數,所有數字都是number
* boolean就是true跟false這兩種
**大寫型別名稱如String、Number、Boolean是合法的,但極少情況才會用到,所以基本上都使用小寫string、number、boolean來定義型別即可。**
### Arrays
array的樣子如同[1, 2, 3],你可以使用這個語法 `number[]`,這語法也可適用於其他類型(e.g string[]),你可能也會看到這種寫法 `Array<number>`, 這是一樣的,但這語法`T<U>`會留到泛型時多做說明。
### `any`
TS中有這這個特殊的型別,當你不想造成任何typechekcing errors時,可以用這個,當值的型別為`any`時,你可以訪問他的任何屬性,把他當成funciton呼叫,會將其賦值或賦值於其他,基本上任何事情都是合法的。
```typescript
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;
```
#### `noImplicitAny`
當你未將一個值賦予型別,TS則無法認知其型別,則會預設認定為any
如果你想避免這種事情,可以使用compiler flag noImplicitAny,將會把任何為any型別的標註成error。
### Type Annotations on Varibles 變數型別註解
當你使用 `const` `let` `var`來宣告變數時,你可以使用變數註解的方式來特別標注此變數的型別。
```typescript
let myName: string = "Alice";
```
:::info
TS的型別註解永遠在你要註解的變數後面。
:::
在多數情況,型別註解或許是不太需要的,因為TS有自動型別推斷的能力。如下:
```typescript
// No type annotation needed -- 'myName' inferred as type 'string'
let myName = "Alice";
```
在大部分的狀況下不需要特別學推斷型別的規則,如果你是TS新手,可以試著少用點型別註解的方式,你也許會對於TS如何理解你的code感到驚訝。
### Function
Funcitons 是JS中主要傳遞資料的方式,TS讓你可以明確定義進出函式的值的型別。
#### Parameter Type Annotations 參數型別註解
當你宣告註解時,你可以加上參數型別註解在每個參數後面,如上所說,參數註解也都在每個參數的後面,下面範例:
```typescript
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
```
當參數有型別註解時,參數會被型別確認如下:
```typescript
// Would be a runtime error if executed!
greet(42); --> 此參數應傳遞一個string,但裡面卻傳了一個number,所以報錯。
Argument of type 'number' is not assignable to parameter of type 'string'.
```
#### Return Type Annotations 回傳值型別註解
你也可以加上回傳值的型別註解,回傳值型別註解會被寫在參數列表後,下面範例:
```typescript
function getFavoriteNumber(): number {
return 26;
}
```
有點像變數型別註解一樣,TS會自動推論函式的回傳值型別,有些代碼庫會這樣寫只是為了文件上的需求來避免意外發生,或是只是個人偏好。
#### Anonymous Functions 匿名函示
匿名函示跟函式宣告有些許不同,當一個函式出現在TS能知道他如何被呼叫的地方,此函式的參數將會自動被定義型別。 下面範例:
```typescript
// No type annotations here, but TypeScript can spot the bug
const names = ["Alice", "Bob", "Eve"];
// Contextual typing for function --> 上下文推斷
names.forEach(function (s) {
console.log(s.toUppercase());
Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
// Contextual typing also applies to arrow functions
names.forEach((s) => {
console.log(s.toUppercase());
Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
});
```
雖然上面的參數s沒有被定義型別,TS利用了`forEach`function的型別以及陣列的推斷型別,來決定參數s會是什麼型別。 跟先前提到的自動型別推論的規則一樣,無需特別專研其中規則,只要知道說這確實會發生且認知道說何時不需要型別註解,後面會看到更多例子關於值的內容會如何影響其型別。
### Object Types 物件型別
與原始型別(primitives)不同,你最常會遇到的型別形式就是物件型別,這也參照到任何JS中任何值的屬性(基本上是所有東西),為要定義一個物件型別,簡單列出他們的屬性以及型別。
如下範例:
```typescript
// 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 });
```
這裡我們將參數內的兩個屬性註解了型別,你可以使用`,`或是`;`來分開屬性
如果你為註解型別,則TS將認為是any型別。
#### Optional Properties 選擇性的屬性
物件型別可以指定一些或全部的屬性為選擇性的,加上`?`,在屬性名稱後面,如下:
```typescript
function printName(obj: { first: string; last?: string }) {
// ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });
```
在JS中如果你嘗試存取一個不存在的屬性,你會得到`undefined`這個值而不是 runtime error,因為這樣,所以當你存取一個選擇性的屬性時,在使用前要特別確認是不是`undefined`。
```typescript
function printName(obj: { first: string; last?: string }) {
// Error - might crash if 'obj.last' wasn't provided!
console.log(obj.last.toUpperCase());
Object 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());
}
```
### Union Types 聯合型別
TS型別系統可以讓你使用多種不同的運算符進而從現有類型中建立新的型別,前面已經認識了一些型別,現在開始要將其組合。
#### Defining a Union Type 定義一個聯合型別
第一種組合型別的方式就是聯合型別,聯合型別是兩個或多個以上的型別組成,代表這個值可以為任何其中一種型別。我們就將其稱作聯合型別。
如下範例:
```typescript
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'.
```
#### Working with Union Types 使用聯合型別
提供正確型別的值是相對簡單的,只要提供能符合union types其中一樣的型別即可,但如果你有了這個值你要如何在其中進行操作? TS只能讓你使用符合union types中每一項的屬性,例如:如果此union types是 `srting | number` 你就不能使用只有string才能用的方法,例如下:
```typescript!
function printId(id: number | string) {
console.log(id.toUpperCase());
Property 'toUpperCase' does not exist on type 'string | number'.
// toUpperCase並不屬於number型別能使用的,所以不能這樣用
Property 'toUpperCase' does not exist on type 'number'.
}
```
解決方式為藉由內部code方式縮小其使用方式,如下:
```typescript
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);
}
}
```
另外一個例子,如下:
```typescript
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);
}
}
```
**有時你會使用到有相同方法的型別,例如Array跟string都有`slice`這功能,所以你無需縮小範圍就可以正常使用**,如下:
```typescript
// Return type is inferred as number[] | string
function getFirstThree(x: number[] | string) {
return x.slice(0, 3);
}
```
### Type Aliases 型別別名
先前已經有使用過型別註解在物件型別以及聯合型別上,這是很方便,但更常方式是多次使用此型別且將其參照到某一個名稱上。
型別別名就是一個名稱包含任何型別在內,如下範例:
```typescript
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 });
```
你可以將type alias使用在任何型別上,不只有物件型別,例如如下聯合型別:
```typescript
type ID = number | string;
```
請記住別名就只是別名,你不能使用型別別名來創造同樣型別的不同版本,當使用別名時,就像是你寫了一個別名型別一樣,換句話說,下面這段code,看起來或許不合理,但根據TS,兩者別名為相同型別,所以是可以的。
```typescript
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";
```
### Interfaces 介面
介面宣告是另一種方式來定義一個物件型別
```typescript
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 });
```
就像我們前面使用的型別別名,就像我們使用了一個匿名的物件型別,TS只關注我們傳入`printCoord`的值的結構,只在乎他是不是有與其相符合的屬性型別,這就是為什麼我們稱呼TS是一種structurally typed type system.
#### Differences Between Type Aliases and Interfaces 型別別名以及介面的差異
### Type Assertions 型別斷言
有時候你會拿到一個值的資訊是TS不清楚的,例如,如果你用`document.getElementById`,TS只知道這會回傳一個`HTMLElement`但你可能預想回傳的是一個`HTMLCanvasElement`,這種情況你可以使用型別斷言來指定一個更準確的型別,如下範例:
```typescript
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
```
就像型別註解一樣,型別斷言會被編譯器移除且不會影響runtime。
你也可以使用尖角括號`< >`,是相同的(除了在`.tsx`的文件)
```typescript
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
```
### Literal Types 字面值型別
除了一般類型的字符串和數字外,我們還可以在類型位置引用特定的字符串和數字。
其中考慮到JS是如何宣告一個變數,`var` `let`都可以變換其他的內容值,而`const`不行,這也反映出TS如何創造字面值型別。
```typescript
let changingString = "Hello World";
changingString = "Olá Mundo";
// Because `changingString` can represent any possible string, that
// is how TypeScript describes it in the type system
let changingString: string
const constantString = "Hello World";
// Because `constantString` can only represent 1 possible string, it
// has a literal type representation
const constantString: "Hello World"
```
如果就其單獨使用,無法展現其字面值型別的價值:
```typescript
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
Type '"howdy"' is not assignable to type '"hello"'.
```
但如果是跟聯合型別一起使用,可以展現出更多有用的概念,如下範例,函式參數只接收特定幾個已知的參數:
```typescript
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"'.
```
數型別的也可以這樣:
```typescript
function compare(a: string, b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
```
另外,你也可以結合兩種非字面值型別,如下:
```typescript
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"'.
```
#### Literal Inference 字面型別推論
當你使用物件建立一個變數時,TS會猜測此物件的屬性未來會做變化,如下:
```typescript
const obj = { counter: 0 };
if (someCondition) {
obj.counter = 1;
}
```
TS不會將上面的0變成1認定為錯誤,而是將`obj.counter`一定是`number`型別並非0。
同樣也可以映照在string上:
```typescript
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"'.
```
上面的範例`req.method`被推斷為`string`,而並非`GET`。因為code會被兩種方式評估一是`req`的創造一是`handleRequest`的呼叫,這可能被賦值一個新的sting像是`GUESS`,所以TS認定這會是一個錯誤。
以下兩種方式來迴避掉錯誤發生
第一種方式為加入型別斷言的方式來避免推斷。
```typescript
// Change 1:
const req = { url: "https://example.com", method: "GET" as "GET" };
// Change 2
handleRequest(req.url, req.method as "GET");
```
方式直接使用字面值型別將method定義為`"GET"`,必免被賦值為`"GUESS"`的可能性,以及在`req.method`加上 `as "GET"`,代表說你就是知道`req.method`就是會有`"GET"` value
第二種方式為加入`as const`來轉變整個物件為一個字面值型別"
```typescript
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
```
### `null` and `undefined`
JS有兩個原始值用於表示不存在或未初始化的值:null 和 undefined。
TS有兩個對應的同名類型。這些類型的行為方式取決於你是否啟用了 strictNullChecks 選項。
#### `strictNullChecks` off
當此選項是off時,`null`和`undefined`還是可以被正常存取,而且`null`和`undefined`可以被賦值到任何屬性的類別上,但這樣可能導致許多bugs,所以通常建議是開起`strictNullChecks` 。
#### `strictNullChecks` on
開啟時,當一個值為`null`或`undefined`時,在使用這個值前你需要進行測試,如下方式:
```typescript
function doSomething(x: string | null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
```
#### Non-null Assertion Operator (Postfix!) **不懂~~~**
TypeScript also has a special syntax for removing null and undefined from a type without doing any explicit checking. Writing ! after any expression is effectively a type assertion that the value isn’t null or undefined:
```typescript
function liveDangerously(x?: number | null) {
// No error
console.log(x!.toFixed());
}
```
Just like other type assertions, this doesn’t change the runtime behavior of your code, so it’s important to only use ! when you know that the value can’t be null or undefined.
### Emums 後續再了解
Enums are a feature added to JavaScript by TypeScript which allows for describing a value which could be one of a set of possible named constants. Unlike most TypeScript features, this is not a type-level addition to JavaScript but something added to the language and runtime. Because of this, it’s a feature which you should know exists, but maybe hold off on using unless you are sure. You can read more about enums in the Enum reference page.
### Less Common Primitives
It’s worth mentioning the rest of the primitives in JavaScript which are represented in the type system. Though we will not go into depth here.
#### bigint
From ES2020 onwards, there is a primitive in JavaScript used for very large integers, BigInt:
```typescript
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
```
#### symbol
There is a primitive in JavaScript used to create a globally unique reference via the function Symbol():
```typescript
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
```
---
# Narrowing 限縮
將某一個可能為多種型別的變數,縮限成某單一種型別的過程就叫做限縮。
### type guard
在`if`檢查中,TS閱讀`typeof padding === "number"`這段並理解這段為一段特殊的形式為 *type guard* 。
### `typeof` type guards
如JS中支援的`typeof`運算符,可以知道值為任何型別,同樣的TS也期待這組運算符回傳其中幾種特定的srting,如下:
* `"string"`
* `"number"`
* `"bigint"`
* `"boolean"`
* `"symbol"`
* `"undefined"`
* `"object"`
* `"function"`
在TS中,確認typeof 回傳的值是一種type guard,因為TS能解讀`typeof`所代表的不同值,也知道JS裡怪異的部分,`typeof`並不回傳字串`null`,看以下範例:
```typescript
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
```
在上面的`printAll`函式,我們嘗試確認`strs`是否為一陣列(陣列在JS中就是物件的一種),但事實上在JS中`null`也是一種物件的一種。
有經驗的使用者對於這情況可能不會感到驚訝,但不是每個人都在JS中遇到這種狀況,幸運地是TS讓我們知道`strs`只有被限縮到`string[] | null`而並非只有`string[]`
這裡就可以往下說到truthiness chekcing
### Truthiness narrowing
在JS中我們可以使用任何條件表達式,如下,`if`表達式中不會永遠都是拿到`boolean`的形式。
```typescript
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
```
在JS中,由`if`開頭都會“強轉”成boolean形式來滿足判斷,像以下的直接會被“強轉”成`false`
* `0`
* `NaN`
* `""` (empty string)
* `0n`(the `bigint` version of zero)
* `null`
* `undefined`
而其他值都會被“強轉”成`true`,你總可以將值強轉成`boolean`值藉由`Boolean`funtion 或是用雙重`Boolean`否定符(`!!`),而第二種方式更有優勢可以將強轉的值的型別定義為`true`而不是像第一個方式只定義為`boolean`。
```typescript
// both of these result in 'true'
Boolean("hello"); // type: boolean, value: true
!!"world"; // type: true, value: true
```
這種方式也相當流行,用來進行確認一些值像是`null`或是`undefined`,如下範例:
```typescript
function printAll(strs: string | string[] | null) {
if (strs && typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
```
如上所見,藉由這樣確認`strs`是否為真的方式幫助我們避免了先前顯示的錯誤。
但請記住,使用truthiness checking常會導致錯誤,如下範例:
```typescript
function printAll(strs: string | string[] | null) {
// !!!!!!!!!!!!!!!!
// DON'T DO THIS!
// KEEP READING
// !!!!!!!!!!!!!!!!
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
```
我們將全部的內容包裝在一個truthy check,但這有個明顯的缺點,我們永遠正確處理到空字串的狀況。
### Equality narrowing
TS也使用`switch` 以及 equality chekc 像是 `===`,`!==`,`!=`來限縮型別,例如下面:
```typescript
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
(method) String.toUpperCase(): string
y.toLowerCase();
(method) String.toLowerCase(): string
} else {
console.log(x);
(parameter) x: string | number
console.log(y);
(parameter) y: string | boolean
}
}
```
我們確認值`x`跟`y`兩者為相等同時,TS也會知道他們兩個的型別為相同,因為`string`是唯一他們共同能接收的型別所以TS知道他們兩個為`string`。
確認特定的字面值也行得通,在上一小節利用truthiness narrowing對於空字串沒有正確處理到,而下面使用equality checking則可以直接擋住`null`且正確的移除`null`型別。
```typescript
function printAll(strs: string | string[] | null) {
if (strs !== null) {
if (typeof strs === "object") {
for (const s of strs) {
(parameter) strs: string[]
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
(parameter) strs: string
}
}
}
```
JS中的較鬆散的比對方式使用 `==`以及 `!=`,如果使用`== null`不只會確認值是否為`null`也同時會確認是否為`undefined`;同樣的也可應用在`==undefined`上。
```typescript
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
(property) Container.value: number
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
```
### The in operator narrowing
JS有一個運算符來判斷一個物件中是否有個這樣的屬性名稱: 就是 `in` 運算符,TS也將其當作一種方式來限縮可能的型別。
下面範例:
```typescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) {
return animal.swim();
}
return animal.fly();
}
```
如上所看到的,範例code `"swim in animal"` 這裡`swim`是string literal,`animal`是聯合型別,藉由if條件式來限縮animal的型別種類。
### `instanceof` narrowing
JS中有個運算符`instanceof`來檢查某一個值是否為另外一個值的實例,更準確的來說,`x instanceof Foo` 是來檢查`x`的原型鏈是否為`Foo.prototype`,`instanceof`也是一種type guard,TS也利用此來縮減型別可能。
```typescript
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
(parameter) x: Date
} else {
console.log(x.toUpperCase());
(parameter) x: string
}
}
```
### Assignment
當我們賦值於一變數,TS會自動看要賦予的值是什麼並且縮減型別可能
例如`let x = Math.random() < 0.5 ? 10 : "hello world"`
當指向x時會是`let x: string | number`
`x = 1;` 指向x就會是 `let x: number`
`x = "googlebye"` 指向x就會是 `let x: string`
請注意這樣賦予變數新的值的方式是合法的,主因是在一開始x就是被設定成不是`number`就是`string`,如果現在想賦予`x`一個`boolean`值,就會報錯如下:
`x = true`
:::danger
Type 'boolean' is not assignable to type 'string | number'.
:::
### Control flow analysis
TS利用 control flow analysis來分析變數的型別,當一個變數被分析時contorl flow可能被分開或是合併,在每一次的分析後,都可能有不同的型別。
如下示範
```typescript
function example() {
let x: string | number | boolean;
x = Math.random() < 0.5;
console.log(x);
let x: boolean --> x 此時型別為boolean
if (Math.random() < 0.5) {
x = "hello";
console.log(x);
let x: string --> 進到if判斷分析後,此時型別為string
} else {
x = 100;
console.log(x);
let x: number --> 進到if判斷分析後,此時型別為number
}
return x;
let x: string | number --> 回傳值可能為 string or number
}
```
### Using type predicates
前面使用了現有JS的方式來限縮型別,然而有時你會想要用直接的控制型別,為了使用這樣的使用者自定義的type guard,可以使用型別斷句來控制。如下:
```typescript
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
```
`pet is Fish`就是一個型別斷句,一斷句格式為`parameterName is Type`,而`parameterName`必須為目前參數中其中一詞。
任何時候`isFish`被某個變數呼叫,TS就會縮減那個變數為某一特定的型別前提是此型別有包含在原始型別內。
```typescript
let pet = getSmallPet();
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
```
### Discriminated unions
前面談到的多是`string`,`boolean`,`number`但多數時刻都是在處理更複雜的狀況。
現在想像我們要來定義一個形狀圓形或是正方形,圓形跟半徑相關,正方形跟邊長相關,下面範例,會使用一個屬性叫做`kind`來知道說我們現在要處理的是何種形狀,以下範例:
```typescript
interface Shape {
kind: "circle" | "square";
radius?: number;
sideLength?: number;
}
```
注意到現在使用的string literal type: `circle` 跟 `square`而不是使用`string`,可以避免掉錯字問題。
```typescript!
function handleShape(shape: Shape) {
// oops!
if (shape.kind === "rect") {
This condition will always return 'false' since the types '"circle" | "square"' and '"rect"' have no overlap.
// ...
}
}
```
現在嘗試寫一個`getArea` function基於針對圓形或是正方形,先來看圓形:
```typescript!
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
```
:::danger
Object is possibly 'undefined'.
:::
這裡TS不知道怎麼做,但此刻我們四雌比原始type checker還要懂型別是什麼,這裡我們加入一個non-null assertion(一個`!`在`shape.radius`後面)來表述`radius`是絕對存在的。
```typescript!
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius! ** 2;
}
}
```
但這樣的方式看起來不太理想,因為我們必須跟type checker說他是存在的,但這樣在過程中極度容易發生錯誤,此外在`stritcNullChecks`外,還是有可能意外存取到原始選項型別,所以採取另外的做法會是更好的選擇。
上面的問題主因為type checker無法理解到`radius` 或是`sideLength`是屬於何種`shape`的,所以來換種方式來定義`shape`,如下範例:
```typescript!
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
```
這裡我們將圓形以及正方形分開來定義,且`radius`跟`sideLenght`為必要的,以下看看嘗試從`shape`存取`radius`
```typescript!
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
}
```
:::danger
Property 'radius' does not exist on type 'Shape'.
Property 'radius' does not exist on type 'Square'.
:::
可以看到還是有錯誤發生,現在`Shape`是個union type,TS認為`shape`可能會是個`Square`而`Square`是沒有`radius`的,那如果我們先確認`kind`property呢?
```typescript!
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
}
}
```
這樣就沒有報錯了,當union內的每個型別都有其字面值型別,TS會將其認為是個*discriminated union* 並可利用其來限縮union。 在這情況下,`kind`是共同的屬性(也被認作為在`Shape`內用來做區別的),確認`kind`是否為`circle`,則可縮減到剩下type`Circle`。
這種確認方式在`switch`上也行得通,
```typescript!
unction getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
(parameter) shape: Circle
case "square":
return shape.sideLength ** 2;
(parameter) shape: Square
}
}
```
The important thing here was the encoding of Shape. Communicating the right information to TypeScript - that Circle and Square were really two separate types with specific kind fields - was crucial. Doing that let us write type-safe TypeScript code that looks no different than the JavaScript we would’ve written otherwise. From there, the type system was able to do the “right” thing and figure out the types in each branch of our switch statement.
### The `never` type
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.
### Exhaustiveness checking
`never`可以被指定到任何型別上,但型別不能被指定到`never`上(除never本身),這代表你可以使用`never`的方式來做縮減型別可能,在switch語句做詳盡的檢查。
---
# More on Functions
Functio是任何應用程式中最基本的建構,不管他們是local funtion, 從另一個module引用的,或是class裡面的一個methos,funtion也是值的一種,TS有許多不同呼叫function的方式,以下看介紹。
### Function Type Expressions
最簡單來描述一個function的方式為*Function Type Expressions*,這方式語法上跟arrow function非常相像。
```typescript!
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
```
這句法 `(a: string) => void` 代表的是“一個function有一個參數名為`a`其型別為`string`且沒有任何回傳值”,就像function declarations,如果參數名稱沒有特別標注型別則型別為`any`。
我們也可以用type aliases型別命名的方式來表達一個function type:
```typescript!
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
```
### Call Signatures
在JS中,function可以有能被呼叫的屬性,然而function expression的形式無法在其中宣告屬性,如果我們想要寫可以被呼叫的屬性,我們可以寫在一個物件型別中寫*call signature*。
```typescript!
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
```
注意到這邊的語法跟function type expression有些許不同,在參數與回傳值中間使用的是`:`而並非`=>`
### Construct Signatures
JS函式也可以藉由`new`運算符來調用,TS把這個當作一個建構子因為他們通常會建立一個新物件,你可以藉由加上`new`關鍵字在*call signature*前來使用*construct signature*,如下:
```typescript!
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
```
有些物件,像JS中的`Date`物件,不管有無`new`都能被調用,你可以任意組合在同一組函式物件內:
```typescript!
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
```
### Generic Functions
常見寫函示的時候是是跟input 以及 output有相關的,或是兩種input的形式是某方面相關的,下面舉例為一個函示會回傳一組陣列的第一個元素。
```typescript!
function firstElement(arr: any[]) {
return arr[0];
}
```
這個函示能成功回傳我們想要的值,但不幸的是他的型別為`any`,能更好的是他能回傳這組陣列的型別。
在TS中,*generics*泛型被使用在當我們想描述兩個值之間的對應關係時, We do this by declaring a type parameter in the function signature:
```typescript!
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
```
藉由加上`Type`,並且把它放在兩個地方,我們就建立了input和output的連結,現在在調用一次就會有更明確的型別了。
```typescript!
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
```
#### Inference
注意到我們沒有指定`Type`在這個範例,型別是被推論出來的,TS自動選的。
我們可以使用多種參數型別,`map`的版本看起來就像如下:
```typescript!
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
```
以上的例子,TS能直接推論Input的type*(是string array),同時Output的type也是根據function express的回傳值(為number)
#### Constraints
前面有寫過一些generic functions可以使用任何種類的值,有時我們會想連結兩個值,但只能針對某一值的子集進行操作,針對這種情形,我們可以使用*constraint*來限制type parameter能接收的型別。
下面這function要回傳一個較長的值,為了達成,需要`length`屬性來做判斷它`number`型別,我們藉由寫`estends`來達成限制參數型別。
```typescript!
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
```
:::danger
Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
:::
關於這個範例有幾個有趣的點,我們讓TS自動推論`longest`的回傳型別,這在泛型函式中也是能做到的,因為我們限制了`Type`為`{length: number}`, 我們才能夠從`a`和`b`參數存取到`.length`,如果沒有型別限制,我們就無法存取到那些屬性,因為那些值可能有其他
# Classes
TS 完整支援了ES2015後的`class` ,跟JS其他功能一樣,TS也增加了型別註解(type annotation)以及其他方式來讓你表示classes跟型別之間的關係。
```typescript!
class Point {
x: number;
y: number;
}
const pt = new Point();
pt.x = 0;
pt.y = 0;
```
就像其餘一樣,不一定要做型別註解(type annotation),但就會被註解成`any` type,
**```--strictPropertyInitialization```**
這個設定控制class field一定要在constructor內初始化。 否則會像以下
```typescript!
class BadGreeter {
name: string;
}
```
:::danger
Property 'name' has no initializer and is not definitely assigned in the constructor.
:::
下面範例才是正確的:
```typescript!
class GoodGreeter {
name: string;
constructor() {
this.name = "hello";
}
}
```
請注意field一定要在costructor本身做初始化。TS本身並不會分析你從constructor調用的method去偵測是否初始化,因為調用此class的分支可能會修改掉那些method並且可能沒有成功的初始化那些filed。 假如你執意不要在constructor來做初始化,你可以使用definite assignment assertion operator`!`,如下:
```typescript!
class OKGreeter {
// Not initialized, but no error
name!: string;
}
```
### `readonly`
field的前綴可以使用`readonly`修改,這可以防止field在constructor外被修改。
```typescript!
class Greeter {
readonly name: string = "world";
constructor(otherName?: string) {
if (otherName !== undefined) {
this.name = otherName;
}
}
err() {
this.name = "not ok";
// Cannot assign to 'name' because it is a read-only property.
}
}
const g = new Greeter();
g.name = "also not ok";
// Cannot assign to 'name' because it is a read-only property.
```
### Constructors
Class constructor跟function非常相像,你可以加入參數並可以使用型別註解(type annotation)、預設值(default values)、overload。
```typescript!
class Point {
x: number;
y: number;
// Normal signature with defaults
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
```
```typescript!
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}
```
class constructor 跟 function 還是有些不同:
* Constructors can’t have type parameters - these belong on the outer class declaration, which we’ll learn about later
* Constructors can’t have return type annotations - the class instance type is always what’s returned
#### Super Calls
就像在JS中,如果你有基礎的class,你需要呼叫`super()`,在你的constructor內容中在你使用任何`this`前:
```typescript!
class Base {
k = 4;
}
class Derived extends Base {
constructor() {
// Prints a wrong value in ES5; throws exception in ES6
console.log(this.k);
// 'super' must be called before accessing 'this' in the constructor of a derived class.
super();
}
}
```
在JS中,忘記呼叫`super()`是很常見的,但TS會在過程中就直接告知你錯誤訊息。
### Methods
在class裡面的函式屬性會被稱作*method*, method跟function和constuctors一樣可以使用type annotation。
```typescript!
class Point {
x = 10;
y = 10;
scale(n: number): void {
this.x *= n;
this.y *= n;
}
}
```
請注意在method裡面,必須使用`this`關鍵字來存取class裡面的任何field。
```typescript!
let x: number = 0;
class C {
x: string = "hello";
m() {
// This is trying to modify 'x' from line 1, not the class property
x = "world";
// Type 'string' is not assignable to type 'number'.
}
}
```
### Getters / Setters
Classes can also have accessors
```typescript!
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}
```
TS針對accessor有幾點特別的推論規則:
* 如果只有`get`存在,但沒有`set`,屬性將會被自動設定成 `readonly`。
* 如果setter參數沒有設定型別,則將自動推斷成getter的型別。
* Getter和Setter一定有相同的Member Visibility(public, private, protected)
自從 TS4.3版本後,get 跟 set 可能有不同的型別。
```typescript!
class Thing {
_size = 0;
get size(): number {
return this._size;
}
set size(value: string | number | boolean) {
let num = Number(value);
// Don't allow NaN, Infinity, etc
if (!Number.isFinite(num)) {
this._size = 0;
return;
}
this._size = num;
}
}
```
###### louis