javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > TS前端必修课

TypeScript前端的必修课之从JS到TS详解

作者:昭昭颂桉a

许多开发者对TypeScript是否值得学习感到困惑,答案是肯定的,TypeScript(TS)为前端开发提供了强大的类型系统,有助于减少错误并提升代码质量,这篇文章主要介绍了TypeScript前端的必修课之从JS到TS的相关资料,需要的朋友可以参考下,

1. TypeScript 是什么?为什么要学?

1.1 一个 bug 引发的思考

假设你要写一个函数,计算商品打折后的价格:

// 商品打折函数
function getDiscountedPrice(price, discount) {
  return price - price * discount
}

// 调用
getDiscountedPrice(100, 0.2)  // 80,正确
getDiscountedPrice('100', '0.2')  // 80?还是报错?

问题: getDiscountedPrice('100', '0.2') 的结果是 80,还是 NaN?还是 报错

答案是 80!因为 JavaScript 偷偷帮你把字符串转成了数字。

但这不是好事——如果用户真的传了字符串进来,你的程序可能计算出错误的结果,而 JavaScript 一声不吭。

更危险的场景是

function getUser(id) {
  const user = users.find(u => u.id === id)
  return user.name  // 如果 user 是 undefined,这里会报错
}

如果 id 传错了,user 是 undefined,然后 user.name 就会报错:

 TypeError: Cannot read property 'name' of undefined

但 JavaScript 只在运行到这行时才报错。如果这个函数很少被调用,bug 可能藏了很久才发现。

这就是动态类型的痛点。

1.2 JavaScript 的痛点

1. 类型隐式转换

console.log('5' - 3)   // 2(字符串转数字)
console.log('5' + 3)   // '53'(数字转字符串)
console.log(true + 1)  // 2(true 转成 1)
console.log([] + [])   // ''(空字符串)

同样的 +,不同的结果。你永远不知道传入的是什么类型。

2. 访问不存在的属性

const user = null
console.log(user.name)  // 运行时才报错

3. 函数参数类型错误

function greet(name) {
  return `Hello, ${name.toUpperCase()}`
}

greet('张三')  // "Hello, 张三"
greet(123)     // 运行时才报错:name.toUpperCase is not a function

4. 重构困难

// 原来 user 有 name 属性
function showUser(user) {
  console.log(user.name)
}

// 后来改成了 username
// 需要手动找到所有调用 showUser 的地方...
// 漏掉一个,线上就报错

总结:JavaScript 很灵活,但灵活意味着 你犯错,它不提醒。

1.3 TypeScript 是什么?

1. 定义

TypeScript = JavaScript + 类型系统

TypeScript 是 JavaScript 的超集,所有合法的 JS 代码,在 TS 里也是合法的。

TypeScript:TS 包含 JS 的所有特性,类型系统(TS 独有的, 类型注解、接口、泛型、类型推断... )

2. 工作流程

你写的 TS 代码  —> TypeScript 编译器(检查类型错误)—> 编译成 JavaScript —> 浏览器/Node.js 运行

关键点:浏览器不认识 TS,需要先编译成 JS。类型检查发生在编译时,不是运行时。

1.4 TS 能解决什么问题?

1. 类型错误提前发现

function getDiscountedPrice(price: number, discount: number): number {
  return price - price * discount
}

getDiscountedPrice(100, 0.2)      // 正确
getDiscountedPrice('100', '0.2')  // 编译时就报错

2. 智能提示

interface User {
  name: string
  age: number
}

const user: User = { name: '张三', age: 18 }
user.  // 输入 . 后,编辑器会提示 name 和 age

3. 重构安全

// 原来 user 有 name 属性
function showUser(user: { name: string }) {
  console.log(user.name)
}

// 改成 username 后
function showUser(user: { username: string }) {
  console.log(user.username)  // 这里改了
}

// 调用处会报错,TS 帮你找全所有地方
showUser({ name: '张三' })  // 报错:name 不在类型中

4. 自文档化

// JS 写法:需要看代码才知道参数是什么
function fetchData(url, options) { ... }

// TS 写法:看一眼就知道怎么用
function fetchData(url: string, options?: {
  method?: 'GET' | 'POST'
  timeout?: number
}): Promise<any> { ... }

1.5 环境搭建与第一个 TS 程序

1. 安装 TypeScript

# 全局安装
npm install -g typescript

# 验证安装
tsc --version

2. 第一个 TS 程序

创建一个 hello.ts 文件:

// hello.ts
function greet(name: string): string {
  return `Hello, ${name}!`
}

const message = greet('TypeScript')
console.log(message)

3. 编译运行

# 编译成 JS
tsc hello.ts

# 生成 hello.js,然后运行
node hello.js

# 或者用 ts-node 直接运行
npx ts-node hello.ts

4. 在线体验

不想安装环境?直接访问:TypeScript Playgroundhttps://www.typescriptlang.org/play

在线写 TS,实时看编译结果

5. 第一个报错

function greet(name: string): string {
  return `Hello, ${name}!`
}

greet(123)  // 报错:参数类型不匹配

编译时会报错:

error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

2. 基础类型

2.1 类型注解

1. 什么是类型注解?

类型注解就是告诉 TypeScript,这个变量是什么类型。

// 语法:变量名: 类型
let name: string = '张三'
let age: number = 18
let isActive: boolean = true

2. 为什么要加类型注解?

// 不加注解:TS 会自己推断
let name = '张三'  // TS 推断 name 是 string 类型
name = 123         // 报错:不能把 number 赋给 string

// 加了注解:明确告诉 TS 你的意图
let age: number = 18
age = '二十'        // 报错

类型注解不是必须的,TS 会自动推断。但加注解可以让代码更清晰。

3. 函数参数和返回值

// 参数加类型,返回值也加类型
function add(a: number, b: number): number {
  return a + b
}

// 如果返回值类型不对,TS 会报错
function wrongAdd(a: number, b: number): number {
  return '结果'  // 报错:不能返回 string
}

2.2 原始类型

1.字符串 string

let name: string = '张三'
let greeting: string = "Hello"
let template: string = `你好,${name}`

// 错误示例
let city: string = 123  // 不能把 number 赋给 string

2. 数字 number

let age: number = 18
let price: number = 99.9
let hex: number = 0xff   // 十六进制
let binary: number = 0b1010  // 二进制
let infinity: number = Infinity

// 错误示例
let count: number = '100'  // 不能把 string 赋给 number

3. 布尔值 boolean

let isLoggedIn: boolean = true
let isAdmin: boolean = false
let isActive: boolean = Boolean('hello')  // true

// 错误示例
let isDone: boolean = 'true'  // 不能把 string 赋给 boolean

4. 类型推断

类型注解可以不写

// 不写类型注解,TS 会自动推断
let name = '张三'      // TS 推断 name 是 string
let age = 18           // TS 推断 age 是 number
let isActive = true    // TS 推断 isActive 是 boolean

// 后面赋值错误类型会报错
name = 123  // 报错:不能把 number 赋给 string

2.3 数组与元组

1. 数组 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> = ['苹果', '香蕉', '橙子']

// 错误示例
let ages: number[] = [18, 20, '二十五']  // 不能放 string

2. 元组 Tuple

元组是固定长度、固定类型顺序的数组。

// 定义元组:[string, number] 表示第一个是 string,第二个是 number
let user: [string, number] = ['张三', 18]

// 正确
user = ['李四', 20]

// 错误
user = [18, '李四']     // 顺序错了
user = ['王五']         // 长度不够
user = ['赵六', 22, '北京']  // 长度超了

什么时候用元组?

// 模拟 API 响应
let response: [boolean, string] = [true, '请求成功']

// 坐标
let position: [number, number] = [100, 200]

// CSV 行数据
let row: [string, number, boolean] = ['张三', 18, true]

2.4 any、unknown、void、never

1. 任意类型 any

any 表示「可以是任何类型」,相当于关闭了类型检查

let something: any = 'hello'
something = 123        //  可以
something = true       //  可以
something.toUpperCase() //  可以(虽然 number 没有 toUpperCase)

// 危险:用了 any,TS 的保护就没了
let data: any = { name: '张三' }
console.log(data.age.toFixed())  // 运行时才报错,TS 不阻止

尽量少用 any,它会让你失去 TS 的保护。

2. 未知类型 unknown

unknown 是 any 的安全版本。

let userInput: unknown = 'hello'

// unknown 不能直接使用
userInput.toUpperCase()  // 报错:unknown 不能直接调用方法

// 需要先「收窄类型」
if (typeof userInput === 'string') {
  userInput.toUpperCase()  // 现在可以了
}

any vs unknown:any 放弃了检查,unknown 要求你先检查再使用。

3. 无返回值 void

void 表示函数没有返回值。

// 没有 return
function logMessage(message: string): void {
  console.log(message)
  // 没有 return,或者 return; 或者 return undefined;
}

// 有 return,但返回 undefined
function doNothing(): void {
  return
}

// 错误示例
function wrong(): void {
  return 'hello'  // 不能返回 string
}

4. 永远不会发生 never

never 表示函数永远不会有返回值(要么一直运行,要么抛出错误)。

// 抛出错误,不会正常结束
function throwError(message: string): never {
  throw new Error(message)
}

// 无限循环,不会结束
function infiniteLoop(): never {
  while (true) {
    console.log('无限循环')
  }
}

// 不可能到达的分支
function checkType(value: string | number) {
  if (typeof value === 'string') {
    // value 是 string
  } else if (typeof value === 'number') {
    // value 是 number
  } else {
    // 这里 value 是 never,因为不可能到这儿
    const exhaust: never = value
  }
}

2.5 联合类型与类型别名

1. 联合类型 Union Type

一个变量可能是多种类型之一,用 | 分隔。

// id 可以是 string 或 number
let id: string | number = 'abc123'
id = 456  // 可以
id = true // 不能 是 boolean

// 函数参数可以是多种类型
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

给类型起个名字,方便复用。

// 定义类型别名
type UserID = string | number
type User = {
  id: UserID
  name: string
  age: number
}

// 使用
let userId: UserID = 'abc123'
let user: User = {
  id: 1,
  name: '张三',
  age: 18
}

// 更复杂的例子
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. 自动推断

TypeScript 很聪明,很多情况下不需要你写类型注解。

// 变量推断
let name = '张三'      // 自动推断为 string
let age = 18           // 自动推断为 number
let isActive = true    // 自动推断为 boolean

// 数组推断
let numbers = [1, 2, 3]        // 推断为 number[]
let mixed = [1, 'hello', true] // 推断为 (string | number | boolean)[]

// 对象推断
let user = {
  name: '张三',
  age: 18
}  // 推断为 { name: string; age: number }

// 函数返回值推断
function add(a: number, b: number) {
  return a + b  // 自动推断返回值是 number
}

2. 什么时候需要写类型注解?

// 1. 函数参数(必须写)
function greet(name: string) { ... }  // 参数不能推断

// 2. 变量初始值为空时
let user: User | null = null

// 3. 函数返回值需要明确时
function getData(): Promise<User> { ... }

// 4. 类型太复杂,推断不准确时
let config: Config = { ... }

3. 最佳实践

// 简单情况:让 TS 推断
let name = '张三'
let numbers = [1, 2, 3]

// 复杂情况:手动标注
let user: User | null = null
function fetchData(): Promise<User[]> { ... }

// 不要过度标注
let age: number = 18  // 多余,TS 已经知道是 number
let age = 18  // 更好

3. 函数与类型

3.1 函数参数类型

1. 基本写法

// 给每个参数加上类型
function greet(name: string, age: number): void {
  console.log(`${name},${age}岁`)
}

// 调用时必须传正确类型 
greet('张三', 18)   // 正确
greet(123, 18)      // 参数1类型错误
greet('张三', '18') // 参数2类型错误

2. 箭头函数

// 参数加类型,返回值加类型
const add = (a: number, b: number): number => a + b

// 如果函数体有多条语句
const multiply = (a: number, b: number): number => {
  const result = a * b
  return result
}

3. 对象参数

// 参数是对象
function printUser(user: { name: string; age: number }): void {
  console.log(`${user.name},${user.age}岁`)
}

printUser({ name: '张三', age: 18 })  // 正确

// 用接口更清晰(后面会讲)
interface User {
  name: string
  age: number
}

function printUser(user: User): void {
  console.log(`${user.name},${user.age}岁`)
}

4. 回调函数参数

// 回调函数的参数也要加类型
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. 基本写法

// 在参数括号后面加 `: 类型`
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 可以自动推断返回值类型
function add(a: number, b: number) {
  return a + b  // TS 推断返回值是 number
}

function getFullName(first: string, last: string) {
  return `${first} ${last}`  // TS 推断返回值是 string
}

// 建议:复杂逻辑还是显式写返回值类型
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 | number

3.3 可选参数与默认参数

1. 可选参数 ?

// 用 ? 表示这个参数可以传,也可以不传
function greet(name: string, greeting?: string): string {
  if (greeting) {
    return `${greeting},${name}`
  } else {
    return `你好,${name}`
  }
}

greet('张三')           // "你好,张三"
greet('张三', '早上好')  // "早上好,张三"

// 可选参数必须在必选参数后面

// 错误:可选参数不能在必选参数前面
function wrong(age?: number, name: string) { }

2. 默认参数

// 参数默认值(同时也成了可选参数)
function greet(name: string, greeting: string = '你好'): string {
  return `${greeting},${name}`
}

greet('张三')           // "你好,张三"
greet('张三', '早上好')  // "早上好,张三"

// 默认参数可以放在必选参数前面(但不推荐)
function greet(greeting: string = '你好', name: string): string {
  return `${greeting},${name}`
}
greet('张三')  // 报错:第二个参数没传

3. 可选参数 vs 默认参数

对比可选参数 ?默认参数 = value
不传时undefined使用默认值
传 undefined 时undefined使用默认值
传 null 时nullnull
适用场景值可以是 undefined有明确的默认值

3.4 剩余参数

1. 什么是剩余参数?

把多个参数收集成一个数组,用 ... 语法。

// 传统方式:参数个数不固定很麻烦
function sum1(a: number, b: number, c: number) { }  // 只能3个

// 剩余参数:可以传任意多个
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()                // 0

2. 剩余参数与其他参数

// 剩余参数必须是最后一个参数
function introduce(name: string, age: number, ...hobbies: string[]): void {
  console.log(`${name},${age}岁`)
  console.log(`爱好:${hobbies.join('、')}`)
}

introduce('张三', 18, '读书', '跑步', '游泳')
// 输出:
// 张三,18岁
// 爱好:读书、跑步、游泳

3. 剩余参数的类型

// 剩余参数是数组类型
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, '北京')  // 错 只能两个参数

3.5 函数重载

一个函数多种用法

1. 什么是重载?

同一个函数名,根据参数类型或数量的不同,执行不同的逻辑。

// JavaScript 中常见:参数不同,行为不同
function getData(id) {
  if (typeof id === 'string') {
    // 通过字符串 ID 获取
  } else if (typeof id === 'number') {
    // 通过数字 ID 获取
  } else if (Array.isArray(id)) {
    // 通过 ID 数组批量获取
  }
}

2. TypeScript 的重载写法

// 1. 先声明重载签名(多个)
function getData(id: string): User
function getData(id: number): User
function getData(ids: number[]): User[]

// 2. 再实现签名(一个)
function getData(id: any): any {
  if (typeof id === 'string') {
    // 获取单个用户(通过字符串 ID)
    return { id, name: '张三' }
  } else if (typeof id === 'number') {
    // 获取单个用户(通过数字 ID)
    return { id, name: '李四' }
  } else if (Array.isArray(id)) {
    // 获取多个用户
    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. 更实用的例子

// 重载签名
function formatDate(timestamp: number): string
function formatDate(date: Date): string
function formatDate(year: number, month: number, day: number): string

// 实现
function formatDate(a: any, b?: any, c?: any): string {
  if (typeof a === 'number' && b === undefined && c === undefined) {
    // 时间戳
    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())          // 当前日期
formatDate(2024, 1, 1)          // 2024-01-01

4. 什么时候用重载?

场景是否用重载
参数类型不同,逻辑差异大
参数个数不同
只是可选参数用可选参数就够了
用联合类型能解决优先用联合类型

3.6 泛型入门

让类型也变成参数

1. 为什么需要泛型?

假设你要写一个函数,返回传入的参数:

// 只能处理 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()  // 运行时没问题,但 TS 不知道类型

2. 泛型解决

用类型参数

// <T> 是一个类型变量,表示「调用时确定的类型」
function identity<T>(value: T): T {
  return value
}

// 使用
const num = identity<number>(100)     // num 类型是 number
const str = identity<string>('hello') // str 类型是 string

// TS 会自动推断类型,可以省略 <类型>
const num2 = identity(100)   // 自动推断为 number
const str2 = identity('hello') // 自动推断为 string

3. 泛型约束

限制类型范围

// 不加约束:T 可以是任何类型
function getLength<T>(value: T): number {
  return value.length  // 报错: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(数组有 length)
getLength(123)          // 报错:number 没有 length

4. 多个泛型参数

// 可以同时用多个类型参数
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. 泛型在实际项目中的应用

// 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. 封装数组工具函数
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]
}

const firstNumber = getFirst([1, 2, 3])  // 类型是 number | undefined
const firstString = getFirst(['a', 'b']) // 类型是 string | undefined

4. 接口与类

4.1 什么是接口?

定义对象的形状

1. 接口的作用

接口用来定义对象应该长什么样,描述对象有哪些属性、属性是什么类型。

// 定义接口
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
}

// 但接口更推荐用于对象,有更好的错误提示和扩展性

3. 接口的实际用途

// 1. 定义 API 响应格式
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' }  // 传了 email

2. 只读属性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'   // 报错:不能修改只读属性

3. 实际应用

// 1. 用户信息(ID 不应该被修改)
interface User {
  readonly id: number
  name: string
  email: string
}

// 2. 坐标点(x, y 不应该被修改)
interface Point {
  readonly x: number
  readonly y: number
}

// 3. 配置(不应该被修改)
interface AppConfig {
  readonly version: string
  readonly apiUrl: string
  debug: boolean  // 这个可以动态改
}

4.3 索引签名

动态属性名

1. 什么时候需要索引签名?

当对象的属性名不确定时,比如字典、配置对象。

// 场景:字典对象,key 是字符串,value 是数字
interface StringMap {
  [key: string]: number
}

const scores: StringMap = {
  math: 90,
  english: 85,
  chinese: 92
}
scores.physics = 88  // 可以动态添加

2. 索引签名的类型

// key 是 string,value 是 string
interface Dictionary {
  [key: string]: string
}

const dict: Dictionary = {
  hello: '你好',
  world: '世界'
}

// key 是 number,value 是 string(数组索引)
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. 实际应用

// 1. 环境变量
interface Env {
  [key: string]: string | undefined
  NODE_ENV: 'development' | 'production'
  PORT: string
}

// 2. 错误消息
interface ErrorMessages {
  [errorCode: string]: string
}

const errors: ErrorMessages = {
  '400': '请求参数错误',
  '401': '未授权',
  '404': '资源不存在'
}

4.4 接口继承

复用接口

1. 为什么要继承?

避免重复定义相同的属性。

interface Animal {
  name: string
  age: number
}

// 不继承:重复写相同属性
interface Dog {
  name: string
  age: number
  breed: string
}

// 用继承:复用 Animal 的属性
interface Dog extends Animal {
  breed: string
}

const myDog: Dog = {
  name: '旺财',
  age: 3,
  breed: '金毛'
}

2. 多接口继承

interface HasName {
  name: string
}

interface HasAge {
  age: number
}

interface HasEmail {
  email: string
}

// 继承多个接口
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 类与接口的区别

1. 类 Class

类是创建对象的蓝图,包含属性和方法的具体实现。

class Animal {
  name: string
  
  constructor(name: string) {
    this.name = name
  }
  
  speak(): void {
    console.log(`${this.name}发出声音`)
  }
}

const dog = new Animal('旺财')
dog.speak()  // 实际执行代码

2. 接口 Interface

接口只定义形状,不包含实现。

interface Animal {
  name: string
  speak(): void
}

// 类可以实现接口
class Dog implements Animal {
  name: string
  
  constructor(name: string) {
    this.name = name
  }
  
  speak(): void {
    console.log(`${this.name}汪汪叫`)
  }
}

3. 关键区别

对比类(Class)接口(Interface)
有没有实现只有声明
能不能 new可以不能
编译后存在(变成 JS 代码)不存在(编译后消失)
用途创建对象定义形状

4. 实现多个接口

interface Flyable {
  fly(): void
}

interface Swimmable {
  swim(): void
}

// 一个类可以实现多个接口
class Duck implements Flyable, Swimmable {
  fly(): void {
    console.log('鸭子飞')
  }
  
  swim(): void {
    console.log('鸭子游泳')
  }
}

4.6 类型别名 vs 接口

1. 对比表格

对比类型别名(type)接口(interface)
定义对象可以可以
定义原始类型可以(type Name = string不能
定义联合类型可以(type ID = string | number不能
定义元组可以不能
继承/扩展&(交叉类型)extends
同名合并报错自动合并
推荐用于复杂类型、联合类型、工具类型对象形状、类实现

2. 什么时候用 type?

// 1. 联合类型
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. 什么时候用 interface?

// 1. 定义对象形状(推荐)
interface User {
  name: string
  age: number
}

// 2. 类实现
interface Runnable {
  run(): void
}
class Dog implements Runnable { ... }

// 3. 需要扩展时
interface Animal {
  name: string
}
interface Dog extends Animal {
  breed: string
}

// 4. 需要同名合并时(比如给第三方库补充类型)
interface Window {
  myCustomProperty: string
}

4. 实际项目中的选择

// 大多数情况用 interface(更标准)
interface Product {
  id: number
  name: string
  price: number
}

// 联合类型用 type
type ProductStatus = 'inStock' | 'outOfStock' | 'discontinued'

// 组合时用 type
type ProductWithStatus = Product & { status: ProductStatus }

5. 泛型

5.1 为什么需要泛型?

1. 一个重复代码的问题

假设你要写一个函数,返回传入的参数:

// 只能处理 number
function identityNumber(value: number): number {
  return value
}

// 只能处理 string
function identityString(value: string): string {
  return value
}

// 只能处理 boolean
function identityBoolean(value: boolean): boolean {
  return value
}

三个函数逻辑完全一样,只是类型不同。能不能写一个函数搞定?

2. 用 any 的问题

function identityAny(value: any): any {
  return value
}

const result = identityAny('hello')
result.toUpperCase()  // 运行时没问题,但 TS 不知道 result 是 string
result.toFixed()      // 运行时报错,但 TS 不阻止

 any让你失去了类型保护。

3. 用联合类型的问题

function identityUnion(value: string | number): string | number {
  return value
}

const result = identityUnion('hello')
// result 类型是 string | number,不能直接调用 toUpperCase()
result.toUpperCase()  // 报错:可能不是 string

4. 泛型的解决方案

// <T> 是一个类型变量,调用时确定
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

// 调用时不用写类型,TS 自动推断
const result = identity('hello')
result.toUpperCase()  // TS 知道是 string

5.2 泛型函数

1. 基本写法

// 函数声明
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]
}

// 箭头函数
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 | undefined

2. 多个泛型参数

// 交换数组中的两个元素
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. 在实际项目中的应用

// 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 响应接口(实际应用)

// 通用的 API 响应格式
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. 实际应用:缓存类

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  // 报错: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(数组有 length)
getLength({ length: 10 })  // 10
getLength(123)         // 报错:number 没有 length

3. 多种约束

// 约束 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: '王五' })  // 缺少 age

4. 约束为某个类的实例

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 类型是 Dog

5.6 多个泛型参数

1. 基本用法

// 合并两个对象
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. 泛型约束 + 多个参数

// 约束 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')  // 报错

5.7 内置泛型工具

Partial、Pick、Omit、Record

1. 所有属性变成可选 Partial

interface User {
  id: number
  name: string
  age: number
  email: string
}

// 所有属性都变成可选
type PartialUser = Partial<User>
// 等价于 { id?: number; name?: string; age?: number; email?: string }

// 适用场景:更新用户(只传要改的字段)
function updateUser(id: number, updates: Partial<User>): void {
  // 只更新传进来的字段
}
updateUser(1, { name: '新名字' })  // 只传 name
updateUser(1, { age: 19 })        // 只传 age

2. 挑选部分属性 Pick

interface User {
  id: number
  name: string
  age: number
  email: string
  password: string
}

// 只挑出需要的属性
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// 等价于 { id: number; name: string; email: string }

// 适用场景:返回给前端的数据(不包含密码)
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'>
// 等价于 { id: number; name: string; age: number; email: string }

// 排除多个属性
type UserWithoutPasswordAndAge = Omit<User, 'password' | 'age'>

// 适用场景:创建用户(不需要 id)
type CreateUser = Omit<User, 'id'>

4. 创建键值对类型 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 是数字,value 是字符串
type WeekdayMap = Record<1 | 2 | 3 | 4 | 5 | 6 | 7, string>
const weekdays: WeekdayMap = {
  1: '周一',
  2: '周二',
  3: '周三',
  4: '周四',
  5: '周五',
  6: '周六',
  7: '周日'
}

5. 内置泛型工具速查表

工具作用示例
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>创建键值对类型Record<string, number>
Exclude<T, U>排除联合类型中的某些Exclude<'a' | 'b' | 'c', 'a'>
Extract<T, U>提取联合类型中的某些Extract<'a' | 'b' | 'c', 'a' | 'b'>
NonNullable<T>排除 null 和 undefinedNonNullable<string | null>
ReturnType<T>获取函数返回值类型ReturnType<typeof fn>
Parameters<T>获取函数参数类型Parameters<typeof fn>

6. 类型操作

6.1 获取对象的键 keyof

1. 基本用法

keyof 获取对象类型的所有键,组成联合类型。

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'             // 报错

2. 实际应用:类型安全的属性访问

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')  // 报错

function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value
}

setProperty(user, 'name', '李四')  
setProperty(user, 'age', '二十')    // 报错:不能把 string 赋给 number

3. 遍历对象的键

function printObject<T>(obj: T): void {
  for (let key in obj) {
    // 需要类型断言,因为 key 是 string,不一定是 keyof T
    console.log(`${key}: ${obj[key as keyof T]}`)
  }
}

6.2 获取变量的类型 typeof

1. 基本用法

typeof 在 TypeScript 中获取变量的类型(不是运行时的值)。

const user = {
  name: '张三',
  age: 18,
  city: '北京'
}

// 获取 user 的类型
type User = typeof user
// 等价于 { name: string; age: number; city: string }

const anotherUser: User = {
  name: '李四',
  age: 20,
  city: '上海'
}

2. 实际应用:避免重复定义类型

// 不用 typeof:重复定义
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retry: 3
}

interface Config {
  apiUrl: string
  timeout: number
  retry: number
}

// 用 typeof:自动推导
type Config = typeof config

// 在函数中使用
function initApp(config: typeof config) {
  console.log(`API: ${config.apiUrl}`)
}

3. 获取函数类型

function add(a: number, b: number): number {
  return a + b
}

type AddFunction = typeof add
// 等价于 (a: number, b: number) => number

const multiply: AddFunction = (a, b) => a * b

4. 与 keyof 结合

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']  // string

2. 联合类型的索引访问

interface User {
  id: number
  name: string
  age: number
}

// 获取多个属性的类型(联合类型)
type IdOrName = User['id' | 'name']  // number | string

// 获取所有值的类型
type UserValues = User[keyof User]  // number | string

3. 数组元素的类型

type StringArray = string[]
type ItemType = StringArray[0]  // string
type ItemType2 = StringArray[number]  // string

// 实际应用:获取数组元素的类型
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. 提取函数返回值类型

function fetchUser(): Promise<{ id: number; name: string }> {
  return Promise.resolve({ id: 1, name: '张三' })
}

// 提取 Promise 里的类型
type User = Awaited<ReturnType<typeof fetchUser>>
// 等价于 { 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. 实际应用:排除 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. 实际应用:提取函数返回值类型

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>  // number

4. 分布式条件类型

type ToArray<T> = T extends any ? T[] : never

type A = ToArray<string | number>
// 等价于 string[] | number[](不是 (string | number)[])

6.5 映射类型

1. 基本用法

映射类型可以基于旧类型创建新类型。

// 把所有属性变成只读
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. 什么是类型断言?

当 TypeScript 无法推断类型,但你知道更准确的类型时,可以「断言」。

// 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. 处理联合类型
function getLength(value: string | number): number {
  // 告诉 TS:这里 value 一定是 string
  return (value as string).length
}

3. 双重断言(危险)

const value: string = 'hello'
// 直接断言成 number 会报错
// const num = value as number  // 报错

// 双重断言:先转成 any,再转成目标类型(危险,不推荐)
const num = value as any as number  // 可能有问题

4. 非空断言 !

当你确定某个值不是 null 或 undefined 时,可以用 !

interface User {
  name?: string
}

function getUserName(user: User): string {
  // 告诉 TS:user.name 一定存在
  return user.name!  // 如果实际是 undefined,运行时会报错
}

// DOM 元素
const element = document.getElementById('app')!
element.innerHTML = 'Hello'

5. 类型断言 vs 类型转换

对比类型断言类型转换
运行时不影响会转换值
语法as stringString(value)
作用告诉 TS 类型实际转换值
// 类型断言:不转换值,只告诉 TS
const num = 123
const str = num as any as string  // 运行时 str 还是 123(数字!)

// 类型转换:实际转换值
const realStr = String(num)  // 运行时 realStr 是 "123"

恭喜你完成了TS的学习,明天还有一篇实战!

总结

到此这篇关于TypeScript前端的必修课之从JS到TS的文章就介绍到这了,更多相关TS前端必修课内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文