Typescript - 进阶篇
- 对基础篇的补充
- 泛型的使用方法和场景
- 常用泛型工具
- 配合 React 使用实例
#
三斜杠 + reference 导入将 TypeScript 类型声明拆分。使用 三斜杠指令 + reference 引用可以大大缩短构建和编辑器的交互时间,强制组件之间的逻辑分离,并以新的和改进的方式组织代码
相关文档:https://www.typescriptlang.org/docs/handbook/project-references.html
/// <reference path="xxx.ts" />
#
从文件中直接引用类型// ./xxx.tsexport type TypeName = { // ...}
type T = import('./xxx.ts').TypeName
- 这种引用方式不需要在文件头部写 import 声明,也不需要将类型导入全局。
- 主要优势是可以快速导入单个类型,但如果需要导入同一文件中的多个类型,还是推荐使用 import 声明。
#
namespace 命名空间namespace 主要用于类型分组,也可用于配合 class(类) 声明类型。
官方文档:https://www.typescriptlang.org/docs/handbook/namespaces.html#namespacing
namespace Shapes { export namespace Polygons { export interface Triangle {} export interface Square {} }
export interface Round {}}
const a: Shapes.Polygons.Triangle = {}
namespace 中的类型可以被重写
namespace Shapes { export interface Round { pi: number }
export namespace Polygons { export interface Triangle { side: 3 } }}
在全局类型声明文件 (*.d.ts) 中
export
关键字不是必须的。
#
模板字符串类型本质上就是字符串类型,通过在模板字符串 (``) 中插入类型,遍历出一个字符串联合类型。
type T = { a: number b: number c: string}
type T2 = 'x' | 'y' | 'z'
type T3 = `key-${keyof T}-${T2}-${number}`
// 结果:type T3 = | `key-a-x-${number}` | `key-a-y-${number}` | `key-a-z-${number}` | `key-b-x-${number}` | `key-b-y-${number}` | `key-b-z-${number}` | `key-c-x-${number}` | `key-c-y-${number}` | `key-c-z-${number}`
主要作用就是对字符串联合做更精准的类型约束。
#
动态索引声明type T = { [K: string]: unknown}
K
是索引签名参数,类型必须为 "string" 或 "number"。
K
不是固定名称,可以自定义语义话命名,如:
type T = { [StringKeys: string]: unknown [NumberKeys: number]: unknown}
#
keyof 将一个类型的索引映射为联合类型type T = { a: unknown b: unknown c: unknown}
type TKeys = keyof T // => 'a' | 'b' | 'c'
注意:如果包含动态索引,可能无法推断出指理想的联合类型。
type T = { [K: string]: unknown a: unknown}
type TKeys = keyof T // => string | number
type T = { [K: number]: unknown a: unknown}
type TKeys = keyof T // => number | "a"
#
inin 关键字可以在索引中使用,用于映射需要推断的联合类型。
const obj = { a: 1, b: 2, c: '3',}
type Keys = keyof typeof obj // 'a' | 'b' | 'c'
type T = { [K in Keys]: typeof obj[K]}
类型 T 的推断结果:
type T = { a: number b: number c: string}
#
索引引用type T = { a: number b: number c: string}
type T2 = T['a']type T3 = T['c']
// T2 = number// T3 = string
#
声明合并将多个相同名称的类型合并。详情查看文档
interface Box { height: number width: number}
interface Box { scale: number}
let box: Box = { height: 5, width: 6, scale: 10 }
以上内容是对基础篇的补充
#
泛型泛型就是可以传递参数并使用参数的类型。
#
泛型参数通过在类型名称后面添加一对 <>
来声明泛型参数。
interface RequestRes<T> { /** 结果码:1000成功 其他情况参考码表 */ businessCode?: string /** 返回对象 */ content?: T /** 返回消息 */ message?: string /** 响应描述 */ responseDes?: string /** 子对象标识 */ subEchoToken?: string}
type TypeA = RequestRes<{}>// {// businessCode?: string// content?: {}// message?: string// responseDes?: string// subEchoToken?: string// }
type TypeA = RequestRes<string[]>// {// businessCode?: string// content?: string[]// message?: string// responseDes?: string// subEchoToken?: string// }
泛型参数可以有多个,使用逻辑与函数一致。
type TypeA<A, B, C> = { a: A b: B c: C}
可以使用 =
赋予参数默认值,与 es6 语法中的函数默认值赋值方式一致。
type TypeA<T = unknown> = {}
没有默认值的泛型参数一律视为必填,如果在引用该泛型时没有传入,就会报错:
type TypeA<T> = {}type TypeB<T = unknown> = {}
type NewTypeA = TypeA // 报错type NewTypeB = TypeB // 不报错
#
在函数中使用泛型通过在函数声明的括号前添加一对 <>
来声明泛型参数,配合函数生命的泛型参数可以在函数体中引用。
function assign<T1, T2>(obj1: T1, obj2: T2): T1 & T2 { return Object.assign(obj1, obj2)}
const a = assign({ a: 1 }, { b: '2' })
// 推断结果:// const a: {// a: number;// } & {// b: string;// }
函数中声明泛型参数与直接声明形参类型有什么区别?
- 泛型参数可以被重复引用
- 泛型参数可以在函数调用时传入
- 在 IDE 中获得更好的类型推断
在什么情况下需要在函数中声明泛型?
- 函数无法自动动推断类型时
- 需要传入与函数形参无关的类型时
- 函数体中需要多次引用参数类型时
函数中的泛型参数与函数形参没有强关联性:
function pick<T = unknown>(obj: unknown, pickedArr: string[]): T { let result = Object.assign(obj)
if (obj !== null && typeof obj === 'object') { result = {} for (const key of pickedArr) { if (Object.prototype.hasOwnProperty.call(obj, key)) { result[key] = obj[key] } } }
return result}
const obj = { 0: 1, a: 1, b: 2, c: '3',}
// 调用 pick 函数时在括号前添加 <> 传入泛型参数const objH = pick<{ a: number; c: string }>(obj, ['a', 'c'])
// 结果:// const objH: {// a: number// c: string// }
在这个例子中泛型参数 T
与函数形参 obj
无关联,但被用作返回值的类型引用
#
泛型约束通过 extends
关键字可以对泛型参数进行类型约束,从而使该参数在后续使用和类型推断中更加精准
function pick<T extends Record<string | number, unknown>, K extends string[]>(obj: T, pickedArr: K): T { let result: T = Object.assign(obj)
if (obj !== null && typeof obj === 'object') { result = {} for (const key of pickedArr) { if (Object.prototype.hasOwnProperty.call(obj, key)) { result[key] = obj[key] } } }
return result}
上面这个例子中泛型参数 T
受到了类型 Record<string | number, unknown
的约束,那么 boj
参数在传入的时候必须满足该约束。
同理,pickedArr
参数在传入时需要满足 string[]
类型约束。
pick('aaa', ['a', 'c']) // 报错pick({ a: 1, b: 2, c: '3' }, 'a') // 报错pick({ a: 1, b: 2, c: '3' }, ['a', 'c']) // 通过
泛型约束与直接声明形参类型约束作用是相同的,区别在于泛型参数可以在函数体中的其他地方被引用。
不难发现上面这个 pick
方法的类型声明效果其实并不理想,我们希望该方法可以自动推断出传入的类型,pickedArr
的数组成员必须是 obj
中的已知属性,并且可以返回一个选取后的高精度类型。可以做以下优化:
function pick<T extends Record<string | number, unknown>, K extends keyof T>(obj: T, pickedArr: K[]): Pick<T, K> { let result = Object.assign(obj)
if (obj !== null && typeof obj === 'object') { result = {} for (const key of pickedArr) { if (Object.prototype.hasOwnProperty.call(obj, key)) { result[key] = obj[key] } } }
return result}
#
extends 三元判断例子 1:实现一个 If 泛型
type If<C extends boolean, T, F> = C extends true ? T : F
// type A = If<true, 'a', 'b'> // => 'a'// type B = If<false, 'a', 'b'> // => 'b'
例子 2:当函数参数为 string
时返回值类型为 string
,否则返回 number
type TypeName<P> = (param: P) => P extends string ? string : number
#
inferinfer 表示在 extends 条件语句中待推断的类型变量。
/** 获取 Promise 返回值 */type Awaited<T> = T extends Promise<infer U> ? U : never
/** 获取 Promise 返回值 (递归) */type AwaitedDeep<T> = T extends Promise<infer U> ? (U extends Promise<unknown> ? Awaited<U> : U) : never
#
内置泛型工具#
Partial - 将类型中所有属性转为非必填type T = { a: string b: string c?: string}
type T2 = Partial<T>
// type T2 = {// a?: string// b?: string// c?: string// }
#
Required - 将类型中所有属性转换为必填type T2 = Required<T>
// type T2 = {// a: string// b: string// c: string// }
#
Readonly - 将类型中所有属性转换为只读type T2 = Readonly<T>
type T2 = { readonly a: string readonly b: string readonly c?: string}
#
Pick - 从一个类型中选取某些属性得到一个新类型type T2 = Pick<T, 'a' | 'c'>
// type T2 = {// a: string// c: string// }
#
Omit - 从一个类型中排除某些属性得到一个新类型type T2 = Omit<T, 'a' | 'c'>
// type T2 = {// b: string// }
#
Record - 构建一个对象类型type T2 = Record<string, unknown>
// type T2 = {// [K:string]: unknown// }
type T2 = Record<keyof T, unknown>
// type T2 = {// a: unknown// b: unknown// c: unknown// }
#
Parameters - 获取函数的参数类型type T2 = Parameters<typeof pick>
// type T2 = [obj: Record<string | number, unknown>, pickedArr: (string | number)[]]
#
ConstructorParameters - 获取一个类的构造函数的参数class ClassName { constructor(props: Record<string, unknown>) {}}
type T2 = ConstructorParameters<typeof ClassName>
// type T2 = [props: Record<string, unknown>]
#
ReturnType - 获取函数返回值type T2 = ReturnType<typeof pick>
// type T2 = {// [x: string]: unknown;// [x: number]: unknown;// }
#
NonNullable - 从联合类型中排除 null 和 undefinedtype T = string | null | number | '1' | 2type T2 = NonNullable<T>
// type T2 = string | number
#
使用泛型工具实现类型重写type T = { a: string b: number}
interface T2 extends Omit<T, 'a'> { a: string[]}
// T2 = {// a: string[]// b: number// }
#
一些自定义泛型工具#
数组转联合type TupleToUnion<T extends readonly unknown[]> = T[number]
// TupleToUnion<[1, 2, '3']> // expected to be 1 | 2 | '3'
#
获取数组长度type Length<T extends readonly unknown[]> = T['length']
// Length<[0, 0, 0]> // expected 3
#
数组转对象type TupleToObject<T extends readonly unknown[]> = { [P in T[number]]: P}
// TupleToObject<['a', 'b', 'c']> // expected {a:'a', b:'b', c:'c' }
#
获取数组第一个元素type First<T extends readonly unknown[]> = T['length'] extends 0 ? never : T[0]
// First<['a', 'b', 'c']]> // expected to be 'a'
#
拼接两个数组type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
// Concat<['a', 'b'], ['c']]> // expected to be ['a', 'b', 'c']
#
Includestype Includes<T extends readonly unknown[], U> = U extends T[number] ? true : false
#
函数重载 (Overload)用于根据单个函数传入参数的不同来分配不同的返回值类型类型。
相关文档: https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads
例子:
interface ConfigType { a?: string b?: string c?: number}
function getConfig(key?: keyof ConfigType) { let config: ConfigType = {}
try { config = JSON.parse(localStorage.getItem('app-config')) } catch {}
if (key) { return config[key] } else { return config }}
- 如果传入 key, 那么从对象
config
中取出并返回这个 key 对应的值。 - 如果不传入, 那么将返回完整的
config
对象。
TS 可以自动推断出这个函数的返回值类型为:string | number | ConfigType
, 但是这并不理想,我们希望它可以更精准一些。
这个时候就可以使用函数重载来实现, 如下所示:
interface ConfigType { a?: string b?: string c?: number}
function getConfig(): ConfigTypefunction getConfig<T extends keyof ConfigType>(key: T): ConfigType[T]function getConfig(key?: keyof ConfigType) { let config: ConfigType = {}
try { config = JSON.parse(localStorage.getItem('app-config')) } catch {}
if (key) { return config[key] } else { return config }}
getConfig() // function getConfig(): ConfigType (+1 overload)getConfig('a') // function getConfig<"a">(key: "a"): string (+1 overload)getConfig('c') // function getConfig<"c">(key: "c"): number (+1 overload)
#
引用内置类型tslib
中包含了大量的内置类型, 如 HtmlElement
, Event
, Window
, WindowEventMap
等, 这些类型是针对 Javascript 基础语法的默认声明。
const el = document.querySelector('#id .class')
这行代码是获取某一个 DOM 元素,看上去没什么问题,但由于 DOM 元素的多样性,TS 无法定位到具体哪一个元素类型,所以变量 el
的推断类型默认是 Element
, 如下图所示:
这个时候 el
在类型上并没有继承到 dom 的原型:
作为开发者,我们可以知道获取元素的具体类型,比如这个元素是一个 div
, 那么就可以通过泛型参数的传递来告诉 TS 我要获取的元素的类型:
const el = document.querySelector<HTMLDivElement>('#id .class')
不同的元素类型不同,比如遇 input
元素:
const el = document.querySelector<HTMLInputElement>('#id .class')
接下来 el
将获得 input
元素特有的属性:
#
实例: 在 React 中使用import React from 'react'
export interface AppProps { /** 展示状态 */ show?: boolean /** 展示内容 */ content: React.ReactNode}
export interface AppState { count: number}
export class App extends React.Component<AppProps, AppState> { readonly state: AppState = { count: 0, }
/** 定时器 */ private TM: NodeJS.Timeout
constructor(props: AppProps) { super(props) this.TM = setInterval(() => { this.setState({ count: this.state.count + 1 }) }, 1000) }
render() { const { show, content } = this.props const { count } = this.state
if (!show) return null
return ( <div> {content} - {count} </div> ) }
componentWillUnmount() { clearInterval(this.TM) }}
React 函数式组件:
import React from 'react'
export interface AppProps { content: React.ReactNode}
export const App: React.FC<AppProps> = (props) => { const [count, setCount] = React.useState<number>(0)
return ( <div> {props.content} - {count} </div> )}
#
高级注释ts 中的注释遵循 tsdoc 规范,并在 vscode 中支持 markdown 语法。
健全良好的注释可以提升代码的可维护性,例子:
import React from 'react'
interface Props {}interface State {}
/** * @module 组件名称 * * 组件的详细说明 bala bala ..... * * - [x] 功能点1:bala bala... * - [x] 功能点2:[一个链接](https://www.typescriptlang.org/docs/handbook/2/basic-types.html), 直接贴地址也可以 https://www.typescriptlang.org/docs/handbook/2/basic-types.html * - [ ] ~~功能点3:(已废弃) ...~~ * - [ ] ... * * - [ ] 测试用例1:..... * - [ ] 测试用例2:..... * - [ ] .... * * - [ ] BUG: xxxx http://jira.huazhu.com/xxxxx * *  */export class App extends React.Component<Props, State> { /** 渲染头部内容 */ renderHeader(): JSX.Element { return <div>header</div> }
/** * 方法说明 bala bala... * * @param text 相关参数说明 * @returns 返回一个 JSX Element */ renderContent(text: string): JSX.Element { return <div>content: {text}</div> }
/** * 废弃方法,例如某些方法已经不再推荐使用,但是还需要兼容老代码的情况下使用 * * @deprecated */ renderFooter: () => null
render() { return ( <> {this.renderHeader()} {this.renderContent('ok')} {this.renderFooter()} </> ) }}
#
unique 唯一值unique 关键字用于表示全局唯一值,unique 后面必跟 symbol unique unique
unique 非常特殊,即表示类型,又可以直接作为值来使用
官方文档:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html
// Worksdeclare const Foo: unique symbol// Error! 'Bar' isn't a constant.let Bar: unique symbol = Symbol()// Works - refers to a unique symbol, but its identity is tied to 'Foo'.let Baz: typeof Foo = Foo// Also works.class C { static readonly StaticSymbol: unique symbol = Symbol()}
#
枚举类型 enum枚举是组织收集有关联变量的一种方式,许多程序语言(如:c/c#/Java)都有枚举数据类型。
相关文档:https://jkchao.github.io/typescript-book-chinese/typings/enums.html
enum CardSuit { Clubs, // 0 Diamonds, // 1 Hearts, // 2 Spades, // 3}
// 简单的使用枚举类型let Card = CardSuit.Clubs // 0
// 类型安全Card = 'not a member of card suit' // Error: string 不能赋值给 `CardSuit` 类型
手动关联值:
enum CardSuit { Clubs = 5, // 5 Diamonds, // 6 Hearts, // 7 Spades, // 8}
enum CardSuit2 { Clubs, // 0 Diamonds = 5, // 6 Hearts, // 7 Spades, // 8}
在手动关联的值后面未关联的部分的值依次递增
#
class 类的语法扩展略...
官方文档:https://www.typescriptlang.org/docs/handbook/2/classes.html
#
总结- 目前我个人所整理的 TS 类型系统相关的常用内容都在这里了。
- TS 除了类型系统外,还有少量的运行时的语法扩展,比如枚举类型
enum
,class
类的扩展等。 - 通过在开发过程中查阅第三方库的类型声明,实现自我学习,无需刻意花时间查阅在线文档及教程。
- 对于复杂类型接口的见识越广,那么面对一个陌生类库的难度就会越低,形成一个良性循环。
- 灵活运用泛型,将可复用泛型抽象封装。
- 尽可能收窄类型约束,提高类型精度,追求高质量代码。
- 无关紧要的高精度声明不宜耗费太长时间,该偷懒的还是要偷懒,避免延误工期。
#
参考- Typescript 官方文档:https://www.typescriptlang.org/docs/handbook/2/basic-types.html
- TS 3.1 中文文档:https://www.tslang.cn/docs/home.html
- 深入理解 Typescript:https://basarat.gitbook.io/typescript/type-system
- Typescript 高级类型:https://www.typescriptlang.org/docs/handbook/advanced-types.html