TS学习(3)交叉类型与泛型深度解析

🔥keyipatience:个人主页

🎬作者简介:C/C++后端开发学习者

🌟专栏传送门:《c++》《linux》《c++高阶数据结构》《c++数据结构与算法》《前端学习》

⭐️patience is key in life

TS 交叉类型 &(Intersection Type)

A & B 代表这个值必须同时满足 A 和 B 两个类型,拥有两者全部属性 ,和 extends 作用很像,但底层逻辑不一样

复制代码
interface Person { 
  name: string 
}
interface Contact { 
  phone: string 
}
// 交叉类型:合并两个接口
type PersonDetail = Person & Contact

// ✅ 必须同时有 name 和 phone,少一个就报错
let obj: PersonDetail = {
  name: 'jack',
  phone: '133...'
}

等价于:

复制代码
type PersonDetail = { name: string; phone: string }

&(交叉) vs extends(接口继承)

共同点:都能合并对象类型

不同点(高频考点):

1.interface B extends A :是接口的继承拓展,同名属性类型冲突 → 直接报错

复制代码
interface A { x: string }
// 报错:x 类型不匹配
interface B extends A { x: number }

//接口继承 extends
interface A {
  fn: (value: number) => string
}
interface B extends A {
  fn: (value: string) => string
}
// ❌ 直接报错!

2.type B = A & C :是类型交叉合并 ,同名属性冲突不会立刻红错,而是这个属性变成 string & number → 也就是 never(不存在的类型,没法赋值)

复制代码
type A = { x: string }
type B = { x: number }
type C = A & B 
// C里的 x: string & number → 变成 never
// 不存在一个值,既是string同时又是number,所以这个属性不可能存在

// 交叉类型 &
interface A {
  fn: (value: number) => string
}
interface B {
  fn: (value: string) => string
}
type C = A & B
// ✅ 不报错!最终 fn 类型:(value: string | number) => string

TS 泛型

泛型 = 类型的 "占位符 / 变量" ,先不固定类型,调用的时候再确定类型,兼顾复用 + 类型安全,解决 any 丢失类型信息的问题。

方案 1:写死 number(不好)

复制代码
function id(value: number): number { 
  return value 
}

缺点:只能传数字,传字符串、对象直接报错,没法复用。

方案 2:any(更不好,类型不安全)

复制代码
function id(value: any): any { 
  return value 
}
  • 可以传任意类型
  • 丢类型信息 :你传 string,返回值 TS 不知道它是 string,它就是any,没有提示、不会校验,等于白写 TS

方案 3:泛型

复制代码
// <T> 就是泛型,T 是类型占位符(名字随便取,常用T、U)
function id<T>(value: T): T {
  return value
}

含义: 调用的时候,T 会自动变成你传入值的类型,参数类型 = 返回值类型,类型信息保留

复制代码
id(10)       // T = number,返回 number
id('hello')  // T = string,返回 string
id(true)     // T = boolean,返回 boolean

也可以手动指定类型:

复制代码
id<string>('abc')

对比:

复制代码
// any版:返回值没有类型
const res1 = id_any("hi") // res1: any

// 泛型版:类型跟着进来
const res2 = id("hi")     // res2: string

泛型简化写法

复制代码
function id<Type>(value: Type): Type {
  return value
}

两种调用写法:

复制代码
// ✅ 显式指定类型(写死<number>)
let num = id<number>(10)

// ✅ 省略<类型>,靠TS自动推断(推荐)
let num = id(10)

1.可以省略 <Type> 不用手动写 <number>,代码更简洁。

2.类型参数推断 TS 看你传进去的实参 10,自动识别它是 number,自动把 Type 赋值成 number。

传 id('abc') → TS 推断 Type = string

3.什么时候不能省略,必须手写 <类型>? 当 TS 猜不出来、或者猜出来的类型不是你想要的时候,就要显式传。

复制代码
function id<T>(v: T): T { return v }

// 自动推断:T 是字面量类型 'hi'
let a = id('hi') 
// a 的类型是字面量 'hi',而不是 string

// 如果你想要的是宽泛的 string,而不是字面量 'hi',就手动写
let b = id<string>('hi')


// 【我想要的类型 > 传入值自带的类型】
const res = id<string | number>('123')
// 输入是string,但我主动要求泛型T是 string|number

泛型约束(extends)

复制代码
function id<Type>(value: Type): Type {
  console.log(value.length) //  报错!
  return value
}

原因:Type 默认可以是任意类型。

  • 如果传字符串 "abc":有 .length
  • 如果传数字 123:number 没有 .length TS 不知道你以后会传什么,为了类型安全,直接不让你访问 .length

方式 1:指定更具体的类型(Type[])

复制代码
function id<Type>(value: Type[]): Type[] {
  console.log(value.length)
  return value
}

参数写成 Type[],代表这是一个元素类型为 Type 的数组 。 数组天生一定有 .length 属性,所以 TS 允许访问 value.length,不再报错。

复制代码
id([1,2,3])    // ✅ 合法
id("abc")      // ❌ 报错!字符串不是数组

方式2 泛型约束 Type extends { length: number }

意思:Type 必须是拥有 length:number 属性的类型(字符串、数组都满足)

复制代码
interface Lengthwise {
  length: number
}

// 约束:Type 必须满足 Lengthwise 的结构
function id<Type extends Lengthwise>(value: Type): Type {
  console.log(value.length) // ✅ 不再报错
  return value
}

现在调用:

复制代码
interface Lengthwise {
  length: number
}

// 约束:Type 必须满足 Lengthwise 的结构
function id<Type extends Lengthwise>(value: Type): Type {
  console.log(value.length) // ✅ 不再报错
  return value
}

简写写法(不用单独写 interface)

复制代码
function id<Type extends { length: number }>(value: Type): Type {
  console.log(value.length)
  return value
}

多泛型 + keyof

复制代码
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}

let person = { name: 'jack', age: 18 }
getProp(person, 'name')

逐段拆解

  1. <Type, Key>:多个泛型变量,逗号隔开
    • Type:代表整个对象的类型
    • Key:代表属性名的类型
  2. keyof Type keyof 是 TS 运算符:拿一个对象类型,提取它所有 key,变成字面量联合类型 这里 keyof typeof person = 'name' | 'age'
  3. Key extends keyof Type 约束:Key 必须是这个对象里真实存在的 key

效果演示

复制代码
getProp(person, 'name') // ✅ 合法
getProp(person, 'age')  // ✅ 合法
getProp(person, 'sex')  // ❌ 直接红色报错!person没有sex

同样number、string、数组本身也都是 TS 里的对象类型,它们也有自己的 key

复制代码
// 泛型函数:获取对象/原始包装类型上指定key对应的属性值
// Type:传入的数据的类型
// Key extends keyof Type:Key必须是Type类型里存在的键
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
    // 返回对应key的属性,TS自动推导出返回值类型
    return obj[key]
}

// 1. 普通对象,取age属性 ✅
getProp({ name: 'jack', age: 18 }, 'age')
// 2. 普通对象,取name属性 ✅
getProp({ name: 'jack', age: 18 }, 'name')

// 3. number类型,toFixed是number自带方法 ✅
getProp(18, 'toFixed')
// 4. string类型,split是字符串自带方法 ✅
getProp('abc', 'split')
// 5. 字符串支持数字索引取值,'abc'[1] 获取第二个字符 ✅
getProp('abc', 1) 
// 6. 数组读取length属性 ✅
getProp(['a'], 'length')
// 7. 数组支持数字索引;TS只校验类型,不判断下标是否越界 ✅
getProp(['a'], 1000)

// 验证:字符串可以用数字下标访问
console.log('object'[1]) // b

// ❌ 错误演示:对象上没有 name1 这个key,TS直接报错
// getProp({ name: 'jack', age: 18 }, 'name1')

泛型接口

泛型接口 = 给接口套上泛型,让这个接口可以复用,适配多种不同类型,不用重复写很多几乎一样的接口

复制代码
// 定义泛型接口 IdFunc,Type 是类型变量(占位符)
interface IdFunc<Type> {
  id: (value: Type) => Type
  ids: () => Type[]
}

// 使用泛型接口,显式传入 number 替换 Type
let obj: IdFunc<number> = {
  id(value) { return value },
  ids() { return [1, 3, 5] }
}
  1. 接口名后面加 <类型变量> → 这个接口就是泛型接口
  2. 这个类型变量在整个接口内部都能用,接口里所有属性 / 方法都可以引用它
  3. 使用泛型接口的时候,必须指定具体类型 ,例子里就是 IdFunc<number>
  4. 代入 number 之后:
    • id:接收 number,返回 number
    • ids:无参,返回 number[](数字数组)

TS 里的Array<T>就是内置泛型接口

复制代码
// 写法等价:string[]  === Array<string>
const strs = ['a', 'b', 'c'] 
// TS自动推断 → strs: Array<string>

// number[]  === Array<number>
const nums = [1, 3, 5]
// TS自动推断 → nums: Array<number>
  1. Array 本身是一个泛型接口 Array<Type> ,Type 代表数组元素的类型
  2. TS 会自动类型推导 ,不用你手动写 Array<string>:
    • 字符串数组 → 代入 Type = string → Array<string>
    • 数字数组 → 代入 Type = number → Array<number>
  3. 所以调用 .forEach 时,回调里的 value 类型就跟着自动变:
    • Array<string>.forEach:value 是 string
    • Array<number>.forEach:value 是 number

所以两种数组写法是等价的

复制代码
let arr1: string[] = ['x','y']
let arr2: Array<string> = ['x','y']

泛型类

复制代码
// 定义泛型类 GenericNumber,NumType 是类型变量
class GenericNumber<NumType> {
  defaultValue: NumType
  add: (x: NumType, y: NumType) => NumType
}

// 实例化,指定 NumType = number
const myNum = new GenericNumber<number>()
myNum.defaultValue = 10
  1. class 类名<类型变量> → 泛型类,和泛型接口写法套路一致
  2. 类型变量 NumType 在整个类内部都可用:
    • defaultValue 的类型是 NumType
    • add 接收两个 NumType 参数,返回 NumType
  3. 实例化的时候 new GenericNumber<number>(),把 NumType 替换成 number
    • 替换后:defaultValue: number,add:(x:number,y:number)=>number

类型能不能自动推导

复制代码
// 版本1:带构造函数,可以自动推导类型(可以省略 <number>)
class GenericNumber<NumType> {
  defaultValue: NumType
  add: (x: NumType, y: NumType) => NumType
  constructor(value: NumType) {
    this.defaultValue = value
  }
}
// 构造函数传了参数 100 → TS 自动推出 NumType = number
const myNum = new GenericNumber(100) // ✅ 不用写 <number>


// 版本2:没有构造函数,没有参数用来推导类型(推荐手动写 <number>)
class GenericNumber<NumType> {
  defaultValue: NumType
  add: (x: NumType, y: NumType) => NumType
}
// new 的时候没有传入任何值,TS没法猜 NumType 是什么类型
const myNum = new GenericNumber<number>() // ✅ 必须显式指定
myNum.defaultValue = 10
  • 有构造入参 :new GenericNumber(100),实参可以让 TS 自动推导泛型类型,<>可以省略
  • 无构造入参 :new GenericNumber(),没有信息用来推导,必须手动写 <number> ,否则泛型会变成 unknown

TS 内置泛型工具类型

全部是 TS 已经写好的泛型,开箱即用,底层都是泛型实现

先准备一个基础接口,后面例子统一用它:

复制代码
interface User {
  name: string
  age: number
  gender: string
}

1. Partial<Type>

作用:把 Type 的所有属性变成可选

复制代码
type PartialUser = Partial<User>
// 等价于:{ name?: string; age?: number; gender?: string }
const u: PartialUser = { name: "张三" } // 只写一部分属性也允许

2. Readonly<Type>

作用:把 Type 的所有属性变成只读,不能修改

复制代码
type ReadonlyUser = Readonly<User>
const u: ReadonlyUser = { name: "张三", age: 18, gender: "男" }
// u.name = "李四"  // ❌ 报错,只读无法修改

3. Pick<Type, Keys>

作用:从 Type 里面挑选指定的几个属性,生成新类型

复制代码
type PickUser = Pick<User, "name" | "age">
// 等价于:{ name:string; age:number }
const u: PickUser = { name:"张三", age:18 }

4. Record<Keys, Type>

作用:构造一个对象类型,键是 Keys 里的类型,值是 Type 类型(快速定义键值对字典)

复制代码
// Record<Keys, Type>
type RecordObj = Record<'a' | 'b' | 'c', string[]>
let obj: RecordObj = {
  a: ['1'],
  b: ['2'],
  c: ['3']
}

等价手写:

复制代码
type RecordObj = {
  a: string[]
  b: string[]
  c: string[]
}

约束效果: 只能有 a/b/c 这三个 key 每一个 key 的值,必须是字符串数组 多写属性、少写属性、值类型写错都会报错

拓展例子:

复制代码
type Status = "success" | "fail"
type MsgObj = Record<Status, string>
// { success: string; fail: string }
const msg: MsgObj = {
  success: "操作成功",
  fail: "操作失败"
}

索引签名类型

适用场景:不确定对象有多少属性、属性名不固定,允许任意多属性

字符串索引签名

复制代码
// 字符串索引签名:任意字符串key,值为number
interface AnyObject {
  [key: string]: number
}
let obj: AnyObject = {
  a: 1,
  abc: 124,
  abcde: 12345
}
  • [key: string]: number
    • key:只是占位名字,可以随便改(比如 [k:string])
    • string:代表属性名(键)必须是 string 类型
    • number:代表所有属性的值必须是 number

效果:可以随便加任意字符串属性:

复制代码
obj.c = 99
obj["xxx"] = 100

报错:值不是 number 就不行

复制代码
obj.d = "abc" // 报错

数字索引(对应数组)

复制代码
interface NumIndex {
  [index: number]: string
}
const arrLike: NumIndex = ["a", "b"]


// 数字索引签名,模拟数组
interface MyArray<Type> {
  [index: number]: Type
}
let arr1: MyArray<number> = [1, 3, 5]
arr1[0]

[index: number]: Type

  • index 是数字 → 模拟数组下标
  • arr1[0]、arr1[1] 通过数字下标访问元素

和 Record 的区分

  • 索引签名 :[k:string]:T → 键不限制,可以写任意字符串键

    type ObjType = { [k:string]: number }
    //a,b,c,d....都可以

  • Record<Keys,Type> :键提前限定在联合类型里,只能写指定的几个键

    type ObjType = Record<"a"|"b", number>
    //只能是a,b

  • 不知道有哪些 key → 索引签名
  • 提前知道只能有哪几个 key → Record
相关推荐
嘶哈哈哈1 小时前
PX4 使用 MTF-01 光流测距一体传感器:接线、参数配置与室内定点
1024程序员节
茉莉玫瑰花茶1 小时前
线性代数 [ 矩阵与线性变化 ]
线性代数·math·1024程序员节
m0_639906941 小时前
JVM 垃圾回收 GC 全解|判断对象死亡、GC 算法、4 种引用、CMS/G1/ZGC 收集器详解
1024程序员节
淡海水2 小时前
07-04-源码-.NET Core GC清扫与压缩
unity·架构·c#·.netcore·gc·1024程序员节
深频率2 小时前
上线一年就撤:微软关掉Outlook的AI邮件助理
人工智能·1024程序员节
橙光成长3 小时前
PDF离线工具箱深度解析:4MB纯前端单文件,40+功能全本地运行,文件绝不上传
前端·pdf·1024程序员节
可爱系程序猿7 小时前
佳能 MF645Cx Windows 11 驱动下载安装:UFR II 队列可打印但扫描入口缺失的排查
程序人生·电脑·1024程序员节
irislinAmelia10 小时前
缺陷检测AI关键点在哪
人工智能·嵌入式硬件·1024程序员节
Ai-_Man10 小时前
Meta Muse还是不可以导出左侧所有对话,只能点进去单条内容导出,方法不对吗?
开发语言·人工智能·ai·小程序·c#·1024程序员节