We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文主要介绍从业务场景出发一步步实现一个深层嵌套对象的类型获取,延伸至 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目前的问题:
有TS基础的可以先停下来自己试试怎么实现
其实最简单的办法就是 AppRequest 加多一个泛型,每次调用的时候传个泛型进去,参数根据这个泛型去取就可以了。但是这样会导致每次调用 AppRequest 都很繁琐: AppRequest<typeof APiConfig.investment.getCompanyDetail>(APiConfig.investment.getCompanyDetail, { id: 'id'} )
AppRequest<typeof APiConfig.investment.getCompanyDetail>(APiConfig.investment.getCompanyDetail, { id: 'id'} )
一开始同事没想到的类型怎么处理,所以提出的方案是函数重载,但是如果函数重载的话,每次写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@@@@@*/
虽然能达到我们的目的,但是也很麻烦,每次都需要跑个脚本去生成,并且后面会导致 这个函数文件一堆冗余的代码。
那么如果想用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> = {};
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; } }
// 如果值不是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 是对象类型,所以这里变成了 '' }
type NestedValueOf<T extends ObjectType> = { ... }[keyof T] // name: string, age: number, address: '',所以取出来的value是: // string | number const data: NestedValueOf<typeof demo> = 'string' || 1
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
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能满足项目大部分需求
The text was updated successfully, but these errors were encountered:
No branches or pull requests
TL;DR
项目背景
目前项目是 monorepo,APP使用RN技术栈,所以有些逻辑可以和PC公用,比如Services部分。目前的设计是这样的,在 shared 中定义请求的类型和配置。
AppRequest目前的问题:
有TS基础的可以先停下来自己试试怎么实现
其实最简单的办法就是 AppRequest 加多一个泛型,每次调用的时候传个泛型进去,参数根据这个泛型去取就可以了。但是这样会导致每次调用 AppRequest 都很繁琐:
AppRequest<typeof APiConfig.investment.getCompanyDetail>(APiConfig.investment.getCompanyDetail, { id: 'id'} )
V1 函数重载
一开始同事没想到的类型怎么处理,所以提出的方案是函数重载,但是如果函数重载的话,每次写ApiConfig都得再写多一处重载声明很麻烦。所以改成用node生成代码。
大致思路如下:
生成的代码效果如下:
虽然能达到我们的目的,但是也很麻烦,每次都需要跑个脚本去生成,并且后面会导致 这个函数文件一堆冗余的代码。
V2 做体操
那么如果想用ts去声明类型,应该怎么做呢?
其实这个参数类型最复杂的一点无非就是 config 类型的定义,因为 ApiConfig是一个至少两层嵌套的对象,可能会有三层、四层等,所以最关键的点是实现一个 DeepValueOf。背景代码如下:
这里读取到的是每个对象最内层的属性,所以分别是: string | number | boolean,并不是我们想要的取到最内层对象的结果。所以我们还需要加多一个判断,如果当前的 T[Key] 是最后一个对象了,那么就直接返回 T[Key],否则才走递归。
现在 NestedValueOf写好之后,再回过头来看 request 的类型应该怎么写。
效果如下: Live Demo

Next
NestedKeyOfUnion 和 NestedKeyPath 的推导过程其实和上述基本一致。
相关链接
超详细 keyof nested object教程
utility-types: 常用的type-utils
type-challenges: 类型体操,做完easy和medium能满足项目大部分需求
The text was updated successfully, but these errors were encountered: