☰
TS学习(3)交叉类型与泛型深度解析
2026/10/26 2:00:05 网站建设 项目流程

🔥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 }

&(交叉) vsextends(接口继承)

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

不同点(高频考点):

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没有.lengthTS 不知道你以后会传什么,为了类型安全,直接不让你访问.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 Typekeyof是 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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询