本文主要介绍从业务场景出发一步步实现一个深层嵌套对象的类型获取,延伸至 DeepKeyOf和 NestedKeyPath。非实际业务代码 原链接
TL;DR
type NestValueOf<T extends ObjectType> = {
[Key in keyof T]: T[Key] extends ObjectType
? NestValueOf<T[Key]> extends ObjectType ? NestValueOf<T[Key]>: T[Key]
: T[Key]
}[keyof T]
const mock = {
dataA: {
name: 'jsonz',
age: 18,
},
keyB: {
dataB: {
city: 'ShanTou'
}
}
}
// expect: { name: string, age: number } | { city: string }
type MockType = NestValueOf<typeof mock>
复制代码
项目背景
目前项目是 monorepo,APP使用RN技术栈,所以有些逻辑可以和PC公用,比如Services部分。目前的设计是这样的,在 shared 中定义请求的类型和配置。
type IConfig<REQ, RES extends object = {}> = {
method: string;
url: string;
summary: string;
request?: REQ;
response?: RES
}
export const getCompanyDetail: IConfig<{ id: string }, { companyName: string}> = {
method: 'get',
url: 'api/v2/company/(id)'
summary: '获取公司详情接口',
}
export const getCompanies: IConfig<{}, { data: { id: string }[]}> = {
method: 'get',
url: 'api/v2/companies'
summary: '获取公司列表接口',
}
// 通用配置
export const Apionfig = {
investment: {
getCompanyDetail,
list: {
getCompanies,
}
},
}
function AppRequest(config: any, params: any = {}, options: any = {}): any {}
复制代码
AppRequest目前的问题:
- 将config制定为 ApiConfig的类型,比如 APiConfig.investment.getCompanyDetail
- params 应该读取 config参数的request类型
- returnType应该是config的response类型
有TS基础的可以先停下来自己试试怎么实现
其实最简单的办法就是 AppRequest 加多一个泛型,每次调用的时候传个泛型进去,参数根据这个泛型去取就可以了。但是这样会导致每次调用 AppRequest 都很繁琐: AppRequest<typeof APiConfig.investment.getCompanyDetail>(APiConfig.investment.getCompanyDetail, { id: 'id'} )
V1 函数重载
一开始同事没想到的类型怎么处理,所以提出的方案是函数重载,但是如果函数重载的话,每次写ApiConfig都得再写多一处重载声明很麻烦。所以改成用node生成代码。 大致思路如下:
import nodePlop from 'node-plop'
const plop = await nodePlop();
// 读取 apiConfig 配置
const apiConfig = await import('..')
plop.setGenerator('requestType', {
actions() {
const actions = []
// 打平对象,生成类型模版代码
getAllConfig(apiConfig).forEach(data => actions.push(`
request(
config: typeof ApiConfig.${data.module}.${data.name},
....
)
`))
return [{ type: 'modify', path: '..', pattern: '..', template: '..'}]
}
})
const basicAdd = plop.getGenerator('modules');
await basicAdd.runActions();
复制代码
生成的代码效果如下:
/* @@@@@代码自动生成注释 start@@@@@*/
AppRequest(
config: typeof APiConfig.investment.getCompanyDetail,
params?: typeof APiConfig.investment.getCompanyDetail.request,
options?: any,
): Promise<typeof APiConfig.investment.getCompanyDetail.response>;
AppRequest(
config: typeof APiConfig.investment.list.getCompanies,
params?: typeof APiConfig.investment.list.getCompanies.request,
options?: any,
): Promise<typeof APiConfig.investment.list.getCompanies.response>;
/* @@@@@代码自动生成注释 end@@@@@*/
复制代码
虽然能达到我们的目的,但是也很麻烦,每次都需要跑个脚本去生成,并且后面会导致 这个函数文件一堆冗余的代码。
V2 做体操
那么如果想用ts去声明类型,应该怎么做呢? 其实这个参数类型最复杂的一点无非就是 config 类型的定义,因为 ApiConfig是一个至少两层嵌套的对象,可能会有三层、四层等,所以最关键的点是实现一个 DeepValueOf。背景代码如下:
type NonUndefined<T> = T extends undefined ? never: T
interface IConfig<REQ, RES> {
summary?: string;
method?: string;
url: string;
request?: REQ;
response?: RES;
}
const getCompany: IConfig<{
id: string,
}, { page: number }> = {
summary: '获取公司详情',
method: 'get',
url: '/api/v2/investment/company/{id}',
}
const getCompanies: IConfig<{}, { pages: number }> = {
summary: '获取公司列表',
method: 'get',
url: '/api/v2/investment/companies',
}
const ApiConfig = {
investment: { getCompany },
test1: { test2: { getCompanies } }
}
function AppRequest(config, params) {}
复制代码
- 声明一个类型,包含一个泛型,用来传入对象类型的
typeof NestedValueOf<T> = {};
复制代码
- 声明一个对象类型约束T
type ObjectType = Record<symbol | number | string, any>
type NestedValueOf<T extends ObjectType> = {}
复制代码
- 遍历一个对象的键
type NestedValueOf<T extends ObjectType> = {
[Key in keyof T]: T[Key]
}
const demo = {
name: 'jsonz',
age: 2,
address: {
city: 'shantou',
native: true,
}
}
type Demo = NestedValueOf<typeof demo>
// expect Demo
{
name: string;
age: number;
address: {
city: string;
native: boolean;
}
}
复制代码
- 在ts中,我们可以通过三元去检查一个类型 T extends object ? number: string
// 如果值不是Object,那么直接把类型返回,如果值是对象的话,TODO
type NestedValueOf<T extends ObjectType> = {
[Key in keyof T]: T[Key] extends ObjectType
? '' // TODO
: T[Key]
}
const data: nestedValueOf<typeof demo> = {
name: 'jsonz',
age: 18,
address: '' // address 是对象类型,所以这里变成了 ''
}
复制代码
- 在这里我们需要的是 value 的部分,而不是 { key: value } 的结构,所以加多个取 value 的操作
type NestedValueOf<T extends ObjectType> = {
...
}[keyof T]
// name: string, age: number, address: '',所以取出来的value是:
// string | number
const data: NestedValueOf<typeof demo> = 'string' || 1
复制代码
- 现在可以开始来写刚才的 TODO ,如果我们发现 value 是一个对象,则把参数传入 NestedValueOf,做一层递归,这样就能取到最底层的类型。
type NestedValueOf<T extends ObjectType> = {
[Key in keyof T]: T[Key] extends ObjectType
? NestedValueOf<T[Key]>
: T[Key]
}[keyof T]
// expect: boolean | number | string
const data: NestedValueOf<typeof demo> = true || 1 || 'string'
复制代码
这里读取到的是每个对象最内层的属性,所以分别是: string | number | boolean,并不是我们想要的取到最内层对象的结果。所以我们还需要加多一个判断,如果当前的 T[Key] 是最后一个对象了,那么就直接返回 T[Key],否则才走递归。
type NestedValueOf<T extends ObjectType> = {
[Key in keyof T]: T[Key] extends ObjectType
? NestedValueOf<T[Key]> extends ObjectType
?
: T[Key]
}[keyof T]
const config = {
a: {
name: 'string',
code: 20
},
b: {
c: {
d: {
title: 'string',
created: true,
}
}
}
}
// expect: { name: string, code: number } | { title: string, created: boolean }
type ConfigType = NestedValueOf<typeof config>
const data: ConfigType = {
title: 'string',
created: false,
}
复制代码
现在 NestedValueOf写好之后,再回过头来看 request 的类型应该怎么写。
// 获取 ApiConfig 打平的对象类型
type ValueOfApiConfigType = NestValueOf<typeof ApiConfig>
// 声明一个泛型,调用时不需要传入,返回值用as显示声明为 config.response
function request<T extends ValueOfApiConfigType>(config: T, params: T['request']) {
console.log(config, params)
return { } as (NonUndefined<T['response']>)
}
复制代码
效果如下: Live Demo
Next
NestedKeyOfUnion 和 NestedKeyPath 的推导过程其实和上述基本一致。
type NestedKeyOfUnion<T extends ObjectType> = {
[Key in keyof T]: T[Key] extends ObjectType ? Key | NestedKeyOfUnion<T[Key]>: Key
}[keyof T]
const ApiConfig = {
data: {
city: '',
address: {
native: ''
}
},
name: '',
age: '',
}
// expect: data | city | address | native | name | age
type ApiConfigKey = NestedKeyOfUnion<typeof ApiConfig>
type NestedKeyPath<ObjectType extends object> =
{[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
// @ts-ignore
? `${Key}` | `${Key}.${NestedKeyPath<ObjectType[Key]>}`
: `${Key}`
}[keyof ObjectType & (string | number)];
const ApiConfig = {
data: {
city: '',
address: {
native: ''
}
},
name: '',
age: '',
}
function get(path: NestedKeyPath<typeof ApiConfig>) {
return lodash.get(ApiConfig, path)
}
const city = get('data.address.native')
复制代码
相关链接
- 超详细 keyof nested object教程
- utility-types: 常用的type-utils
- type-challenges: 类型体操,做完easy和medium能满足项目大部分需求