Author: Mr. Thief_ronffy
foreword
This article mainly explains the knowledge points such as extends
, infer
and template literal types of typescript. For each knowledge point, I will use them to solve some practical problems in daily development.
Finally, use these knowledge points to gradually solve the type problem when using .
illustrate:
extends
andinfer
are features introduced in TS 2.8.- Template Literal Types is a feature introduced in TS 4.1.
- This article is not a typescript introductory document, and requires a certain TS foundation, such as TS basic types, interfaces, generics, etc.
Before formally speaking about the knowledge points, I will throw a few questions first, please think about each question carefully, and the following explanation will be slowly rolled out around these questions.
throw some questions
1. Get the parameter type of the function
function fn(a: number, b: string): string {
return a + b;
}
// 期望值 [a: number, b: string]
type FnArgs = /* TODO */
2. How to define the get method
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. Get all Actions types of dva
is a data flow solution based on redux and redux-saga, which is a good data flow solution. I borrow 0621eebd09b927 in model
here to learn how to better apply TS in practice. If you are not familiar with dva, it will not affect your continued learning.
// 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;
};
Problem: Actions
type 0621eebd09b985 from AllModels
// 期望
type Actions =
| {
type: 'foo/add';
payload: string;
}
| {
type: 'bar/reset';
payload: boolean;
};
Knowledge point
extends
extends
has three main functions: type inheritance, conditional types, and generic constraints.
type inheritance
grammar:
interface I {}
class C {}
interface T extends I, C {}
Example:
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
is a conditional type used in conditional expressions.
grammar:
T extends U ? X : Y
If T
conforms to the type range of U
, return type X
, otherwise return type Y
.
Example:
type LimitType<T> = T extends number ? number : string
type T1 = LimitType<string>; // string
type T2 = LimitType<number>; // number
If T
conforms to the type range of number
, return type number
, otherwise return type string
.
generic constraints
extends
can be used to constrain the scope and shape of generics.
Example:
Goal: TS verification of the passed parameters when calling dispatch
method: type
, payload
and are mandatory attributes, and the payload type is
number
.
// 期望:ts 报错:缺少属性 "payload"
dispatch({
type: 'add',
})
// 期望:ts 报错:缺少属性 "type"
dispatch({
payload: 1
})
// 期望:ts 报错:不能将类型“string”分配给类型“number”。
dispatch({
type: 'add',
payload: '1'
})
// 期望:正确
dispatch({
type: 'add',
payload: 1
})
accomplish:
// 增加泛型 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
Type deduction in conditional types.
Example 1:
// 推导函数的返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function fn(): number {
return 0;
}
type R = ReturnType<typeof fn>; // number
Returns R
if T
is assignable to type (...args: any[]) => any
, otherwise returns type any
. R
is when ReturnType
is used, the type of the return value of the function is deduced according to the incoming or deduced T
function type.
Example 2: Take out the type in the array
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
Template strings are marked with backticks (\`), and union types in the template string will be expanded and combined.
Example:
function request(api, options) {
return fetch(api, options);
}
How to use TS to constrain api
to be a string starting with https://abc.com
?
type Api = `${'http' | 'https'}://abc.com${string}`; // `http://abc.com${string}` | `https://abc.com${string}`
Author: Mr. Thief_ronffy
Solve the problem
Now, I believe you have mastered extends
, infer
and template literal types, let's solve the problems thrown at the beginning of the article one by one.
Fix: Q1 Get the parameter type of the function
You have learned ReturnType
above, and know how to get the return value type of the function through extends
and infer
. Let's see how to get the parameter type of the function.
type Args<T> = T extends (...args: infer A) => any ? A : never;
type FnArgs = Args<typeof fn>;
Fix: Q2 How to define get method
class MyC {
get<T extends keyof MyC['data']>(key: T): MyC['data'][T] {
return this.data[key];
}
}
Extension: If get
supports the parameter form of "attribute path", such as const y = c.get('o.y')
, how should TS be written?
Note : Only the data format of data
and the deep structure of object
is considered here, and other data formats such as arrays are not considered.
First realize the parameter type of get
:
Idea: According to the object, find all paths of the object from top to bottom, and return the union type of all paths
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}`;
Then implement the return value type of the get
method:
Idea: According to the object and path, verify whether the path exists layer by layer from top to bottom, and return the value type corresponding to the path if it exists.
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 Get all Actions types of dva
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>;
use
// 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,
};
Continue a series of questions:
3.1 Extract Reducer
3.2 Extract Model
3.3 No payload
?
3.4 Non- payload
?
3.5 Reducer
not pass State
?
Fix: Q3.1 Extract 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 Extract 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 No payload?
Add WithoutNever
, not add payload
verification for action
without 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]
>;
use
interface FooModel extends Model {
reducers: {
del: Reducer<FooState>;
};
}
// TS 校验通过
const e: Actions = {
type: 'foo/del',
};
Fix: Q3.4 Non-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];
use
interface FooModel extends Model {
state: FooState;
reducers: {
add: Reducer<
FooState,
{
x: string;
}
>;
};
}
// TS 校验通过
const f: Actions = {
type: 'foo/add',
x: 'true',
};
Can the legacy Q3.5 Reducer not pass State?
The answer is yes, there are many ideas for this question, one of which is: state
and reducer
are both on the defined model
, after state
model
inject the type of 0621eebd09bffc into reducer
,
In this way, when defining model
of reducer
, there is no need to manually pass state
.
This question is left for everyone to think about and practice, and will not be expanded here.
Summarize
extends
, infer
, Template Literal Types and other functions are very flexible and powerful,
I hope that based on this article, you can think more about how to apply them in practice, reduce bugs, and improve efficiency.
Reference article
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types
https://dev.to/tipsy_dev/advanced-typescript-reinventing-lodash-get-4fhe
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。