環境就緒後,TS 系列的第一塊磚是型別系統的入口:原始型別、陣列、tuple、object、any/unknown/never 這些日常會反覆碰到的東西。重點不在記語法,而在養成「這個值在型別系統裡是什麼」的直覺。
型別標註的兩種寫法
// 1. 顯式標註
let name: string = 'Jeremy'
// 2. 型別推導(推薦,能省則省)
let age = 30 // 自動推導為 number
只有在編譯器無法推導,或想刻意鎖死型別時才標註。
原始型別
let s: string = 'hello'
let n: number = 42 // 整數、浮點、NaN、Infinity 都是 number
let b: boolean = true
let big: bigint = 100n
let sym: symbol = Symbol('id')
let nu: null = null
let un: undefined = undefined
字串字面值樣板:
const name = 'world'
const greeting: string = `hello, ${name}`
array 與 tuple
Array
兩種等價寫法:
const a: number[] = [1, 2, 3]
const b: Array<number> = [1, 2, 3] // 泛型寫法
混合型別用 union:
const mixed: (number | string)[] = [1, 'a', 2, 'b']
Tuple:固定長度、各位置型別不同
const pair: [string, number] = ['age', 30]
pair[0] // string
pair[1] // number
// 帶名稱(純標籤,提高可讀性)
const point: [x: number, y: number] = [10, 20]
// 可選元素
const t: [string, number?] = ['a']
// rest
const r: [string, ...number[]] = ['head', 1, 2, 3]
// readonly tuple(常用於 const assertion)
const rgb = [255, 0, 0] as const // readonly [255, 0, 0]
object 與物件型別
const user: { name: string; age: number } = {
name: 'Jeremy',
age: 30,
}
// 可選屬性
const config: { debug?: boolean; retries: number } = {
retries: 3,
}
// readonly 屬性
const point: { readonly x: number; readonly y: number } = { x: 0, y: 0 }
// point.x = 1 // ❌
行內型別寫多了會很亂,下一篇會用 interface / type 抽出來。
enum
兩種:數字 enum 與字串 enum。
// 數字 enum(值預設從 0 開始遞增)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 字串 enum(必須顯式賦值)
enum Status {
Idle = 'idle',
Loading = 'loading',
Done = 'done',
}
const s: Status = Status.Loading
TS 5 之後,社群越來越偏好用 as const 物件取代 enum,原因有三:
- enum 編譯後會額外產生程式碼(雙向 mapping)
- enum 在 type-only import 與 erasable syntax 模式下行為怪異
as const純型別、零執行期成本
const Status = {
Idle: 'idle',
Loading: 'loading',
Done: 'done',
} as const
type Status = typeof Status[keyof typeof Status]
// 'idle' | 'loading' | 'done'any:型別系統的逃生口
any 等於關掉型別檢查。能不用就不用。
let x: any = 5
x.foo.bar.baz() // 編譯通過,執行期炸
x = 'string' // OK
x = [] // OK
常見會「不小心是 any」的情況:
- 沒寫型別標註且編譯器推不出來(例如
function f(x) {},x預設any,除非開noImplicitAny) - 從
JSON.parse()拿到的東西 - 第三方套件沒有型別宣告
unknown:安全版的 any
let v: unknown = 5
v.toFixed() // ❌ 直接用會報錯
if (typeof v === 'number') {
v.toFixed() // ✅ 窄化後才能用
}
差別:
any | unknown | |
|---|---|---|
| 可以做任何操作 | ✅ | ❌(要先窄化) |
| 可賦給其他型別 | ✅ 任何型別 | ❌ 只能賦給 unknown / any |
| 何時用 | 真的不在乎、過渡用 | API 邊界、JSON.parse、catch 子句 |
try {
doSomething()
} catch (err) { // err 預設是 unknown(TS 4.4+)
if (err instanceof Error) {
console.error(err.message)
}
}
never:永不發生
代表「不可能有值」。常見情境:
// 永不回傳的函式
function fail(msg: string): never {
throw new Error(msg)
}
function loop(): never {
while (true) {}
}
// 窮舉檢查(exhaustive check)
type Shape = { kind: 'circle' } | { kind: 'square' }
function area(s: Shape) {
switch (s.kind) {
case 'circle': return 1
case 'square': return 2
default:
const _exhaustive: never = s // 漏掉一個 case 這裡會報錯
return _exhaustive
}
}
void
函式沒有「有意義的回傳值」時用 void:
function log(msg: string): void {
console.log(msg)
}
void 與 undefined 的差異:
type F1 = () => void
type F2 = () => undefined
const f1: F1 = () => 42 // ✅ void 不在意實際回傳值
const f2: F2 = () => 42 // ❌ F2 規定必須回傳 undefined
void 的重點在語意:告訴呼叫方「別拿這個回傳值做事」。最常見的就是 callback 簽章 (item: T) => void,避免使用者被回傳值誤導。
型別斷言(as)
告訴編譯器:「我比你清楚這是什麼型別」。
const el = document.getElementById('app') as HTMLDivElement
const n = '42' as unknown as number // 雙重 as,繞開不相容檢查(少用)
慎用。斷言錯了不會在編譯期報錯,會在執行期炸。
non-null 斷言(!)
const el = document.getElementById('app')! // 告訴 TS:保證不會是 null
el.innerHTML = 'hi'
跟 as 一樣是逃生口。能用 if (el) 守衛就用守衛。
const assertion:as const
把字面值固定下來:
let x = 'hello' // type: string
let y = 'hello' as const // type: 'hello'
const arr = [1, 2, 3] // number[]
const tup = [1, 2, 3] as const // readonly [1, 2, 3]
const obj = { name: 'a', age: 1 } // { name: string; age: number }
const obj2 = { name: 'a', age: 1 } as const // { readonly name: 'a'; readonly age: 1 }
非常適合搭配 keyof typeof 產生精準 union type(取代 enum)。
型別與值的雙重身份
TS 中有些名字同時存在於「型別空間」與「值空間」:
class Foo {}
const f: Foo = new Foo() // 第一個 Foo 是型別、第二個 Foo 是值
type 與 interface 只存在於型別空間,編譯後會被擦除(type erasure)。執行期沒有 TS 型別資訊,所以不能用 if (typeof x === 'MyInterface') 這種寫法。
所謂「型別空間」是編譯期的檢查層:型別名、interface、type alias 都住在這裡,執行期完全看不到。「值空間」則是執行期真正存在的東西——變數、函式、class instance。tsc 編譯時會把純型別符號全部刪掉(type erasure),剩下的就是純 JavaScript。
下一篇正式進 interface 與 type,把這篇行內物件型別寫起來會亂的問題解掉。
Latest Updates
- 2026.06.11 Content updated
