TypeScript 基礎

真的頗基礎的 QQ

前備知識

  1. TS 只是一個工具,用來讓 JS 可以有強型別的驗證
  2. 我們只是先用 TS 的方式來寫,接著再編譯器來 Complie 成 JS

相關指令

  • tsc index.js 編譯某個檔案
  • tsc 根據 config 來做編譯
  • tsc --watch 有變動就重新編譯

最基本範例

1
2
3
// index.ts
let aaa: string = '123'
console.log(aaa)

編譯後:

1
2
var aaa = '123'
console.log(aaa)

另外我也有看過這種寫法:

1
let aaa = 123 as number;

這樣的意思應該跟上面是差不多的,就看你喜歡哪種吧!

設定 config

先在專案底下執行:

1
tsc --init

專案就會多出一個 tsconfig.json,裡面就可以去調整設定(跟 VS Code 差不多)

這邊來設定最基本的兩個東西:

  1. 入口資料夾
  2. 出口資料夾
1
2
3
4
5
// tsconfig.json
{
"rootDir": "./src",
"outDir": "./dist"
}

接著只要執行 tsc 就會自動根據 config 來做編譯了。

另外要開啟 source map 的話可以把 sourceMap 設為 true

型別介紹

基本

1
2
3
4
5
6
7
8
9
10
11
12
13
let str: string = '123'
// 通常這種沒值的才會建議寫上 type
let str1: string
str1 = 'PeaNu'

let num: number = 123
let bool: boolean = true
let n: null = null
let un: undefined = undefined

// 如名,可以是任何 type
// 但一般只會用在必要的地方
let anyType: any = 123

陣列

1
2
3
4
5
6
7
8
9
10
// 決定陣列中的值的 type
let arr1: string[] = ['a', '123']
// 二維裡的 type
let arr2: string[][] = [['a'], ['b']]

// 元祖(Tuple)
// 按照「順序」決定每一個 type
let tuple1: [number, string, boolean] = [123, 'abc', true]
// 二維元祖裡的 type
let tuple2: [string, string][] = [['abc', 'def']]

Enum

有點像代號的感覺,但改用文字來表示:

1
2
3
4
5
6
7
enum LiveStatue {
SUCCESS = 0,
FAIL = -1,
STREAMING = 1
}

const liveStatus = LiveStatue.SUCCESS

Union

一個變數可以設定多個 type:

1
2
3
4
// 可以是 number 或 string
let union: number | string
union = 123
union = 'abc'

type

把 union 改用 type 來儲存(跟變數有點像)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type A = number | string
type B = boolean | string

let useTypeA: A
useTypeA = 123
useTypeA = 'abc'

let useTypeB: B
useTypeB = true
useTypeB = 'yoyoyo'

type Card = {
name: string
des: string
}

const user: Card = {
name: 'PeaNu',
des: '...'
}

interface

應該是用在 Object:

1
2
3
4
5
6
7
8
9
10
11
interface Card2 {
name: string
des: string
age: number
}

const user: Card2 = {
name: 'PeaNu',
des: '...',
age: 20
}

也可以用 ? 來變成「可選」的:

1
2
3
4
5
6
7
8
9
10
11
interface Card2 {
name: string;
des: string;
age?: number;
}

// 沒有 age 也沒關係
const user: Card2 = {
name: 'PeaNu',
des: '...'
}

type 跟 interface 的區別

一個可以「擴充」,一個不行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Card1 = {
name: string
des: string
}
// type 不可以在往下加
type Card1 = {
age: number
}

interface Card2 {
name: string
des: string
}
// interface 可以
interface Card2 {
age?: number
}

Function

function 主要是用來處理參數和回傳值的部分:

1
2
3
4
5
6
7
8
9
10
// 回傳值的 type
function stringConcat(a: string, b: string): string {
return a + b
}

// undefined 的處理(加上問號)
// undefined 的類型只能寫在最後面,畢竟參數要按照順序傳
function saySomething(age: string, name?: string) {
return age + name
}

斷言(unknown)

第一個會用到的情境是 call API 的時候。

因為 TypeScript 沒辦法知道 response 長怎樣,所以什麼都沒加的話就會是 any。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Data = {
userId: number
id: number
title: string
completed: boolean
}

async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
// 如果沒有 as Data,就會被當作 any
const data = (await res.json()) as Data
console.log(data)
}

所以上面的例子就透過 as Data 來做斷言,告訴它回傳值會長得像 Data 這個模樣。

第二種用法是可能某個值被 TypeScript 自動判斷,但這結果可能不太準確,就能用 as unknown as xxx 的手法來做:

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
type Data = {
userId: number
id: number
title: string
completed: boolean
}

// as unknown 後再重新斷言
// 先把 data1 設為 Data 這個 type
// 接著把值賦予給 data2,它就會自動被判定為 Data 這個 type
// 但如果 data1 的值不固定的話就會有一些誤差問題
const data1: Data = {
userId: 1,
id: 1,
title: 'delectus aut autem',
completed: false
}

// data2: Data
const data2 = data1

type Data2 = {
name: string
}
// data2: Data2
const data2 = data1 as unknown as Data2

Class 的應用

一般在 class 裡可以宣告 privatepubliceprotected 等等之類的關鍵字,but 我們都知道 JS 本身並沒有實際支援這些功能,你還是可以在外面直接去存取 private 的成員。

而 TS 解決了這個問題,如果你在 TS 去存取私有成員,就會編譯失敗。

提醒:privateprotected 的差別在於,透過繼承的 class 沒辦法存取到 private,但 protected 可以。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Live {
public name: string
private id: number
protected user: string
constructor(nameParam: string, idParam: number, userParam: string) {
this.name = nameParam
this.id = idParam
this.user = userParam
}
}

const live = new Live("PeaNu's live", 12345, 'PeaNu')
console.log(live)

這樣的結果會是:

1
2
3
4
5
6
// 還是存取的到 private 跟 protected
{
"name": "PeaNu's live",
"id": 12345,
"user": "PeaNu"
}

但如果你在 TS 這樣寫的話就會錯誤:

not-accessible

至於 protected 的部分也一樣,TS 一樣會去判斷繼承的 class 可以存取到哪些成員:

1
2
3
4
5
6
7
8
9
10
class SpecailLive extends Live {
constructor(nameParam: string, idParam: number, userParam: string) {
super(nameParam, idParam, userParam)
}
start() {
super.name
super.user
super.id // 不可以存取到 private 的值
}
}

如果不依賴 TS 的話,JS 原生的作法是這樣:

1
2
3
4
5
6
7
8
9
10
11
12
// 原生的 private 方法(比較新)
class Live2 {
#name
constructor(name: string) {
this.#name = name
}
}

// 背後是用 weakMap 去實作的
const testLive = new Live2('Test Live')
// {}
console.log(testLive)

implement

class 如果想把某個 interface 實作出來,可以用 implement 這個關鍵字,像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface CarProps {
name: string
age: number
}

class Car implements CarProps {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}

不過要注意,interface 中的成員一定是 public,不可以在 class 中寫成 private

implement

泛型

簡單來說就是「call function 的時候才決定 type 是什麼」,直接看範例:

1
2
3
4
5
6
7
// 如果要多個的話可以寫成 print<A, B, C>
function print<T>(data: T) {
console.log(data)
}
print<number>(999)
print<string>('Hello')
print<boolean>(123) // 錯誤,沒有傳正確的 type

另外也能用在 class 上(constructor):

1
2
3
4
5
6
7
8
9
class Print<T> {
data: T
constructor(d: T) {
this.data = d
}
}

const p = new Print<number>(123)
const p1 = new Print<string>('Hello')

& 與 |

其實他們就跟 &&|| 的意思是ㄧ樣的(應該是為了方便區分吧!),直接來看 code 就明白了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface OP1 {
a: number
}
interface OP2 {
b: number
}

// 指定 type 為 OP1 且 OP2
// 只要漏掉一個就不正確
const op1AndOp2: OP1 & OP2 = {
a: 123,
b: 456
}

// 指定 type 為 OP1 或 OP2
// 可以擇一就好
const op1OrOp2: OP1 | OP2 = {
a: 123
}

TS 內部提供的 utility

Record

第一個是 Record,其中一個例子是決定 Object 的 key 跟 value:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 決定物件的 value
interface CatInfo {
age: number
breed: string
}

// 決定物件的 key
type CatName = 'miffy' | 'boris' | 'mordred'

// 一個物件包物件的結構
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: 'Persian' },
boris: { age: 5, breed: 'Maine Coon' },
mordred: { age: 16, breed: 'British Shorthair' },
// 不存在的 key 會報錯
peanu: { age: 22, breed: '...' }
}

Pick

第二個是 Pick,可以從 interface 抽一部分的內容來用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Todo {
title: string
description: string
completed: boolean
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
// 沒有 pick 出來的東西就不能用
description: 'yoyoyo'
}

Omit

第三個是 Omit,就是 Pick 顛倒過來而已,Pick 是把要的拿出來,Omit 是把不要的東西拿掉,其他留下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Todo {
title: string
description: string
completed: boolean
createdAt: number
}

type TodoPreview = Omit<Todo, 'description'>

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
createdAt: 20210511,
// 已經 Omit 掉的東西就不該出現
description: '...'
}

Partial

有時候你可能定義了這樣的型別:

1
2
3
4
5
6
7
8
9
interface Blog {
id: string
title: string
slug: string
categories: string[]
tags: string[]
featureImageUrl?: string
content: string
}

但你沒辦法確保每一次使用時都能填入每一個值,這時候就可以用 Partial<T> 的方式把所有選項變成「Optional」,避免 TypeScript 的編譯錯誤:

1
2
3
const post: Partial<Blog> = {
title: 'Partial so good'
}

你並沒有直接改變 Blog 的型別,而是在「使用時」來讓他變成可選的,我覺得這是很划算的功能。

React

基本範例

這邊主要介紹幾個東西:

  1. Functional Component 的 type
  2. Props 的 type
  3. hook 的 type
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState } from 'react'
import './App.css'

// 我希望傳進來的 props 的 types
type TitleProps = {
name: string | number
desc?: string
}

// 只要是 Function Component 基本上都會這樣寫(React.FC)
const Title: React.FC<TitleProps> = ({ name, desc }) => {
return <h1>Hello, {name}</h1>
}

const App: React.FC = () => {
// 當我 useState 可能有多個值的時候才會寫出來
const [title, setTitle] = useState<string | number>('PeaNu')

return <Title name={title} />
}

export default App

Event

如果要指定 Event 的 type,可以加上 React 前綴來處理:

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
function App() {
const canvas1 = useRef<HTMLCanvasElement>(null)

// 原本是直接 TouchEvent | MouseEvent,但在 React 要加上前綴
function onStart(event: React.TouchEvent | React.MouseEvent) {
const cavasSize = canvas1.current?.getBoundingClientRect()!
let position = {
x: 0,
y: 0
}

if (event.type === 'mousemove') {
position.x = (event as React.MouseEvent).clientX - cavasSize.left
position.y = (event as React.MouseEvent).clientY - cavasSize.right
} else {
position.x = (event as React.TouchEvent).changedTouches[0].clientX - cavasSize.left
position.y = (event as React.TouchEvent).changedTouches[0].clientY - cavasSize.right
}

return position
}

return (
<div className='App'>
<canvas
ref={canvas1}
id='canvas'
width='500'
height='300'
onMouseDown={onStart}
onTouchStart={onStart}
></canvas>
</div>
)
}
設定 Mac iterm2 懶人包 從零開始建立一個 React 開發環境
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×