TS 泛型的使用
大纲链接 §
[toc]
简单的泛型 Generic Types ⇧
一般类型(非泛型)的局限 V.S. 泛型的作用 ⇧
有一个直接返回参数的函数
|
|
- 以上的函数不能表示 参数类型需要与返回值类型相同的情况
使用泛型
|
|
- 泛型 可以理解为 不定类型,用来表示多种类型
- 将泛型的类型
string代入就可得到s的类型 - 一旦确定类型,将不同类型重复赋值相同变量名,也会类型报错
- 可以使用任意大写字母表示,一般使用尖括号来声明
<T>,在冒号后像类型一样使用
返回数组类型的例子 ⇧
|
|
不推荐的写法 ⇧
|
|
<T>(arg: T) => T和 函数中的类型声明<T>(arg: T): T等价
区别于类型断言 ⇧
类型断言,在编译时起作用
“尖括号” 用法
1 2let someValue: unknown = '' let strLength: number = (<string>someValue).lengthas语法1 2let someValue: unknown = '' let strLength: number = (someValue as string).length
泛型常用组合形式 ⇧
- 泛型 + 接口
interface x<T> {(a: T): T;}- 使用
let xxx: x<string> = (a) => a;
- 泛型 + 类型别名
type t<T> = {a: T;} - 多个泛型
type t<T, P> = {a: T; b: P} - 泛型 + 函数
function fn<T>(arg: T): T {}
- 泛型 + 类
class C<T> {attr: T; fn: (a: T) => T }- 使用
let cc = new C<string>()
约束 构造函数/类
new(): TJS工厂函数function create(C) {return new C()}- 使用泛型
function create<T>(C: {new (): T): T { return new C()} 表示 实例对象的类型 为
T;传递的参数为一个 类C1 2 3 4 5 6 7 8function create<T>(C: {new(): T}) { return new C() } class Human {} class Animal {} let P = create<Human>(Human) let P1 = create<Human>(Animal) // 报错 let S = create<String>(String)
泛型就像函数 ⇧
|
|
语法 ⇧
| 区别项 \ 类比项 | 名称 | 参数列表 可传默认项 |
主体 | 调用 | 返回项 |
|---|---|---|---|---|---|
| 函数 | 函数名 | 形参/实参 (默认值) |
函数体 | 函数调用 | 返回值 |
| 泛型 | 具名:类型别名/接口等 匿名:函数/类等 |
泛型参数 / 代入具体类型 (默认类型) |
类型主体(包含属性类型声明、类型集合、集合运算等逻辑) | 类型调用 | 返回新的类型 |
泛型是面向对象的经典概念之一 ⇧
- 泛型,即“参数化类型”,也是一种类型
- 泛型是一种接受其他类型的类型
也称为泛型函数
1 2 3 4 5 6 7type A = 'hi' | 123 // TS let a = ['hi', 123] type F<T> = T | T[] // type F_number = F<number> // type F_string = F<string> const fn = (x: number) => x + 1泛型:未被声明具体类型的类型(类似
unknown),type X<T> = T extends unknown ? T : never可以理解为类型的占位符
函数的本质是:
- 延后执行 一段逻辑,之前学过函数调用的时机,声明和执行分离
- 部分待定的代码作为参数,调用时才填入具体值
类比泛型
- 延后声明 具体类型,先用占位符表示,之后使用到时再替换为具体类型
部分待定的类型作为参数,调用时才填入具体类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15const add = (a: number, b: number) => a + b // const add = (a: string, b: string) => a + b type Add<T> = (a: T, b: T) => T // type AddNumber = Add<number> // type AddString = Add<string> const addN: Add<number> = (a, b) => a + b const addS: Add<string> = (a, b) => `${a} ${b}` function returnIt<T>(arg: T): T{ return arg; } let s = returnIt<string>('hi')
具体代码:码上掘金,鼠标滑过类型查看类型提示
以函数形参来类比泛型,实参就可以理解为:为泛型明确声明具体类型,可称为泛型的类型收窄
为什么会有泛型 ⇧
- 弱类型语言不需要泛型,比如
JS 具有复杂的类型系统的计算机语言需要泛型,比如
JAVA1 2 3function echo(whatever: string | number | boolean) { console.log(typeof whatever) }判断分支代码无法实现以一对应,存在类型不精确或错乱的情况
1 2 3 4 5 6 7 8 9// bad function echo(whatever: string | number | boolean) { switch(typeof whatever) case('string') case('number') case('boolean') default return whatever // ... }如果参数的类型和返回的类型有特殊的要求,比如一一对应,就需要泛型
判断分支代码无法实现以一一对应,存在类型不精确或错乱的情况
没有泛型,就无法实现复杂的逻辑需求
没有泛型的类型系统,就如同没有函数的编程语言
一星难度的 TS 体操:代入法 ⇧
|
|
- 鼠标悬浮的提示无法知道详细类型
- 可使用代入法理解
- 无需逻辑操作
可传类型默认值
|
|
二星难度的 TS 体操:条件类型 ⇧
补全类型
- 提示:使用条件类型
Conditional Types,即三元表达式 提示:类型的 关系运算符 只有一种:
extends意为包含于,表示类型集合的从属关系1 2 3 4 5 6 7 8 9 10 11 12 13 14 15type Person = {name: string} /* type LikeString<T> = ??? type LikeNumber<T> = ??? type LikePerson<T> = ??? */ // 补全上分代码,可实现 type S1 = LikeString<'hi'> // true type S2 = LikeString<true> // false type N1 = LikeNumber<8> // 1 type N2 = LikeNumber<false> // 2 type P1 = LikePerson<{name: 'frank', xxx: 1}> // yes type P1 = LikePerson<{xxx: 1}> // no
- 集合的关系判断无法像值那样,有大于小于或相等的判断,只有从属关系的判断
- 集合范围决定了是否为从属包含关系,范围完全一样的是一种特殊的情况
extends读作 “包含于”,而不是面向对象中的继承1 2 3type LikeString<T> = T extends string ? true : false type LikeNumber<T> = T extends number ? 1 : 2 type LikePerson<T> = T extends Person ? 'yse' : 'no'
条件类型的特殊情况 never ⇧
never可理解为空集,但在类型判断中,存在特殊情况1 2type LikeString<T> = T extends string ? true : false type X1 = LikeString<never> // X1: never 而不是 true
规则
- 若
<T>为联合类型,则各个类型,分开计算 若
<T>代入never,则表达式的值为never1 2 3 4 5 6 7 8 9 10 11 12type ToArray<T> = T extends unknown ? T[] : never type res = ToArray<string | number> // res 结果为 // (string | number)[] 类型 // string[] | number[] 类型 √ /* * 使用代入法 * (string | number) extends unknown ? ... * (string extends unknown ? ...) * | (number extends unknown ? ...) * string[] | number[] */
联合类型的条件判断,分开计算可以理解为 进行类型收窄时的处理过程
1 2 3 4 5 6 7type ToArray<T> = T extends unknown ? T[] : never type res = ToArray<never> /** * res 的类型为 * never[] * never √ */空集没有元素,直接返回
never
使用类比来理解
- 泛型 + 联合类型,比作乘法的 分配律
(A | B) extends X -> A extends X | B extends X never比作乘法中的0- 只对泛型有效
在泛型中使用 extends 泛型约束 ⇧
- 给泛型添加类型约束,缩小泛型的范围
关键字
extends,翻译为包含于(集合范围小于或等于),区别于类class的继承关键字1 2 3 4 5 6 7 8 9 10 11 12 13 14 15// 添加约束之前 function returnIt<T>(arg: T): T { console.log(arg.length) // error return arg; } // 添加约束之后 interface HasLength{ length: number } function returnIt<T extends HasLength>(arg: T): T { console.log(arg.length) // no error return arg; }声明的泛型
<T>必须满足 包含于HasLength接口
在泛型中使用 keyof ⇧
在泛型中使用 extends keyof ⇧
泛型的实际使用:在React中使用泛型 ⇧
- 安装
react和react-dom - 安装
@types/react和@types/react-dom Ctrl + 点击查看泛型是什么,使用代入法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18import React, { FunctionComponent, FC } from 'react' type P = { name: string } // 代入法 /* interface FunctionComponent<P = {}> = { (props: P, context?: any): ReactElement<any, any> | null; // ... } */ const App: FunctionComponent<P> = (props) => { console.log(props.name) console.log(props.children) return ( <div>hi</div> ) }
泛型 区别于 函数重载 ⇧
- 泛型:使用时声明具体类型类型
- 难以根据不同条件,约束所有参数的类型
函数重载:根据参数类型条件判断
支持多种属性签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17// 泛型 type Add<T> = (a: T, b: T) => T const add: Add<string | number> = (a, b) => { // 防止出现 a: string, b: number 的情况,必须使用 函数重载 return a + b } // 函数重载 function add2 (a: string, b: string): string; // 重载1 function add2 (a: number, b: number): number; // 重载2 function add2 (a: unknown, b: unknown): unknown { // 具体实现 if(typeof a === 'string' && b === 'string' ) return `${a} ${b}` if(typeof a === 'number' && b === 'number' ) return a + b } add2(1, 2) add2('1', '2') // add2('1', 2) // error
让函数满足多种的参数条件就是函数重载
如何用泛型封装网络请求库 ⇧
- 使用
axios时 可以根据参数的类型,来自动判断执行的逻辑分支axios.get('/url', {})axios.get({url: '/xxx', header: ''})axios.get(url?: string, options?: {url: string, header: unknown}): void
拿获取用户来举例
const user = createResource('/api/v1/user')1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33import axios from 'axios' type User = { id: string | number; name: string; age: number } type Resonse<T> = {data: T} type O = Omit<Partial<User>, 'id'> // type O2 = Partial<Omit<User, 'id'>> type CreateResource = (path: string) => { create: (arrts: O) => Promise<Resonse<User>>; delete: (id: User['id']) => Promise<Resonse<never>>; update: (id: User['id'], attrs: O) => Promise<Resonse<User>>; get: (id: User['id']) => Promise<Resonse<User>>; getPage: (page: number) => Promise<Resonse<User[]>>; } const createResource: CreateResource = (path) => { return { create(attrs) { const url = `${path}s` return axios.post<User>(url, {data: attrs}) }, delte(attrs) {}, update(attrs) {}, get(attrs) {}, getPage(attrs) {}, } } const user = createResource('/api/v1/user')
使用映射类型 in ⇧
难度为三星的泛型类型体操 ⇧
如何使用 -readonly ⇧
类型体操有多难 ⇧
如何提问 ⇧
stackblitz.comvanilla TS- 需要默认导出任意
export {}来消除报错
- 码上掘金
- 将有疑问的代码在平台上运行,保存后分享路径
url - 描述问题
参考文章 ⇧
相关文章 ⇧
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名