TypeScript前端的必修課之從JS到TS詳解
1. TypeScript 是什么?為什么要學(xué)?
1.1 一個(gè) bug 引發(fā)的思考
假設(shè)你要寫一個(gè)函數(shù),計(jì)算商品打折后的價(jià)格:
// 商品打折函數(shù)
function getDiscountedPrice(price, discount) {
return price - price * discount
}
// 調(diào)用
getDiscountedPrice(100, 0.2) // 80,正確
getDiscountedPrice('100', '0.2') // 80?還是報(bào)錯(cuò)?問題: getDiscountedPrice('100', '0.2') 的結(jié)果是 80,還是 NaN?還是 報(bào)錯(cuò)?
答案是 80!因?yàn)?JavaScript 偷偷幫你把字符串轉(zhuǎn)成了數(shù)字。
但這不是好事——如果用戶真的傳了字符串進(jìn)來,你的程序可能計(jì)算出錯(cuò)誤的結(jié)果,而 JavaScript 一聲不吭。
更危險(xiǎn)的場景是
function getUser(id) {
const user = users.find(u => u.id === id)
return user.name // 如果 user 是 undefined,這里會(huì)報(bào)錯(cuò)
}如果 id 傳錯(cuò)了,user 是 undefined,然后 user.name 就會(huì)報(bào)錯(cuò):
TypeError: Cannot read property 'name' of undefined
但 JavaScript 只在運(yùn)行到這行時(shí)才報(bào)錯(cuò)。如果這個(gè)函數(shù)很少被調(diào)用,bug 可能藏了很久才發(fā)現(xiàn)。
這就是動(dòng)態(tài)類型的痛點(diǎn)。
1.2 JavaScript 的痛點(diǎn)
1. 類型隱式轉(zhuǎn)換
console.log('5' - 3) // 2(字符串轉(zhuǎn)數(shù)字)
console.log('5' + 3) // '53'(數(shù)字轉(zhuǎn)字符串)
console.log(true + 1) // 2(true 轉(zhuǎn)成 1)
console.log([] + []) // ''(空字符串)同樣的 +,不同的結(jié)果。你永遠(yuǎn)不知道傳入的是什么類型。
2. 訪問不存在的屬性
const user = null console.log(user.name) // 運(yùn)行時(shí)才報(bào)錯(cuò)
3. 函數(shù)參數(shù)類型錯(cuò)誤
function greet(name) {
return `Hello, ${name.toUpperCase()}`
}
greet('張三') // "Hello, 張三"
greet(123) // 運(yùn)行時(shí)才報(bào)錯(cuò):name.toUpperCase is not a function4. 重構(gòu)困難
// 原來 user 有 name 屬性
function showUser(user) {
console.log(user.name)
}
// 后來改成了 username
// 需要手動(dòng)找到所有調(diào)用 showUser 的地方...
// 漏掉一個(gè),線上就報(bào)錯(cuò)總結(jié):JavaScript 很靈活,但靈活意味著 你犯錯(cuò),它不提醒。
1.3 TypeScript 是什么?
1. 定義
TypeScript = JavaScript + 類型系統(tǒng)
TypeScript 是 JavaScript 的超集,所有合法的 JS 代碼,在 TS 里也是合法的。
TypeScript:TS 包含 JS 的所有特性,類型系統(tǒng)(TS 獨(dú)有的, 類型注解、接口、泛型、類型推斷... )
2. 工作流程
你寫的 TS 代碼 —> TypeScript 編譯器(檢查類型錯(cuò)誤)—> 編譯成 JavaScript —> 瀏覽器/Node.js 運(yùn)行
關(guān)鍵點(diǎn):瀏覽器不認(rèn)識(shí) TS,需要先編譯成 JS。類型檢查發(fā)生在編譯時(shí),不是運(yùn)行時(shí)。
1.4 TS 能解決什么問題?
1. 類型錯(cuò)誤提前發(fā)現(xiàn)
function getDiscountedPrice(price: number, discount: number): number {
return price - price * discount
}
getDiscountedPrice(100, 0.2) // 正確
getDiscountedPrice('100', '0.2') // 編譯時(shí)就報(bào)錯(cuò)2. 智能提示
interface User {
name: string
age: number
}
const user: User = { name: '張三', age: 18 }
user. // 輸入 . 后,編輯器會(huì)提示 name 和 age3. 重構(gòu)安全
// 原來 user 有 name 屬性
function showUser(user: { name: string }) {
console.log(user.name)
}
// 改成 username 后
function showUser(user: { username: string }) {
console.log(user.username) // 這里改了
}
// 調(diào)用處會(huì)報(bào)錯(cuò),TS 幫你找全所有地方
showUser({ name: '張三' }) // 報(bào)錯(cuò):name 不在類型中4. 自文檔化
// JS 寫法:需要看代碼才知道參數(shù)是什么
function fetchData(url, options) { ... }
// TS 寫法:看一眼就知道怎么用
function fetchData(url: string, options?: {
method?: 'GET' | 'POST'
timeout?: number
}): Promise<any> { ... }1.5 環(huán)境搭建與第一個(gè) TS 程序
1. 安裝 TypeScript
# 全局安裝 npm install -g typescript # 驗(yàn)證安裝 tsc --version
2. 第一個(gè) TS 程序
創(chuàng)建一個(gè) hello.ts 文件:
// hello.ts
function greet(name: string): string {
return `Hello, ${name}!`
}
const message = greet('TypeScript')
console.log(message)3. 編譯運(yùn)行
# 編譯成 JS tsc hello.ts # 生成 hello.js,然后運(yùn)行 node hello.js # 或者用 ts-node 直接運(yùn)行 npx ts-node hello.ts
4. 在線體驗(yàn)
不想安裝環(huán)境?直接訪問:TypeScript Playground:https://www.typescriptlang.org/play
在線寫 TS,實(shí)時(shí)看編譯結(jié)果
5. 第一個(gè)報(bào)錯(cuò)
function greet(name: string): string {
return `Hello, ${name}!`
}
greet(123) // 報(bào)錯(cuò):參數(shù)類型不匹配編譯時(shí)會(huì)報(bào)錯(cuò):
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
2. 基礎(chǔ)類型
2.1 類型注解
1. 什么是類型注解?
類型注解就是告訴 TypeScript,這個(gè)變量是什么類型。
// 語法:變量名: 類型 let name: string = '張三' let age: number = 18 let isActive: boolean = true
2. 為什么要加類型注解?
// 不加注解:TS 會(huì)自己推斷 let name = '張三' // TS 推斷 name 是 string 類型 name = 123 // 報(bào)錯(cuò):不能把 number 賦給 string // 加了注解:明確告訴 TS 你的意圖 let age: number = 18 age = '二十' // 報(bào)錯(cuò)
類型注解不是必須的,TS 會(huì)自動(dòng)推斷。但加注解可以讓代碼更清晰。
3. 函數(shù)參數(shù)和返回值
// 參數(shù)加類型,返回值也加類型
function add(a: number, b: number): number {
return a + b
}
// 如果返回值類型不對,TS 會(huì)報(bào)錯(cuò)
function wrongAdd(a: number, b: number): number {
return '結(jié)果' // 報(bào)錯(cuò):不能返回 string
}2.2 原始類型
1.字符串 string
let name: string = '張三'
let greeting: string = "Hello"
let template: string = `你好,${name}`
// 錯(cuò)誤示例
let city: string = 123 // 不能把 number 賦給 string2. 數(shù)字 number
let age: number = 18 let price: number = 99.9 let hex: number = 0xff // 十六進(jìn)制 let binary: number = 0b1010 // 二進(jìn)制 let infinity: number = Infinity // 錯(cuò)誤示例 let count: number = '100' // 不能把 string 賦給 number
3. 布爾值 boolean
let isLoggedIn: boolean = true
let isAdmin: boolean = false
let isActive: boolean = Boolean('hello') // true
// 錯(cuò)誤示例
let isDone: boolean = 'true' // 不能把 string 賦給 boolean4. 類型推斷
類型注解可以不寫
// 不寫類型注解,TS 會(huì)自動(dòng)推斷 let name = '張三' // TS 推斷 name 是 string let age = 18 // TS 推斷 age 是 number let isActive = true // TS 推斷 isActive 是 boolean // 后面賦值錯(cuò)誤類型會(huì)報(bào)錯(cuò) name = 123 // 報(bào)錯(cuò):不能把 number 賦給 string
2.3 數(shù)組與元組
1. 數(shù)組 Array
兩種寫法:
// 寫法1:類型[] let numbers: number[] = [1, 2, 3, 4, 5] let names: string[] = ['張三', '李四', '王五'] // 寫法2:Array<類型> let scores: Array<number> = [90, 85, 100] let fruits: Array<string> = ['蘋果', '香蕉', '橙子'] // 錯(cuò)誤示例 let ages: number[] = [18, 20, '二十五'] // 不能放 string
2. 元組 Tuple
元組是固定長度、固定類型順序的數(shù)組。
// 定義元組:[string, number] 表示第一個(gè)是 string,第二個(gè)是 number let user: [string, number] = ['張三', 18] // 正確 user = ['李四', 20] // 錯(cuò)誤 user = [18, '李四'] // 順序錯(cuò)了 user = ['王五'] // 長度不夠 user = ['趙六', 22, '北京'] // 長度超了
什么時(shí)候用元組?
// 模擬 API 響應(yīng) let response: [boolean, string] = [true, '請求成功'] // 坐標(biāo) let position: [number, number] = [100, 200] // CSV 行數(shù)據(jù) let row: [string, number, boolean] = ['張三', 18, true]
2.4 any、unknown、void、never
1. 任意類型 any
any 表示「可以是任何類型」,相當(dāng)于關(guān)閉了類型檢查。
let something: any = 'hello'
something = 123 // 可以
something = true // 可以
something.toUpperCase() // 可以(雖然 number 沒有 toUpperCase)
// 危險(xiǎn):用了 any,TS 的保護(hù)就沒了
let data: any = { name: '張三' }
console.log(data.age.toFixed()) // 運(yùn)行時(shí)才報(bào)錯(cuò),TS 不阻止盡量少用 any,它會(huì)讓你失去 TS 的保護(hù)。
2. 未知類型 unknown
unknown 是 any 的安全版本。
let userInput: unknown = 'hello'
// unknown 不能直接使用
userInput.toUpperCase() // 報(bào)錯(cuò):unknown 不能直接調(diào)用方法
// 需要先「收窄類型」
if (typeof userInput === 'string') {
userInput.toUpperCase() // 現(xiàn)在可以了
}any vs unknown:any 放棄了檢查,unknown 要求你先檢查再使用。
3. 無返回值 void
void 表示函數(shù)沒有返回值。
// 沒有 return
function logMessage(message: string): void {
console.log(message)
// 沒有 return,或者 return; 或者 return undefined;
}
// 有 return,但返回 undefined
function doNothing(): void {
return
}
// 錯(cuò)誤示例
function wrong(): void {
return 'hello' // 不能返回 string
}4. 永遠(yuǎn)不會(huì)發(fā)生 never
never 表示函數(shù)永遠(yuǎn)不會(huì)有返回值(要么一直運(yùn)行,要么拋出錯(cuò)誤)。
// 拋出錯(cuò)誤,不會(huì)正常結(jié)束
function throwError(message: string): never {
throw new Error(message)
}
// 無限循環(huán),不會(huì)結(jié)束
function infiniteLoop(): never {
while (true) {
console.log('無限循環(huán)')
}
}
// 不可能到達(dá)的分支
function checkType(value: string | number) {
if (typeof value === 'string') {
// value 是 string
} else if (typeof value === 'number') {
// value 是 number
} else {
// 這里 value 是 never,因?yàn)椴豢赡艿竭@兒
const exhaust: never = value
}
}2.5 聯(lián)合類型與類型別名
1. 聯(lián)合類型 Union Type
一個(gè)變量可能是多種類型之一,用 | 分隔。
// id 可以是 string 或 number
let id: string | number = 'abc123'
id = 456 // 可以
id = true // 不能 是 boolean
// 函數(shù)參數(shù)可以是多種類型
function formatValue(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
} else {
return value.toFixed(2)
}
}
formatValue('hello') // "HELLO"
formatValue(3.1415) // "3.14"2. 類型別名 Type Alias
給類型起個(gè)名字,方便復(fù)用。
// 定義類型別名
type UserID = string | number
type User = {
id: UserID
name: string
age: number
}
// 使用
let userId: UserID = 'abc123'
let user: User = {
id: 1,
name: '張三',
age: 18
}
// 更復(fù)雜的例子
type Status = 'pending' | 'success' | 'error'
type ApiResponse = {
status: Status
data: any
message: string
}
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
console.log('成功:', response.data)
} else if (response.status === 'error') {
console.error('失敗:', response.message)
}
}2.6 類型推斷
不寫類型也行?
1. 自動(dòng)推斷
TypeScript 很聰明,很多情況下不需要你寫類型注解。
// 變量推斷
let name = '張三' // 自動(dòng)推斷為 string
let age = 18 // 自動(dòng)推斷為 number
let isActive = true // 自動(dòng)推斷為 boolean
// 數(shù)組推斷
let numbers = [1, 2, 3] // 推斷為 number[]
let mixed = [1, 'hello', true] // 推斷為 (string | number | boolean)[]
// 對象推斷
let user = {
name: '張三',
age: 18
} // 推斷為 { name: string; age: number }
// 函數(shù)返回值推斷
function add(a: number, b: number) {
return a + b // 自動(dòng)推斷返回值是 number
}2. 什么時(shí)候需要寫類型注解?
// 1. 函數(shù)參數(shù)(必須寫)
function greet(name: string) { ... } // 參數(shù)不能推斷
// 2. 變量初始值為空時(shí)
let user: User | null = null
// 3. 函數(shù)返回值需要明確時(shí)
function getData(): Promise<User> { ... }
// 4. 類型太復(fù)雜,推斷不準(zhǔn)確時(shí)
let config: Config = { ... }3. 最佳實(shí)踐
// 簡單情況:讓 TS 推斷
let name = '張三'
let numbers = [1, 2, 3]
// 復(fù)雜情況:手動(dòng)標(biāo)注
let user: User | null = null
function fetchData(): Promise<User[]> { ... }
// 不要過度標(biāo)注
let age: number = 18 // 多余,TS 已經(jīng)知道是 number
let age = 18 // 更好3. 函數(shù)與類型
3.1 函數(shù)參數(shù)類型
1. 基本寫法
// 給每個(gè)參數(shù)加上類型
function greet(name: string, age: number): void {
console.log(`${name},${age}歲`)
}
// 調(diào)用時(shí)必須傳正確類型
greet('張三', 18) // 正確
greet(123, 18) // 參數(shù)1類型錯(cuò)誤
greet('張三', '18') // 參數(shù)2類型錯(cuò)誤2. 箭頭函數(shù)
// 參數(shù)加類型,返回值加類型
const add = (a: number, b: number): number => a + b
// 如果函數(shù)體有多條語句
const multiply = (a: number, b: number): number => {
const result = a * b
return result
}3. 對象參數(shù)
// 參數(shù)是對象
function printUser(user: { name: string; age: number }): void {
console.log(`${user.name},${user.age}歲`)
}
printUser({ name: '張三', age: 18 }) // 正確
// 用接口更清晰(后面會(huì)講)
interface User {
name: string
age: number
}
function printUser(user: User): void {
console.log(`${user.name},${user.age}歲`)
}4. 回調(diào)函數(shù)參數(shù)
// 回調(diào)函數(shù)的參數(shù)也要加類型
function processData(data: string[], callback: (item: string) => void): void {
data.forEach(item => callback(item))
}
processData(['a', 'b', 'c'], (item) => {
console.log(item.toUpperCase())
})3.2 返回值類型
1. 基本寫法
// 在參數(shù)括號后面加 `: 類型`
function add(a: number, b: number): number {
return a + b
}
function greet(name: string): string {
return `Hello, ${name}!`
}
function logMessage(msg: string): void {
console.log(msg)
// 沒有 return,或者 return undefined
}2. 返回值類型推斷
// TS 可以自動(dòng)推斷返回值類型
function add(a: number, b: number) {
return a + b // TS 推斷返回值是 number
}
function getFullName(first: string, last: string) {
return `${first} ${last}` // TS 推斷返回值是 string
}
// 建議:復(fù)雜邏輯還是顯式寫返回值類型
function fetchUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then(res => res.json())
}3. 返回多種類型
// 返回值可以是多種類型
function getValue(flag: boolean): string | number {
if (flag) {
return 'hello'
} else {
return 123
}
}
const result = getValue(true) // result 類型是 string | number3.3 可選參數(shù)與默認(rèn)參數(shù)
1. 可選參數(shù) ?
// 用 ? 表示這個(gè)參數(shù)可以傳,也可以不傳
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting},${name}`
} else {
return `你好,${name}`
}
}
greet('張三') // "你好,張三"
greet('張三', '早上好') // "早上好,張三"
// 可選參數(shù)必須在必選參數(shù)后面
// 錯(cuò)誤:可選參數(shù)不能在必選參數(shù)前面
function wrong(age?: number, name: string) { }2. 默認(rèn)參數(shù)
// 參數(shù)默認(rèn)值(同時(shí)也成了可選參數(shù))
function greet(name: string, greeting: string = '你好'): string {
return `${greeting},${name}`
}
greet('張三') // "你好,張三"
greet('張三', '早上好') // "早上好,張三"
// 默認(rèn)參數(shù)可以放在必選參數(shù)前面(但不推薦)
function greet(greeting: string = '你好', name: string): string {
return `${greeting},${name}`
}
greet('張三') // 報(bào)錯(cuò):第二個(gè)參數(shù)沒傳3. 可選參數(shù) vs 默認(rèn)參數(shù)
| 對比 | 可選參數(shù) ? | 默認(rèn)參數(shù) = value |
|---|---|---|
| 不傳時(shí) | undefined | 使用默認(rèn)值 |
傳 undefined 時(shí) | undefined | 使用默認(rèn)值 |
傳 null 時(shí) | null | null |
| 適用場景 | 值可以是 undefined | 有明確的默認(rèn)值 |
3.4 剩余參數(shù)
1. 什么是剩余參數(shù)?
把多個(gè)參數(shù)收集成一個(gè)數(shù)組,用 ... 語法。
// 傳統(tǒng)方式:參數(shù)個(gè)數(shù)不固定很麻煩
function sum1(a: number, b: number, c: number) { } // 只能3個(gè)
// 剩余參數(shù):可以傳任意多個(gè)
function sum2(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0)
}
sum2(1, 2, 3, 4, 5) // 15
sum2(10, 20) // 30
sum2() // 02. 剩余參數(shù)與其他參數(shù)
// 剩余參數(shù)必須是最后一個(gè)參數(shù)
function introduce(name: string, age: number, ...hobbies: string[]): void {
console.log(`${name},${age}歲`)
console.log(`愛好:${hobbies.join('、')}`)
}
introduce('張三', 18, '讀書', '跑步', '游泳')
// 輸出:
// 張三,18歲
// 愛好:讀書、跑步、游泳3. 剩余參數(shù)的類型
// 剩余參數(shù)是數(shù)組類型
function log(...args: string[]): void {
args.forEach(arg => console.log(arg))
}
// 也可以是元組類型(固定長度)
function format(...args: [string, number]): string {
return `${args[0]},${args[1]}歲`
}
format('張三', 18) // 對
format('張三', 18, '北京') // 錯(cuò) 只能兩個(gè)參數(shù)3.5 函數(shù)重載
一個(gè)函數(shù)多種用法
1. 什么是重載?
同一個(gè)函數(shù)名,根據(jù)參數(shù)類型或數(shù)量的不同,執(zhí)行不同的邏輯。
// JavaScript 中常見:參數(shù)不同,行為不同
function getData(id) {
if (typeof id === 'string') {
// 通過字符串 ID 獲取
} else if (typeof id === 'number') {
// 通過數(shù)字 ID 獲取
} else if (Array.isArray(id)) {
// 通過 ID 數(shù)組批量獲取
}
}2. TypeScript 的重載寫法
// 1. 先聲明重載簽名(多個(gè))
function getData(id: string): User
function getData(id: number): User
function getData(ids: number[]): User[]
// 2. 再實(shí)現(xiàn)簽名(一個(gè))
function getData(id: any): any {
if (typeof id === 'string') {
// 獲取單個(gè)用戶(通過字符串 ID)
return { id, name: '張三' }
} else if (typeof id === 'number') {
// 獲取單個(gè)用戶(通過數(shù)字 ID)
return { id, name: '李四' }
} else if (Array.isArray(id)) {
// 獲取多個(gè)用戶
return id.map(i => ({ id: i, name: '用戶' + i }))
}
}
// 使用
const user1 = getData('abc') // 類型是 User
const user2 = getData(123) // 類型是 User
const users = getData([1, 2, 3]) // 類型是 User[]3. 更實(shí)用的例子
// 重載簽名
function formatDate(timestamp: number): string
function formatDate(date: Date): string
function formatDate(year: number, month: number, day: number): string
// 實(shí)現(xiàn)
function formatDate(a: any, b?: any, c?: any): string {
if (typeof a === 'number' && b === undefined && c === undefined) {
// 時(shí)間戳
return new Date(a).toLocaleDateString()
} else if (a instanceof Date) {
// Date 對象
return a.toLocaleDateString()
} else if (typeof a === 'number' && typeof b === 'number' && typeof c === 'number') {
// 年月日
return new Date(a, b - 1, c).toLocaleDateString()
}
return ''
}
formatDate(1704067200000) // 2024-01-01
formatDate(new Date()) // 當(dāng)前日期
formatDate(2024, 1, 1) // 2024-01-014. 什么時(shí)候用重載?
| 場景 | 是否用重載 |
|---|---|
| 參數(shù)類型不同,邏輯差異大 | 用 |
| 參數(shù)個(gè)數(shù)不同 | 用 |
| 只是可選參數(shù) | 用可選參數(shù)就夠了 |
| 用聯(lián)合類型能解決 | 優(yōu)先用聯(lián)合類型 |
3.6 泛型入門
讓類型也變成參數(shù)
1. 為什么需要泛型?
假設(shè)你要寫一個(gè)函數(shù),返回傳入的參數(shù):
// 只能處理 number
function identityNumber(value: number): number {
return value
}
// 只能處理 string
function identityString(value: string): string {
return value
}
// 用 any 可以,但失去了類型安全
function identityAny(value: any): any {
return value
}
const result = identityAny('hello')
result.toUpperCase() // 運(yùn)行時(shí)沒問題,但 TS 不知道類型2. 泛型解決
用類型參數(shù)
// <T> 是一個(gè)類型變量,表示「調(diào)用時(shí)確定的類型」
function identity<T>(value: T): T {
return value
}
// 使用
const num = identity<number>(100) // num 類型是 number
const str = identity<string>('hello') // str 類型是 string
// TS 會(huì)自動(dòng)推斷類型,可以省略 <類型>
const num2 = identity(100) // 自動(dòng)推斷為 number
const str2 = identity('hello') // 自動(dòng)推斷為 string3. 泛型約束
限制類型范圍
// 不加約束:T 可以是任何類型
function getLength<T>(value: T): number {
return value.length // 報(bào)錯(cuò):T 可能沒有 length 屬性
}
// 加約束:T 必須有 length 屬性
interface HasLength {
length: number
}
function getLength<T extends HasLength>(value: T): number {
return value.length // 可以
}
getLength('hello') // 5(string 有 length)
getLength([1, 2, 3]) // 3(數(shù)組有 length)
getLength(123) // 報(bào)錯(cuò):number 沒有 length4. 多個(gè)泛型參數(shù)
// 可以同時(shí)用多個(gè)類型參數(shù)
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b }
}
const result = merge({ name: '張三' }, { age: 18 })
// result 類型是 { name: string } & { age: number }
console.log(result.name, result.age)5. 泛型在實(shí)際項(xiàng)目中的應(yīng)用
// 1. 封裝 API 請求
async function fetchApi<T>(url: string): Promise<T> {
const response = await fetch(url)
return response.json()
}
interface User {
id: number
name: string
}
const user = await fetchApi<User>('/api/users/1')
console.log(user.name) // TS 知道 user 有 name 屬性
// 2. 封裝數(shù)組工具函數(shù)
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
const firstNumber = getFirst([1, 2, 3]) // 類型是 number | undefined
const firstString = getFirst(['a', 'b']) // 類型是 string | undefined4. 接口與類
4.1 什么是接口?
定義對象的形狀
1. 接口的作用
接口用來定義對象應(yīng)該長什么樣,描述對象有哪些屬性、屬性是什么類型。
// 定義接口
interface User {
name: string
age: number
}
// 使用接口
function greet(user: User): void {
console.log(`你好,${user.name}!你今年${user.age}歲`)
}
// 傳入的對象必須符合接口的形狀
greet({ name: '張三', age: 18 })
greet({ name: '張三' }) // 缺少 age
greet({ name: '張三', age: 18, city: '北京' }) // 多余屬性2. 為什么不用 type ?
// type 也能定義對象形狀
type User = {
name: string
age: number
}
// 但接口更推薦用于對象,有更好的錯(cuò)誤提示和擴(kuò)展性3. 接口的實(shí)際用途
// 1. 定義 API 響應(yīng)格式
interface ApiResponse<T> {
code: number
data: T
message: string
}
// 2. 定義組件 Props(React)
interface ButtonProps {
text: string
onClick: () => void
disabled?: boolean
}
// 3. 定義配置對象
interface Config {
apiUrl: string
timeout: number
retry: number
}4.2 可選屬性與只讀屬性
1. 可選屬性 ?
有些屬性可以存在,也可以不存在。
interface User {
name: string
age: number
email?: string // 可選屬性
phone?: string // 可選屬性
}
const user1: User = { name: '張三', age: 18 } // 沒傳 email
const user2: User = { name: '李四', age: 20, email: 'li@example.com' } // 傳了 email2. 只讀屬性readonly
屬性一旦賦值,就不能再修改。
interface Config {
readonly appId: string
readonly apiKey: string
timeout: number // 可以修改
}
const config: Config = {
appId: 'abc123',
apiKey: 'key123',
timeout: 5000
}
config.timeout = 10000 // 可以修改
config.appId = 'newId' // 報(bào)錯(cuò):不能修改只讀屬性3. 實(shí)際應(yīng)用
// 1. 用戶信息(ID 不應(yīng)該被修改)
interface User {
readonly id: number
name: string
email: string
}
// 2. 坐標(biāo)點(diǎn)(x, y 不應(yīng)該被修改)
interface Point {
readonly x: number
readonly y: number
}
// 3. 配置(不應(yīng)該被修改)
interface AppConfig {
readonly version: string
readonly apiUrl: string
debug: boolean // 這個(gè)可以動(dòng)態(tài)改
}4.3 索引簽名
動(dòng)態(tài)屬性名
1. 什么時(shí)候需要索引簽名?
當(dāng)對象的屬性名不確定時(shí),比如字典、配置對象。
// 場景:字典對象,key 是字符串,value 是數(shù)字
interface StringMap {
[key: string]: number
}
const scores: StringMap = {
math: 90,
english: 85,
chinese: 92
}
scores.physics = 88 // 可以動(dòng)態(tài)添加2. 索引簽名的類型
// key 是 string,value 是 string
interface Dictionary {
[key: string]: string
}
const dict: Dictionary = {
hello: '你好',
world: '世界'
}
// key 是 number,value 是 string(數(shù)組索引)
interface NumberIndex {
[index: number]: string
}
const arr: NumberIndex = ['a', 'b', 'c']
console.log(arr[0]) // 'a'3. 索引簽名 + 具體屬性
interface User {
name: string
age: number
[key: string]: string | number // 其他屬性只能是 string 或 number
}
const user: User = {
name: '張三',
age: 18,
nickname: '小張', // 符合 string | number
score: 90, // 符合 string | number
// isActive: true // boolean 不符合
}4. 實(shí)際應(yīng)用
// 1. 環(huán)境變量
interface Env {
[key: string]: string | undefined
NODE_ENV: 'development' | 'production'
PORT: string
}
// 2. 錯(cuò)誤消息
interface ErrorMessages {
[errorCode: string]: string
}
const errors: ErrorMessages = {
'400': '請求參數(shù)錯(cuò)誤',
'401': '未授權(quán)',
'404': '資源不存在'
}4.4 接口繼承
復(fù)用接口
1. 為什么要繼承?
避免重復(fù)定義相同的屬性。
interface Animal {
name: string
age: number
}
// 不繼承:重復(fù)寫相同屬性
interface Dog {
name: string
age: number
breed: string
}
// 用繼承:復(fù)用 Animal 的屬性
interface Dog extends Animal {
breed: string
}
const myDog: Dog = {
name: '旺財(cái)',
age: 3,
breed: '金毛'
}2. 多接口繼承
interface HasName {
name: string
}
interface HasAge {
age: number
}
interface HasEmail {
email: string
}
// 繼承多個(gè)接口
interface User extends HasName, HasAge, HasEmail {
id: number
}
const user: User = {
id: 1,
name: '張三',
age: 18,
email: 'zhang@example.com'
}3. 接口繼承接口
interface ButtonProps {
text: string
onClick: () => void
}
interface PrimaryButtonProps extends ButtonProps {
variant: 'primary'
size: 'large' | 'small'
}
const btn: PrimaryButtonProps = {
text: '提交',
onClick: () => {},
variant: 'primary',
size: 'large'
}4. 接口繼承類
了解即可
class User {
name: string = ''
age: number = 0
}
// 接口可以繼承類,得到類的所有屬性
interface Admin extends User {
role: string
}
const admin: Admin = {
name: '管理員',
age: 30,
role: 'admin'
}4.5 類與接口的區(qū)別
1. 類 Class
類是創(chuàng)建對象的藍(lán)圖,包含屬性和方法的具體實(shí)現(xiàn)。
class Animal {
name: string
constructor(name: string) {
this.name = name
}
speak(): void {
console.log(`${this.name}發(fā)出聲音`)
}
}
const dog = new Animal('旺財(cái)')
dog.speak() // 實(shí)際執(zhí)行代碼2. 接口 Interface
接口只定義形狀,不包含實(shí)現(xiàn)。
interface Animal {
name: string
speak(): void
}
// 類可以實(shí)現(xiàn)接口
class Dog implements Animal {
name: string
constructor(name: string) {
this.name = name
}
speak(): void {
console.log(`${this.name}汪汪叫`)
}
}3. 關(guān)鍵區(qū)別
| 對比 | 類(Class) | 接口(Interface) |
|---|---|---|
| 有沒有實(shí)現(xiàn) | 有 | 只有聲明 |
| 能不能 new | 可以 | 不能 |
| 編譯后 | 存在(變成 JS 代碼) | 不存在(編譯后消失) |
| 用途 | 創(chuàng)建對象 | 定義形狀 |
4. 實(shí)現(xiàn)多個(gè)接口
interface Flyable {
fly(): void
}
interface Swimmable {
swim(): void
}
// 一個(gè)類可以實(shí)現(xiàn)多個(gè)接口
class Duck implements Flyable, Swimmable {
fly(): void {
console.log('鴨子飛')
}
swim(): void {
console.log('鴨子游泳')
}
}4.6 類型別名 vs 接口
1. 對比表格
| 對比 | 類型別名(type) | 接口(interface) |
|---|---|---|
| 定義對象 | 可以 | 可以 |
| 定義原始類型 | 可以(type Name = string) | 不能 |
| 定義聯(lián)合類型 | 可以(type ID = string | number) | 不能 |
| 定義元組 | 可以 | 不能 |
| 繼承/擴(kuò)展 | &(交叉類型) | extends |
| 同名合并 | 報(bào)錯(cuò) | 自動(dòng)合并 |
| 推薦用于 | 復(fù)雜類型、聯(lián)合類型、工具類型 | 對象形狀、類實(shí)現(xiàn) |
2. 什么時(shí)候用 type?
// 1. 聯(lián)合類型
type ID = string | number
type Status = 'pending' | 'success' | 'error'
// 2. 原始類型別名
type UserName = string
// 3. 元組
type Point = [number, number]
// 4. 工具類型
type Nullable<T> = T | null
type Readonly<T> = { readonly [P in keyof T]: T[P] }3. 什么時(shí)候用 interface?
// 1. 定義對象形狀(推薦)
interface User {
name: string
age: number
}
// 2. 類實(shí)現(xiàn)
interface Runnable {
run(): void
}
class Dog implements Runnable { ... }
// 3. 需要擴(kuò)展時(shí)
interface Animal {
name: string
}
interface Dog extends Animal {
breed: string
}
// 4. 需要同名合并時(shí)(比如給第三方庫補(bǔ)充類型)
interface Window {
myCustomProperty: string
}4. 實(shí)際項(xiàng)目中的選擇
// 大多數(shù)情況用 interface(更標(biāo)準(zhǔn))
interface Product {
id: number
name: string
price: number
}
// 聯(lián)合類型用 type
type ProductStatus = 'inStock' | 'outOfStock' | 'discontinued'
// 組合時(shí)用 type
type ProductWithStatus = Product & { status: ProductStatus }5. 泛型
5.1 為什么需要泛型?
1. 一個(gè)重復(fù)代碼的問題
假設(shè)你要寫一個(gè)函數(shù),返回傳入的參數(shù):
// 只能處理 number
function identityNumber(value: number): number {
return value
}
// 只能處理 string
function identityString(value: string): string {
return value
}
// 只能處理 boolean
function identityBoolean(value: boolean): boolean {
return value
}三個(gè)函數(shù)邏輯完全一樣,只是類型不同。能不能寫一個(gè)函數(shù)搞定?
2. 用 any 的問題
function identityAny(value: any): any {
return value
}
const result = identityAny('hello')
result.toUpperCase() // 運(yùn)行時(shí)沒問題,但 TS 不知道 result 是 string
result.toFixed() // 運(yùn)行時(shí)報(bào)錯(cuò),但 TS 不阻止any讓你失去了類型保護(hù)。
3. 用聯(lián)合類型的問題
function identityUnion(value: string | number): string | number {
return value
}
const result = identityUnion('hello')
// result 類型是 string | number,不能直接調(diào)用 toUpperCase()
result.toUpperCase() // 報(bào)錯(cuò):可能不是 string4. 泛型的解決方案
// <T> 是一個(gè)類型變量,調(diào)用時(shí)確定
function identity<T>(value: T): T {
return value
}
const num = identity(100) // num 類型是 number
const str = identity('hello') // str 類型是 string
const bool = identity(true) // bool 類型是 boolean
// 調(diào)用時(shí)不用寫類型,TS 自動(dòng)推斷
const result = identity('hello')
result.toUpperCase() // TS 知道是 string5.2 泛型函數(shù)
1. 基本寫法
// 函數(shù)聲明
function getFirst<T>(arr: T[]): T | undefined {
return arr[0]
}
// 箭頭函數(shù)
const getLast = <T>(arr: T[]): T | undefined => {
return arr[arr.length - 1]
}
// 使用
const numbers = [1, 2, 3, 4, 5]
const firstNum = getFirst(numbers) // 類型是 number | undefined
const strings = ['a', 'b', 'c']
const lastStr = getLast(strings) // 類型是 string | undefined2. 多個(gè)泛型參數(shù)
// 交換數(shù)組中的兩個(gè)元素
function swap<T, U>(arr: [T, U]): [U, T] {
return [arr[1], arr[0]]
}
const swapped = swap([1, 'hello']) // 類型是 [string, number]
console.log(swapped) // ['hello', 1]3. 在實(shí)際項(xiàng)目中的應(yīng)用
// 1. 封裝 API 請求
async function fetchApi<T>(url: string): Promise<T> {
const response = await fetch(url)
return response.json()
}
interface User {
id: number
name: string
}
interface Product {
id: number
title: string
price: number
}
const user = await fetchApi<User>('/api/users/1')
const product = await fetchApi<Product>('/api/products/1')
// 2. 類型安全的 localStorage
function getStorage<T>(key: string): T | null {
const value = localStorage.getItem(key)
if (value === null) return null
return JSON.parse(value) as T
}
function setStorage<T>(key: string, value: T): void {
localStorage.setItem(key, JSON.stringify(value))
}
interface UserSettings {
theme: 'light' | 'dark'
fontSize: number
}
const settings = getStorage<UserSettings>('settings')5.3 泛型接口
1. 基本寫法
// 泛型接口
interface Box<T> {
value: T
getValue(): T
}
// 使用
const numberBox: Box<number> = {
value: 123,
getValue() {
return this.value
}
}
const stringBox: Box<string> = {
value: 'hello',
getValue() {
return this.value
}
}2. API 響應(yīng)接口(實(shí)際應(yīng)用)
// 通用的 API 響應(yīng)格式
interface ApiResponse<T> {
code: number
data: T
message: string
timestamp: number
}
// 不同接口返回不同 data 類型
interface User {
id: number
name: string
}
interface Product {
id: number
title: string
price: number
}
// 使用
const userResponse: ApiResponse<User> = {
code: 200,
data: { id: 1, name: '張三' },
message: 'success',
timestamp: Date.now()
}
const productResponse: ApiResponse<Product[]> = {
code: 200,
data: [{ id: 1, title: '商品', price: 99 }],
message: 'success',
timestamp: Date.now()
}3. 分頁接口
interface PageResult<T> {
data: T[]
total: number
page: number
pageSize: number
hasNext: boolean
}
async function getUsers(page: number): Promise<PageResult<User>> {
const response = await fetch(`/api/users?page=${page}`)
return response.json()
}5.4 泛型類
1. 基本寫法
// 泛型類
class Stack<T> {
private items: T[] = []
push(item: T): void {
this.items.push(item)
}
pop(): T | undefined {
return this.items.pop()
}
peek(): T | undefined {
return this.items[this.items.length - 1]
}
isEmpty(): boolean {
return this.items.length === 0
}
}
// 使用
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
numberStack.push(3)
console.log(numberStack.pop()) // 3
const stringStack = new Stack<string>()
stringStack.push('a')
stringStack.push('b')
console.log(stringStack.pop()) // 'b'2. 實(shí)際應(yīng)用:緩存類
interface CacheItem<T> {
value: T
expireAt: number
}
class Cache<T> {
private cache: Map<string, CacheItem<T>> = new Map()
set(key: string, value: T, ttl: number = 60000): void {
this.cache.set(key, {
value,
expireAt: Date.now() + ttl
})
}
get(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() > item.expireAt) {
this.cache.delete(key)
return null
}
return item.value
}
delete(key: string): void {
this.cache.delete(key)
}
clear(): void {
this.cache.clear()
}
}
// 使用
const userCache = new Cache<User>()
userCache.set('user:1', { id: 1, name: '張三' }, 30000)
const cachedUser = userCache.get('user:1')5.5 泛型約束(限制泛型的范圍)
1. 為什么要約束?
// 不加約束:T 可以是任何類型
function getLength<T>(value: T): number {
return value.length // 報(bào)錯(cuò):T 可能沒有 length 屬性
}2. 用 extends 添加約束
// 約束 T 必須有 length 屬性
interface HasLength {
length: number
}
function getLength<T extends HasLength>(value: T): number {
return value.length // 可以
}
getLength('hello') // 5(string 有 length)
getLength([1, 2, 3]) // 3(數(shù)組有 length)
getLength({ length: 10 }) // 10
getLength(123) // 報(bào)錯(cuò):number 沒有 length3. 多種約束
// 約束 T 必須有 name 和 age
interface HasNameAndAge {
name: string
age: number
}
function greet<T extends HasNameAndAge>(user: T): string {
return `你好,${user.name}!你今年${user.age}歲`
}
greet({ name: '張三', age: 18 })
greet({ name: '李四', age: 20, city: '北京' }) // 可以有額外屬性
greet({ name: '王五' }) // 缺少 age4. 約束為某個(gè)類的實(shí)例
class Animal {
name: string = ''
}
// 約束 T 必須是 Animal 的子類
function createInstance<T extends Animal>(Ctor: new () => T): T {
return new Ctor()
}
class Dog extends Animal {
breed: string = ''
}
const dog = createInstance(Dog) // dog 類型是 Dog5.6 多個(gè)泛型參數(shù)
1. 基本用法
// 合并兩個(gè)對象
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const result = merge({ name: '張三' }, { age: 18 })
// result 類型是 { name: string } & { age: number }
console.log(result.name, result.age)
// 鍵值對
class KeyValuePair<K, V> {
constructor(public key: K, public value: V) {}
}
const pair = new KeyValuePair<string, number>('age', 18)
console.log(pair.key, pair.value)2. 泛型約束 + 多個(gè)參數(shù)
// 約束 T 和 U 必須有共同的 key
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '張三', age: 18, city: '北京' }
const name = getProperty(user, 'name') // 類型是 string
const age = getProperty(user, 'age') // 類型是 number
// const invalid = getProperty(user, 'invalid') // 報(bào)錯(cuò)5.7 內(nèi)置泛型工具
Partial、Pick、Omit、Record
1. 所有屬性變成可選 Partial
interface User {
id: number
name: string
age: number
email: string
}
// 所有屬性都變成可選
type PartialUser = Partial<User>
// 等價(jià)于 { id?: number; name?: string; age?: number; email?: string }
// 適用場景:更新用戶(只傳要改的字段)
function updateUser(id: number, updates: Partial<User>): void {
// 只更新傳進(jìn)來的字段
}
updateUser(1, { name: '新名字' }) // 只傳 name
updateUser(1, { age: 19 }) // 只傳 age2. 挑選部分屬性 Pick
interface User {
id: number
name: string
age: number
email: string
password: string
}
// 只挑出需要的屬性
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// 等價(jià)于 { id: number; name: string; email: string }
// 適用場景:返回給前端的數(shù)據(jù)(不包含密碼)
function getUser(): PublicUser {
return {
id: 1,
name: '張三',
email: 'zhang@example.com'
}
}3. 排除部分屬性 Omit
interface User {
id: number
name: string
age: number
email: string
password: string
}
// 排除密碼屬性
type PublicUser = Omit<User, 'password'>
// 等價(jià)于 { id: number; name: string; age: number; email: string }
// 排除多個(gè)屬性
type UserWithoutPasswordAndAge = Omit<User, 'password' | 'age'>
// 適用場景:創(chuàng)建用戶(不需要 id)
type CreateUser = Omit<User, 'id'>4. 創(chuàng)建鍵值對類型 Record
// 定義對象,key 是 string,value 是 number
type ScoreMap = Record<string, number>
const scores: ScoreMap = {
math: 90,
english: 85,
chinese: 92
}
// 定義 key 只能是特定值
type Status = 'pending' | 'success' | 'error'
type StatusConfig = Record<Status, { color: string; text: string }>
const config: StatusConfig = {
pending: { color: 'orange', text: '處理中' },
success: { color: 'green', text: '成功' },
error: { color: 'red', text: '失敗' }
}
// 定義 key 是數(shù)字,value 是字符串
type WeekdayMap = Record<1 | 2 | 3 | 4 | 5 | 6 | 7, string>
const weekdays: WeekdayMap = {
1: '周一',
2: '周二',
3: '周三',
4: '周四',
5: '周五',
6: '周六',
7: '周日'
}5. 內(nèi)置泛型工具速查表
| 工具 | 作用 | 示例 |
|---|---|---|
Partial<T> | 所有屬性變可選 | Partial<User> |
Required<T> | 所有屬性變必選 | Required<PartialUser> |
Readonly<T> | 所有屬性變只讀 | Readonly<User> |
Pick<T, K> | 挑選部分屬性 | Pick<User, 'id' | 'name'> |
Omit<T, K> | 排除部分屬性 | Omit<User, 'password'> |
Record<K, T> | 創(chuàng)建鍵值對類型 | Record<string, number> |
Exclude<T, U> | 排除聯(lián)合類型中的某些 | Exclude<'a' | 'b' | 'c', 'a'> |
Extract<T, U> | 提取聯(lián)合類型中的某些 | Extract<'a' | 'b' | 'c', 'a' | 'b'> |
NonNullable<T> | 排除 null 和 undefined | NonNullable<string | null> |
ReturnType<T> | 獲取函數(shù)返回值類型 | ReturnType<typeof fn> |
Parameters<T> | 獲取函數(shù)參數(shù)類型 | Parameters<typeof fn> |
6. 類型操作
6.1 獲取對象的鍵 keyof
1. 基本用法
keyof 獲取對象類型的所有鍵,組成聯(lián)合類型。
interface User {
id: number
name: string
age: number
email: string
}
type UserKeys = keyof User // "id" | "name" | "age" | "email"
let key: UserKeys = 'id'
key = 'name'
key = 'address' // 報(bào)錯(cuò)2. 實(shí)際應(yīng)用:類型安全的屬性訪問
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: '張三', age: 18, city: '北京' }
const name = getProperty(user, 'name') // 類型是 string
const age = getProperty(user, 'age') // 類型是 number
// const invalid = getProperty(user, 'invalid') // 報(bào)錯(cuò)
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value
}
setProperty(user, 'name', '李四')
setProperty(user, 'age', '二十') // 報(bào)錯(cuò):不能把 string 賦給 number3. 遍歷對象的鍵
function printObject<T>(obj: T): void {
for (let key in obj) {
// 需要類型斷言,因?yàn)?key 是 string,不一定是 keyof T
console.log(`${key}: ${obj[key as keyof T]}`)
}
}6.2 獲取變量的類型 typeof
1. 基本用法
typeof 在 TypeScript 中獲取變量的類型(不是運(yùn)行時(shí)的值)。
const user = {
name: '張三',
age: 18,
city: '北京'
}
// 獲取 user 的類型
type User = typeof user
// 等價(jià)于 { name: string; age: number; city: string }
const anotherUser: User = {
name: '李四',
age: 20,
city: '上海'
}2. 實(shí)際應(yīng)用:避免重復(fù)定義類型
// 不用 typeof:重復(fù)定義
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retry: 3
}
interface Config {
apiUrl: string
timeout: number
retry: number
}
// 用 typeof:自動(dòng)推導(dǎo)
type Config = typeof config
// 在函數(shù)中使用
function initApp(config: typeof config) {
console.log(`API: ${config.apiUrl}`)
}3. 獲取函數(shù)類型
function add(a: number, b: number): number {
return a + b
}
type AddFunction = typeof add
// 等價(jià)于 (a: number, b: number) => number
const multiply: AddFunction = (a, b) => a * b4. 與 keyof 結(jié)合
const user = {
name: '張三',
age: 18,
address: {
city: '北京',
street: '長安街'
}
}
type User = typeof user
type UserKeys = keyof User // "name" | "age" | "address"6.3 索引訪問類型
1. 基本用法
用 T[K] 獲取類型 T 中鍵 K 的類型。
interface User {
id: number
name: string
age: number
address: {
city: string
street: string
}
}
type IdType = User['id'] // number
type NameType = User['name'] // string
type AddressType = User['address'] // { city: string; street: string }
type CityType = User['address']['city'] // string2. 聯(lián)合類型的索引訪問
interface User {
id: number
name: string
age: number
}
// 獲取多個(gè)屬性的類型(聯(lián)合類型)
type IdOrName = User['id' | 'name'] // number | string
// 獲取所有值的類型
type UserValues = User[keyof User] // number | string3. 數(shù)組元素的類型
type StringArray = string[]
type ItemType = StringArray[0] // string
type ItemType2 = StringArray[number] // string
// 實(shí)際應(yīng)用:獲取數(shù)組元素的類型
const users = [
{ id: 1, name: '張三' },
{ id: 2, name: '李四' }
]
type User = typeof users[0] // { id: number; name: string }
type User2 = typeof users[number] // { id: number; name: string }4. 提取函數(shù)返回值類型
function fetchUser(): Promise<{ id: number; name: string }> {
return Promise.resolve({ id: 1, name: '張三' })
}
// 提取 Promise 里的類型
type User = Awaited<ReturnType<typeof fetchUser>>
// 等價(jià)于 { id: number; name: string }6.4 條件類型
1. 基本用法
T extends U ? X : Y:如果 T 可以賦值給 U,則類型是 X,否則是 Y。
type IsString<T> = T extends string ? true : false type A = IsString<string> // true type B = IsString<number> // false type C = IsString<'hello'> // true('hello' 是 string 的子類型)
2. 實(shí)際應(yīng)用:排除 null 和 undefined
type NonNullable<T> = T extends null | undefined ? never : T type A = NonNullable<string | null> // string type B = NonNullable<string | undefined> // string type C = NonNullable<string | null | undefined> // string
3. 實(shí)際應(yīng)用:提取函數(shù)返回值類型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function add(a: number, b: number): number {
return a + b
}
type AddReturn = ReturnType<typeof add> // number4. 分布式條件類型
type ToArray<T> = T extends any ? T[] : never type A = ToArray<string | number> // 等價(jià)于 string[] | number[](不是 (string | number)[])
6.5 映射類型
1. 基本用法
映射類型可以基于舊類型創(chuàng)建新類型。
// 把所有屬性變成只讀
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// 把所有屬性變成可選
type Partial<T> = {
[P in keyof T]?: T[P]
}
// 使用
interface User {
id: number
name: string
age: number
}
type ReadonlyUser = Readonly<User>
// { readonly id: number; readonly name: string; readonly age: number }
type PartialUser = Partial<User>
// { id?: number; name?: string; age?: number }2. 自定義映射類型
// 把所有屬性變成 null 可選
type Nullable<T> = {
[P in keyof T]: T[P] | null
}
// 把所有屬性變成字符串
type Stringify<T> = {
[P in keyof T]: string
}
interface Product {
id: number
name: string
price: number
}
type NullableProduct = Nullable<Product>
// { id: number | null; name: string | null; price: number | null }
type StringProduct = Stringify<Product>
// { id: string; name: string; price: string }3. 修改屬性名
// 給屬性名加 get 前綴
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}
interface User {
name: string
age: number
}
type UserGetters = Getters<User>
// { getName: () => string; getAge: () => number }6.6 類型斷言
告訴 TS:相信我
1. 什么是類型斷言?
當(dāng) TypeScript 無法推斷類型,但你知道更準(zhǔn)確的類型時(shí),可以「斷言」。
// as 語法 const value: any = 'hello' const length = (value as string).length // 尖括號語法(在 JSX 中不能用) const length2 = (<string>value).length
2. 常見使用場景
// 1. 處理 DOM 元素
const input = document.getElementById('username') as HTMLInputElement
console.log(input.value) // TS 知道這是 input 元素
// 2. 處理 any 類型
const data: any = { name: '張三', age: 18 }
const name = (data as { name: string }).name
// 3. 處理聯(lián)合類型
function getLength(value: string | number): number {
// 告訴 TS:這里 value 一定是 string
return (value as string).length
}3. 雙重?cái)嘌裕ㄎkU(xiǎn))
const value: string = 'hello' // 直接斷言成 number 會(huì)報(bào)錯(cuò) // const num = value as number // 報(bào)錯(cuò) // 雙重?cái)嘌裕合绒D(zhuǎn)成 any,再轉(zhuǎn)成目標(biāo)類型(危險(xiǎn),不推薦) const num = value as any as number // 可能有問題
4. 非空斷言 !
當(dāng)你確定某個(gè)值不是 null 或 undefined 時(shí),可以用 !。
interface User {
name?: string
}
function getUserName(user: User): string {
// 告訴 TS:user.name 一定存在
return user.name! // 如果實(shí)際是 undefined,運(yùn)行時(shí)會(huì)報(bào)錯(cuò)
}
// DOM 元素
const element = document.getElementById('app')!
element.innerHTML = 'Hello'5. 類型斷言 vs 類型轉(zhuǎn)換
| 對比 | 類型斷言 | 類型轉(zhuǎn)換 |
|---|---|---|
| 運(yùn)行時(shí) | 不影響 | 會(huì)轉(zhuǎn)換值 |
| 語法 | as string | String(value) |
| 作用 | 告訴 TS 類型 | 實(shí)際轉(zhuǎn)換值 |
// 類型斷言:不轉(zhuǎn)換值,只告訴 TS const num = 123 const str = num as any as string // 運(yùn)行時(shí) str 還是 123(數(shù)字?。? // 類型轉(zhuǎn)換:實(shí)際轉(zhuǎn)換值 const realStr = String(num) // 運(yùn)行時(shí) realStr 是 "123"
恭喜你完成了TS的學(xué)習(xí),明天還有一篇實(shí)戰(zhàn)!
總結(jié)
到此這篇關(guān)于TypeScript前端的必修課之從JS到TS的文章就介紹到這了,更多相關(guān)TS前端必修課內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ionic由于使用了header和subheader導(dǎo)致被遮擋的問題的兩種解決方法
這篇文章主要介紹了ionic由于使用了header和subheader導(dǎo)致被遮擋的問題的兩種解決方法,本文介紹的非常詳細(xì),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
JavaScript canvas實(shí)現(xiàn)流星特效
這篇文章主要為大家詳細(xì)介紹了JavaScript canvas實(shí)現(xiàn)流星特效,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
js獲取url參數(shù)的使用擴(kuò)展實(shí)例
js獲取參數(shù)的具體應(yīng)用,歡迎加入腳本之家交流群2007-12-12
javascript設(shè)計(jì)模式之解釋器模式詳解
這篇文章主要介紹了javascript設(shè)計(jì)模式之解釋器模式詳解,當(dāng)有一個(gè)語言需要解釋執(zhí)行,并且可以將該語言中的句子表示為一個(gè)抽象語法樹的時(shí)候,可以考慮使用解釋器模式,需要的朋友可以參考下2014-06-06
JavaScript實(shí)現(xiàn)下拉列表選擇框
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)下拉列表選擇框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10

