如何为带动态属性名的TypeScript函数添加返回类型提示?
解决TypeScript动态属性重命名的类型提示问题
问题背景
我写了一个用来重命名react-query UseQueryResult 属性的TypeScript函数,能根据传入的名称前缀自动生成新属性,但使用时TypeScript无法识别这些动态生成的属性(比如解构cards时提示不存在),需要给函数添加正确的返回类型提示,同时避免手动逐个重命名的繁琐。
原函数代码:
function refactorQueryResult<D, E>( name: string, original: UseQueryResult<D, E> ) { return { [name]: original.data, [`${name}UpdatedAt`]: original.dataUpdatedAt, [`${name}Error`]: original.error, [`${name}ErrorUpdatedAt`]: original.errorUpdatedAt, [`${name}FailureCount`]: original.failureCount, [`${name}HasError`]: original.isError, [`${name}Fetched`]: original.isFetched, [`${name}FetchedAfterMount`]: original.isFetchedAfterMount, [`${name}Fetching`]: original.isFetching, [`${name}Idle`]: original.isIdle, [`${name}Loading`]: original.isLoading, [`${name}HasLoadingError`]: original.isLoadingError, [`${name}IsPlaceholder`]: original.isPlaceholderData, [`${name}IsPrevious`]: original.isPreviousData, [`${name}HasRefetchError`]: original.isRefetchError, [`${name}Refetching`]: original.isRefetching, [`${name}HasSuccess`]: original.isSuccess, [`${name}IsStale`]: original.isStale, [`fetch${name[0].toUpperCase()}${name.slice(1)}`]: original.refetch, [`remove${name[0].toUpperCase()}${name.slice(1)}`]: original.remove, [`${name}Status`]: original.status, }; }
使用时报错:
// TypeScript提示`cards`不存在,但实际属性存在 const { cards } = refactorQueryResult("cards", useQuery(listCards(filters)))
解决方案
核心思路是利用字符串字面量类型和映射类型让TypeScript跟踪动态属性的具体名称,步骤如下:
1. 定义泛型映射类型
先创建一个映射类型,把UseQueryResult的每个属性对应到带前缀的新属性:
import { UseQueryResult } from '@tanstack/react-query'; // 辅助类型:将首字母大写 type CapitalizeFirstLetter<S extends string> = S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : S; // 核心映射类型:根据名称前缀生成新属性 type RefactoredQueryResult<N extends string, D, E> = { [K in keyof UseQueryResult<D, E> as K extends 'data' ? N : K extends 'dataUpdatedAt' ? `${N}UpdatedAt` : K extends 'error' ? `${N}Error` : K extends 'errorUpdatedAt' ? `${N}ErrorUpdatedAt` : K extends 'failureCount' ? `${N}FailureCount` : K extends 'isError' ? `${N}HasError` : K extends 'isFetched' ? `${N}Fetched` : K extends 'isFetchedAfterMount' ? `${N}FetchedAfterMount` : K extends 'isFetching' ? `${N}Fetching` : K extends 'isIdle' ? `${N}Idle` : K extends 'isLoading' ? `${N}Loading` : K extends 'isLoadingError' ? `${N}HasLoadingError` : K extends 'isPlaceholderData' ? `${N}IsPlaceholder` : K extends 'isPreviousData' ? `${N}IsPrevious` : K extends 'isRefetchError' ? `${N}HasRefetchError` : K extends 'isRefetching' ? `${N}Refetching` : K extends 'isSuccess' ? `${N}HasSuccess` : K extends 'isStale' ? `${N}IsStale` : K extends 'refetch' ? `fetch${CapitalizeFirstLetter<N>}` : K extends 'remove' ? `remove${CapitalizeFirstLetter<N>}` : K extends 'status' ? `${N}Status` : never ] : UseQueryResult<D, E>[K];
2. 修改函数的泛型参数
将name参数约束为字符串字面量类型N,并指定返回类型为上面定义的映射类型:
function refactorQueryResult<N extends string, D, E>( name: N, original: UseQueryResult<D, E> ): RefactoredQueryResult<N, D, E> { return { [name]: original.data, [`${name}UpdatedAt`]: original.dataUpdatedAt, [`${name}Error`]: original.error, [`${name}ErrorUpdatedAt`]: original.errorUpdatedAt, [`${name}FailureCount`]: original.failureCount, [`${name}HasError`]: original.isError, [`${name}Fetched`]: original.isFetched, [`${name}FetchedAfterMount`]: original.isFetchedAfterMount, [`${name}Fetching`]: original.isFetching, [`${name}Idle`]: original.isIdle, [`${name}Loading`]: original.isLoading, [`${name}HasLoadingError`]: original.isLoadingError, [`${name}IsPlaceholder`]: original.isPlaceholderData, [`${name}IsPrevious`]: original.isPreviousData, [`${name}HasRefetchError`]: original.isRefetchError, [`${name}Refetching`]: original.isRefetching, [`${name}HasSuccess`]: original.isSuccess, [`${name}IsStale`]: original.isStale, [`fetch${name[0].toUpperCase()}${name.slice(1)}`]: original.refetch, [`remove${name[0].toUpperCase()}${name.slice(1)}`]: original.remove, [`${name}Status`]: original.status, } as RefactoredQueryResult<N, D, E>; }
3. 使用验证
现在调用函数时,TypeScript能正确推断所有动态生成的属性:
// 无报错,cards、cardsFetched、fetchCards等属性均能被正确识别 const { cards, cardsFetched, fetchCards } = refactorQueryResult("cards", useQuery(listCards(filters)))
关键说明
- 之前的写法失败是因为
name参数是string类型,TypeScript无法跟踪具体的字符串值;将其约束为字符串字面量类型N extends string后,TypeScript能基于这个字面量推导所有动态属性的名称。 - 如果
name是动态变量(不是字面量),需要显式指定泛型参数:const queryName = "cards"; const result = refactorQueryResult<typeof queryName, Card[], Error>(queryName, useQuery(listCards(filters)));
内容的提问来源于stack exchange,提问作者kaan_atakan
相关产品推荐
相关产品推荐

