0%

TypeScript 學習紀錄

前言

這篇文章主要來紀錄 TypeScript 的學習紀錄

使用的教材有

  1. TypeScript Tutorial
  2. 本文

什麼是 TypeScript

TypeScript 是 JavaScript 的延伸,也就是說,TypeScript 是建立在 JavaScript 之上的

因此我們可以透過 TypeScript 編譯器 來讓 TypeScript 程式碼轉換為 JavaScript。

那我們也可以將一個沒有語法錯誤的 JS 程式 視為 TS 程式,因此在這兩種語言之間進行轉換,所需要的成本是相對比較低的。

透過引進 型別 到 JS,能夠使開發過程更加嚴謹,進一步減少 Bugs。

Basic Type

Type Annotation

透過 Type Annotations 的方式來表明變數的型態

1
let <variable>: <type>: <value>

E.g.

如果指定錯誤的值,會發生 compiler error

1
let name: number = 'one'; // Type 'string' is not assignable to type 'number'.

指定一個 sayHello function,並回傳 strin

1
2
3
4
5
6
7
8
9
10
let sayHello: (name: string) => string;

const hello = function(name: string) {
return `Hi ${name}`
}

// 不符合格式的話,也會發生 compiler error
const hello = function(name: string) {
return `Hi ${name}`
}

Type Inference

Type Annotation 相反, Type Inference不表明型態的方式。

E.g.

1
2
// 不表明 counter 是一個 number,但透過指定變數的方式可以讓 TypeScript 得知 counter 是一個 number 
let counter = 0

Type Inference vs. Type Annotation

在大多數的實務上都會使用 Type Inference,會使用 Type Annotation 的時機

  • 先宣告變數而不定義
  • 希望變數的型態不透過 TypeScript 推論
  • 當函式回傳 any

Best common type algorithm

假如今天有一個變數如下,TypeScript 會主動給予該變數一個最適合的變數型態

1
2
3
4
5
let items = [1, 2, 3, null]; // number[]

let items = [0, 1, null, 'Hi'] // (number | string)[]

let array = [new Date(), new RegExp('\d+')]; // (RegExp | Date)[]

Contextual typing

Contextual 代表變數會隨著前後文來決定自己的型態

1
2
3
4
5
6
7
8
9
document.addEventListener('click', function (event) {
console.log(event.button); // 這裡個 event 變數指的是 MouseEvent
});

// Compiler Error
// This event is an instance of UIEvent, not a MouseEvent
document.addEventListener('scroll', function (event) {
console.log(event.button); // Property 'button' does not exist on type 'Event'
});

Object Type

object 代表的是所有非原型(primitive)型別的值。

TypeScript 的原型型別

  • number
  • bigint
  • string
  • boolean
  • null
  • undefined
  • sybmol

定義 object 變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let employee: object;

employee = {
firstName: 'Percy',
lastName: 'Chen',
age: 22,
jobTitle: 'Web Developer'
};

console.log(employee)
/*
{
firstName: 'John',
lastName: 'Doe',
age: 25,
jobTitle: 'Web Developer'
}
*/

Object

object 容易搞混的有是 Object,後者所代表的是所有 objects 所能使用的功能,也就是說在 typescript 中幾乎所有的值都有 toString() & valueOf 這些功能可以使用。

empty type {}

{} 代表一個沒有任何屬性存在內的 object,如果想要存取 object 的值會發生 compiler error

1
2
let vacant: {};
vacant.name = 'Percy'; // Property 'name' does not exist on type '{}'

array

array 是一個有序的資料,定義的方法如下

1
2
// let arrayName: type[];
let skills: string[];
1
2
3
4
5
6
7
8
let skills: string[];

// 根據定義的方式,skills 被推論為 string array
skills = ['a', 'b']

// 若在此時插入一個非字串的值會發生錯誤

skills.push(123) // Argument of type 'number' is not assignable to parameter of type 'string'

properties & methods

array 有許多 屬性方法 可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// init
let series = [0, 1, 2, 3, 4]

// length property
console.log(series.length) // 5

// method
// map
series.map(v => v^2) // [ 0, 1, 4, 9, 16 ]

// forEach
let sum = 0;
console.log(series.forEach(function(value){sum += value}))

// reduce 情境比較多,這邊只提供單純的情境
let tmp = series.reduce(function(pre_value, cur_value,){return pre_value+cur_value}, 5) // 15

mixed type

1
2
3
4
5
6
// typescript 也支援儲存混合型態的 arryr
let scores = ['Programming', 5, 'Software Design', 4];

// 或是透過混合的方式定義
let scores2 : (string | number)[];
scores2 = ['a', 'b', 1]

Tuple

比起 Array,Tuple 多了更多條件

  • 元素內的數量是固定的
  • 元素內容的型態不一定要相同
1
2
3
4
5
let skills: [number, string]

// 宣告好的 tuple, 在定義時不能夠將變數放置錯誤的位置,不然會發生 compiler error
skills = ['box', 5] // Compiler error
skills = [5, 'box'] // Correct

Optional tuple elements

從 TypeScript 3.0 開始,便可以透過 ? 來表示一個 tuple 的內容是非必填的。

1
2
3
let bgColor, headerColor: [number, number, number, number?];
bgColor = [0, 255, 255, 0.5];
headerColor = [0, 255, 255]; // 有注意到這裡並沒有定義第四個 number 嗎?

Enum

Enum 是一組常數數值,可以用來定義一連串有關聯性的變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
enum Month {
Jan,
Feb,
Mar,
Apr,
May,
Jun,
Jul,
Aug,
Sep,
Oct,
Nov,
Dec
};

/*
{
'0': 'Jan',
'1': 'Feb',
'2': 'Mar',
'3': 'Apr',
'4': 'May',
'5': 'Jun',
'6': 'Jul',
'7': 'Aug',
'8': 'Sep',
'9': 'Oct',
'10': 'Nov',
'11': 'Dec',
Jan: 0,
Feb: 1,
Mar: 2,
Apr: 3,
May: 4,
Jun: 5,
Jul: 6,
Aug: 7,
Sep: 8,
Oct: 9,
Nov: 10,
Dec: 11
}
*/

// 也可以修改 enum 的起始數值
enum Month {
A = 5,
B,
C,
D,
E
}

Any type

如果在宣告的時候沒有給予型態,那 typescript 會透過 type inference 的特性給予這些變數any 的型態。

可以透過修改 tsconfig.jsonnoImplictAny 數值來停用自動賦予 any 的這個特性

any vs. object

使用 any 的情境下,在變數沒有該方法可以呼叫時,也不會出現錯誤訊息;相反的,使用 object,可以提早知道錯誤的資訊。

1
2
3
4
5
6
7
8
9
let result: any;
result = 10.123;
console.log(result.toFixed()); // compile 時不會發生錯誤,會到執行的時候才會出現錯誤資訊
result.willExist();

let result: object;
result = 10.123; // Type 'number' is not assignable to type 'object'
console.log(result.toFixed()); // Property 'toFixed' does not exist on type 'object'
result.willExist(); //

Void

any 剛好相反, void 表示的是沒有任何型態,通常會用來表示不回傳任何值的狀況

1
2
3
function lo(message: string): void{
console.log(message)
}

never

never 通常被使用來表示回傳的格式中會 觸發錯誤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function raiseError(message: string): never {
throw new Error(message)
}

// 回傳格式若為 Error,則回傳格式則會被推論為 never
function reject() {
return raiseError('Rejected')
}

// 函式若包含無限 Loop,則格式也會被視為 never
let loop = () => {
while (true){
console.log("Hello!");
}
}

union

union 用來描述一個變數可能會有多種型態的狀況。

1
a: number | string

Type Alias

type alias 賦予現有的型態新的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
// type alias = existingType

type chars = string;

let name: chars; // 等同於 let name: string

// type alias 也可以與 union 結合

type alphanumeric = number | string;
let value: alphanumeric;
value = 'Percy'; // valid
value = 100; // valid
value = false; // Compiler error

string literal types

透過 宣告不同的字串 來達成 自定義的型態

1
2
3
4
5
6
7
8
9
10
11
12
13
let tax: 'tax';

tax = 'tax';
tax = 'tax1'; // Type '"tax1"' is not assignable to type '"tax"'

let petsound: 'Meow' | 'woof'

petsound = 'Meow';
petsound = 'woof';
petsound = 'woooof'; // Type '"woooof"' is not assignable to type '"Meow" | "woof"'

// 也可以與 type aliases 做結合
let anotherpetsound: petsound

Control Flow Statements

Functions

Rest Parameter

rest parameter 意味著函式的參數可以接受零到多個參數,此外還有以下規定:

  • 一個函式只能有一個 rest parameter
  • 只能出現在參數的最後一個位子
  • 是一個 array 格式
1
2
3
4
5
6
7
8
9
function fn(...names: string[]): void{
names.forEach((name)=> console.log(name))
}

fn("Percy", "Chen")
/*
Percy
Chen
*/

Classes

在 ES6 以前,在 JS 中並沒有類別 class 的概念,在 ES6 之後變具備了建立 class 的語法糖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
firstName: string;
lastName: string;
age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getHelloMessage() {
return `Welcome ${this.lastName} ${this.firstName}!`
}
}

let person = new Person('Percy', 'Chen', 27);

console.log(person.getHelloMessage());

Access Modifiers

存取修飾符 Access Modifiers 是用來修改類別的 屬性 & 方法 的能見度,在 TypeScript 中提供了三種

  • private
  • protected
  • public(預設)

private

private 將能見度限制在類別內,意味著類別以外的人是不能夠存取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
firstName: string;
lastName: string;
age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getHelloMessage() {
return `Welcome ${this.lastName} ${this.firstName}!`
}
}

let person = new Person('Percy', 'Chen', 27);

console.log(person.fiirstname); // compiler error

public

public 的屬性可以在任何地方被存取,在沒有使用修飾符的狀況下,預設都是使用 public。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
(public) firstName: string;
(public) lastName: string;
(public) age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getHelloMessage() {
return `Welcome ${this.lastName} ${this.firstName}!`
}
}

let person = new Person('Percy', 'Chen', 27);

console.log(person.getHelloMessage());

protected

protected 是允許 自己類別 以及 子類別存取 的屬性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
(procted) firstName: string;
(public) lastName: string;
(public) age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getHelloMessage() {
return `Welcome ${this.lastName} ${this.firstName}!`
}
}

let person = new Person('Percy', 'Chen', 27);

console.log(person.getHelloMessage());

readonly

透過 readonly 讓類別屬性不可變(immutable),與 const 的差異在於,前者用在類別屬性,後者用在一般變數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person2 {
readonly birthDate: Date;

constructor(birthDate: Date) {
this.birthDate = birthDate;
}
}

/*
上面的內容可以合併成
class Person2{
constructor(readonly birthDate: Date) {
this.birthDate = birthDate;
}
}
*/

let person2 = new Person2(new Date(2010, 10, 25));
person2.birthDate = new Date(2002, 1, 2); // Cannot assign to 'birthDate' because it is a read-only property

Static properties

static 在整個類別及其實例中共用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StaticBox{
static initCount: number = 0

constructor(
private length: number,
private width: number,
private color: string,
){
StaticBox.initCount+=1
}

public static getCounter(): number {
return StaticBox.initCount;
}
}

let box1: StaticBox = new StaticBox(3, 4, 'Blue')
console.log(StaticBox.getCounter())

let box2: StaticBox = new StaticBox(1, 2, 'Red')
console.log(StaticBox.getCounter())

Interface

將不同的變數透過 interface 的方式包裝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// this type annotation is difficult to read
function getFullName(person: {
firstName: string;
lastName: string;
}) {
return `${person.firstName} ${person.lastName}`
}

let person4 = {
firstName: 'John',
lastName: 'Doe'
};

console.log(getFullName(person4));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Person3 {
firstName: string;
lastName: string;
}

function getFullName2(person: Person3) {
return `${person.firstName} ${person.lastName}`;
}

let me = {
firstName: 'Percy',
lastName: 'Chen'
}

console.log(getFullName2(me))

function types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IsTeeanger {
(age: number): string
}

let checkIsTeen: IsTeeanger;

checkIsTeen = function (age: number) {
return age < 18 ? "YES" : "NO"
}

// 參數不一定要符合原本的設定,這裡把 age 改成 anotherage 是可行的
checkIsTeen = function(anotherage: number) {
return anotherage < 18 ? "YES" : "NO"
}

console.log(checkIsTeen(45))

class types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Json {
toVolume(): number
}

class Box implements Json {
constructor(private length: number, private width: number){}

toVolume(): number {
return this.length * this.width
}
}

let box = new Box(5, 12);
console.log(box.toVolume());

Advanced Types

Intersection Type

intersection type 代表的是透過多個型態組合成一個新型態。

1
2
// tpyeAB 會繼承 typeA + typeB
type typeAB = typeA & typeB;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface Person {
name: string;
sex: string;
}

interface Info {
address: string;
identity: string;
}


// resident1, resident2 都是由 Person, Info 組成,但組成的方式規則不同
// resident1 的內容不能缺少,但 resident2 的內容可以缺少
type resident1 = Person & Info
type resident2 = Person | Info

let man: resident1 = {
name: "Percy",
sex: "Man",
address: "Taipei",
identity: "haha"
}

let man2: resident2 = {
name: "Percy",
address: "Taipei",
identity: "haha"
}

型態的順序不影響 type intersection

Modules

透過模組化 modules,可以將 A 檔案的內容傳遞給 B 檔案做使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// my_modules.ts
function foo (number: number): boolean{
if (number % 2 == 0){
return true
} else {
return false
}

}

export {foo}

// another_modules.ts
import * as mod from "./my_module";

console.log(mod.foo(5)) // false