【干货】TypeScript|【干货】TypeScript 实战之 extends、infer 与 dva type
作者:小贼先生_ronffy前言 本文主要讲解 typescript 的
extends
、infer
和 template literal types 等知识点,针对每个知识点,我将分别使用它们解决一些日常开发中的实际问题。最后,活用这些知识点,渐进的解决使用 dva 时的类型问题。
说明:
extends
、infer
是 TS 2.8 版本推出的特性。- Template Literal Types 是 TS 4.1 版本推出的特性。
- 本文非 typescript 入门文档,需要有一定的 TS 基础,如 TS 基础类型、接口、泛型等。
抛几个问题 1. 获取函数的参数类型
function fn(a: number, b: string): string {
return a + b;
}// 期望值 [a: number, b: string]
type FnArgs = /* TODO */
2. 如何定义 get 方法
class MyC {
data = https://www.it610.com/article/{
x: 1,
o: {
y:'2',
},
};
get(key) {
return this.data[key];
}
}const c = new MyC();
// 1. x 类型应被推导为 number
const x = c.get('x');
// 2. y 类型应被推导为 string;z 不在 o 对象上,此处应 TS 报错
const { y, z } = c.get('o');
// 3. c.data 上不存在 z 属性,此处应 TS 报错
const z = c.get('z');
3. 获取 dva 所有的 Actions 类型
dva 是一个基于 redux 和 redux-saga 的数据流方案,是一个不错的数据流解决方案。此处借用 dva 中
model
来学习如何更好的将 TS 在实践中应用,如果对 dva 不熟悉也不会影响继续往下学习。// foo
type FooModel = {
state: {
x: number;
};
reducers: {
add(
S: FooModel['state'],
A: {
payload: string;
},
): FooModel['state'];
};
};
// bar
type BarModel = {
state: {
y: string;
};
reducers: {
reset(
S: BarModel['state'],
A: {
payload: boolean;
},
): BarModel['state'];
};
};
// models
type AllModels = {
foo: FooModel;
bar: BarModel;
};
问题:根据
AllModels
推导出 Actions
类型// 期望
type Actions =
| {
type: 'foo/add';
payload: string;
}
| {
type: 'bar/reset';
payload: boolean;
};
知识点 extends
extends
有三种主要的功能:类型继承、条件类型、泛型约束。类型继承 语法:
interface I {}
class C {}
interface T extends I, C {}
示例:
interface Action {
type: any;
}interface PayloadAction extends Action {
payload: any;
[extraProps: string]: any;
}// type 和 payload 是必传字段,其他字段都是可选字段
const action: PayloadAction = {
type: 'add',
payload: 1
}
条件类型(conditional-types)
extends
用在条件表达式中是条件类型。语法:
T extends U ? X : Y
如果
T
符合 U
的类型范围,返回类型 X
,否则返回类型 Y
。示例:
type LimitType = T extends number ? number : stringtype T1 = LimitType;
// string
type T2 = LimitType;
// number
如果
T
符合 number
的类型范围,返回类型 number
,否则返回类型 string
。泛型约束 可以使用
extends
来约束泛型的范围和形状。示例:
目标:调用
dispatch
方法时对传参进行 TS 验证:type
、payload
是必传属性,payload
类型是 number
。// 期望:ts 报错:缺少属性 "payload"
dispatch({
type: 'add',
})// 期望:ts 报错:缺少属性 "type"
dispatch({
payload: 1
})// 期望:ts 报错:不能将类型“string”分配给类型“number”。
dispatch({
type: 'add',
payload: '1'
})// 期望:正确
dispatch({
type: 'add',
payload: 1
})
实现:
// 增加泛型 P,使用 PayloadAction 时有能力对 payload 进行类型定义
interface PayloadAction extends Action {
payload: P;
[extraProps: string]: any;
}// 新增:Dispatch 类型,泛型 A 应符合 Action
type Dispatch = (action: A) => A;
// 备注:此处 dispatch 的 js 实现只为示例说明,非 redux 中的真实实现
const dispatch: Dispatch> = (action) => action;
infer
条件类型中的类型推导。
示例 1:
// 推导函数的返回类型
type ReturnType = T extends (...args: any[]) => infer R ? R : any;
function fn(): number {
return 0;
}type R = ReturnType;
// number
如果
T
可以分配给类型 (...args: any[]) => any
,返回 R
,否则返回类型 any
。R
是在使用 ReturnType
时,根据传入或推导的 T
函数类型推导出函数返回值的类型。示例 2:取出数组中的类型
type ArrayItemType = T extends (infer U)[] ? U : T;
type T1 = ArrayItemType;
// string
type T2 = ArrayItemType;
// Date
type T3 = ArrayItemType;
// number
模版字符串类型(Template Literal Types)
模版字符串用反引号(\`)标识,模版字符串中的联合类型会被展开后排列组合。
示例:
function request(api, options) {
return fetch(api, options);
}
如何用 TS 约束
api
为 https://abc.com
开头的字符串?type Api = `${'http' | 'https'}://abc.com${string}`;
// `http://abc.com${string}` | `https://abc.com${string}`
作者:小贼先生_ronffy解决问题 现在,相信你已掌握了
extends
、infer
和 template literal types,接下来,让我们逐一解决文章开头抛出的问题。Fix: Q1 获取函数的参数类型
上面已学习了
ReturnType
,知道了如何通过 extends
和 infer
获取函数的返回值类型,下面看看如何获取函数的参数类型。type Args = T extends (...args: infer A) => any ? A : never;
type FnArgs = Args;
Fix: Q2 如何定义 get 方法
class MyC {
get(key: T): MyC['data'][T] {
return this.data[key];
}
}
扩展:如果
get
支持「属性路径」的参数形式,如 const y = c.get('o.y')
,TS 又当如何书写呢?备注:此处只考虑
data
及深层结构均为 object
的数据格式,其他数据格式如数组等均未考虑。先实现
get
的传参类型:思路:根据对象,自顶向下找出对象的所有路径,并返回所有路径的联合类型
class MyC {
get>(path: P) {
// ... 省略 js 实现代码
}
}{
x: number;
o: {
y: string
}
}
'x' | 'o' | 'o.y'
type ObjectPropName = {
[K in keyof T]: K extends string
? T[K] extends Record
? ObjectPath | ObjectPropName
: ObjectPath: Path;
}[keyof T];
type ObjectPath = `${Pre extends ''
? Curr
: `${Pre}.`}${Curr}`;
再实现
get
方法的返回值类型:思路:根据对象和路径,自顶向下逐层验证路径是否存在,存在则返回路径对应的值类型
class MyC {
get>(path: P): ObjectPropType {
// ... 省略 js 实现代码
}
}type ObjectPropType = Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? ObjectPropType
: unknown
: unknown;
Fix: Q3 获取 dva 所有的 Actions 类型
type GenerateActions = {
[ModelName in keyof Models]: Models[ModelName]['reducers'] extends never
? never
: {
[ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends (
state: any,
action: infer A,
) => any
? {
type: `${string & ModelName}/${string & ReducerName}`;
payload: A extends { payload: infer P } ? P : never;
}
: never;
}[keyof Models[ModelName]['reducers']];
}[keyof Models];
type Actions = GenerateActions;
使用
// TS 报错:不能将类型“string”分配给类型“boolean”
export const a: Actions = {
type: 'bar/reset',
payload: 'true',
};
// TS 报错:不能将类型“"foo/add"”分配给类型“"bar/reset"”(此处 TS 根据 payload 为 boolean 反推的 type)
export const b: Actions = {
type: 'foo/add',
payload: true,
};
export const c: Actions = {
type: 'foo/add',
// TS 报错:“payload1”中不存在类型“{ type: "foo/add";
payload: string;
}”。是否要写入 payload?
payload1: true,
};
// TS 报错:类型“"foo/add1"”不可分配给类型“"foo/add" | "bar/reset"”
export const d: Actions = {
type: 'foo/add1',
payload1: true,
};
继续一连串问: 3.1 抽取
Reducer
3.2 抽取
Model
3.3 无
payload
? 3.4 非
payload
?3.5
Reducer
可以不传 State
吗?Fix: Q3.1 抽取 Reducer
// 备注:此处只考虑 reducer 是函数的情况,dva 中的 reducer 还可能是数组,这种情况暂不考虑。
type Reducer = (state: S, action: A) => S;
// foo
interface FooState {
x: number;
}
type FooModel = {
state: FooState;
reducers: {
add: Reducer<
FooState,
{
payload: string;
}
>;
};
};
Fix: Q3.2 抽取 Model
type Model = {
state: S;
reducers: {
[reducerName: string]: (state: S, action: A) => S;
};
};
// foo
interface FooState {
x: number;
}
interface FooModel extends Model {
state: FooState;
reducers: {
add: Reducer<
FooState,
{
payload: string;
}
>;
};
}
Fix: Q3.3 无 payload ? 增加
WithoutNever
,不为无 payload
的 action
增加 payload
验证。type GenerateActions = {
[ModelName in keyof Models]: Models[ModelName]['reducers'] extends never
? never
: {
[ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends (
state: any,
action: infer A,
) => any
? WithoutNever<{
type: `${string & ModelName}/${string & ReducerName}`;
payload: A extends { payload: infer P } ? P : never;
}>
: never;
}[keyof Models[ModelName]['reducers']];
}[keyof Models];
type WithoutNever = Pick<
T,
{
[k in keyof T]: T[k] extends never ? never : k;
}[keyof T]
>;
使用
interface FooModel extends Model {
reducers: {
del: Reducer;
};
}
// TS 校验通过
const e: Actions = {
type: 'foo/del',
};
Fix: Q3.4 非 payload ?
type GenerateActions = {
[ModelName in keyof Models]: Models[ModelName]['reducers'] extends never
? never
: {
[ReducerName in keyof Models[ModelName]['reducers']]: Models[ModelName]['reducers'][ReducerName] extends (
state: any,
action: infer A,
) => any
? A extends Record
? {
type: `${string & ModelName}/${string & ReducerName}`;
} & {
[K in keyof A]: A[K];
}
: {
type: `${string & ModelName}/${string & ReducerName}`;
}
: never;
}[keyof Models[ModelName]['reducers']];
}[keyof Models];
使用
interface FooModel extends Model {
state: FooState;
reducers: {
add: Reducer<
FooState,
{
x: string;
}
>;
};
}
// TS 校验通过
const f: Actions = {
type: 'foo/add',
x: 'true',
};
遗留 Q3.5 Reducer 可以不传 State 吗? 答案是肯定的,这个问题有多种思路,其中一种思路是:
state
和 reducer
都在定义的 model
上,拿到 model
后将 state
的类型注入给 reducer
,这样在定义
model
的 reducer
就不需手动传 state
了。这个问题留给大家思考和练习,此处不再展开了。
总结
extends
、infer
、 Template Literal Types 等功能非常灵活、强大,希望大家能够在本文的基础上,更多的思考如何将它们运用到实践中,减少 BUG,提升效率。
参考文章 https://www.typescriptlang.or...
https://www.typescriptlang.or...
https://www.typescriptlang.or...
【【干货】TypeScript|【干货】TypeScript 实战之 extends、infer 与 dva type】https://dev.to/tipsy_dev/adva...
推荐阅读
- 蓝桥杯|【蓝桥杯】真题训练 2015年C++B组 题1 奖券数目
- 《寒假算法集训》|《寒假算法集训》(专题十九)广度优先搜索
- Java学习|HTML5 入门( 一)
- Experience|【2021年度总结】不断学习的卡卡
- 面试集锦专栏|【面试常问】说一下你对单例模式的理解
- 面试集锦专栏|【面试常问】BS 与 CS 的联系与区别
- 面试集锦专栏|【面试常问】线程中常用的方法
- 三次握手和四次挥手
- java|支付宝集五福最全攻略!「一行黑科技」
- 【Python】系列|【Python】面试官:元组列表都分不清,回去等通知pa