作者:小贼学生_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 = {
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> = T extends number ? number : string
type T1 = LimitType<string>; // string
type T2 = LimitType<number>; // 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<P = any> extends Action {
payload: P;
[extraProps: string]: any;
}
// 新增:Dispatch 类型,泛型 A 应合乎 Action
type Dispatch<A extends Action> = (action: A) => A;
// 备注:此处 dispatch 的 js 实现只为示例阐明,非 redux 中的实在实现
const dispatch: Dispatch<PayloadAction<number>> = (action) => action;
infer
条件类型中的类型推导。
示例 1:
// 推导函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function fn(): number {return 0;}
type R = ReturnType<typeof fn>; // number
如果 T
能够调配给类型 (...args: any[]) => any
,返回 R
,否则返回类型 any
。R
是在应用 ReturnType
时,依据传入或推导的 T
函数类型推导出函数返回值的类型。
示例 2:取出数组中的类型
type ArrayItemType<T> = T extends (infer U)[] ? U : T;
type T1 = ArrayItemType<string>; // string
type T2 = ArrayItemType<Date[]>; // Date
type T3 = ArrayItemType<number[]>; // 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> = T extends (...args: infer A) => any ? A : never;
type FnArgs = Args<typeof fn>;
Fix: Q2 如何定义 get 办法
class MyC {get<T extends keyof MyC['data']>(key: T): MyC['data'][T] {return this.data[key];
}
}
扩大:如果 get
反对「属性门路」的参数模式,如 const y = c.get('o.y')
,TS 又当如何书写呢?
备注 :此处只思考 data
及深层构造均为 object
的数据格式,其余数据格式如数组等均未思考。
先实现 get
的传参类型:
思路:依据对象,自顶向下找出对象的所有门路,并返回所有门路的联结类型
class MyC {get<P extends ObjectPropName<MyC['data']>>(path: P) {// ... 省略 js 实现代码}
}
{
x: number;
o: {y: string}
}
'x' | 'o' | 'o.y'
type ObjectPropName<T, Path extends string = ''> = {[K in keyof T]: K extends string
? T[K] extends Record<string, any>
? ObjectPath<Path, K> | ObjectPropName<T[K], ObjectPath<Path, K>>
: ObjectPath<Path, K>
: Path;
}[keyof T];
type ObjectPath<Pre extends string, Curr extends string> = `${Pre extends ''
? Curr
: `${Pre}.`}${Curr}`;
再实现 get
办法的返回值类型:
思路:依据对象和门路,自顶向下逐层验证门路是否存在,存在则返回门路对应的值类型
class MyC {get<P extends ObjectPropName<MyC['data']>>(path: P): ObjectPropType<MyC['data'], P> {// ... 省略 js 实现代码}
}
type ObjectPropType<T, Path extends string> = Path extends keyof T
? T[Path]
: Path extends `${infer K}.${infer R}`
? K extends keyof T
? ObjectPropType<T[K], R>
: unknown
: unknown;
Fix: Q3 获取 dva 所有的 Actions 类型
type GenerateActions<Models extends Record<string, any>> = {[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<AllModels>;
应用
// 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<S = any, A = any> = (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<S = any, A = any> = {
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<Models extends Record<string, any>> = {[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<T> = Pick<
T,
{[k in keyof T]: T[k] extends never ? never : k;
}[keyof T]
>;
应用
interface FooModel extends Model {
reducers: {del: Reducer<FooState>;};
}
// TS 校验通过
const e: Actions = {type: 'foo/del',};
Fix: Q3.4 非 payload ?
type GenerateActions<Models extends Record<string, any>> = {[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<string, any>
? {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…
https://dev.to/tipsy_dev/adva…