假设我有一个字典,像下面这样:

const obj = {
  something: 123,
  otherThing: "asd",
  nested: {
    nestedSomething: 456,
    nestedOther: "fgh",
    deepNested: {
      deepNested1: "hello",
      deepNested2: 42,
      deepNestedArr: ["a", "b", "c"],
    },
  },
};

我希望有一个函数access,它可以用来访问这个字典的值,如下所示:

access(obj, "something") //should return number
access(obj, "nested", "nestedOther") //should return string
access(obj, "nested", "deepNested", "deepNestedArr") //should return string[]
//and even:
access(obj, "nested", "deepNested", "deepNestedArr", 0) //should return string

为此,我首先需要一个实用程序类型,它可以获取一个对象类型,并输出该对象中所有可能的叶子路径的并集.我是这样实现的:

type AllPaths<Obj extends object, Key = keyof Obj> = Key extends keyof Obj
  ? Readonly<Obj[Key]> extends Readonly<Array<any>>
    ? [Key] | [Key, number]
    : Obj[Key] extends object
      ? [Key] | [Key, ...AllPaths<Obj[Key]>]
      : [Key]
  : never;

当给定一个具体类型作为参数时,它就可以工作:

type Test = AllPaths<typeof obj>; 
//["something"] | ["otherThing"] | ["nested"] | ["nested", "nestedSomething"] | ["nested", "nestedOther"] | ["nested", "deepNested"] | ["nested", "deepNested", "deepNested1"] | [...] | [...] | [...]

然后,我需要一个实用程序类型,它接受对象类型和我们前面生成的路径,索引到对象中,并返回结果类型.我是这样实现的:

type GetTypeFromPath<Obj extends object, Path extends PropertyKey[]> = Path extends [
  infer Head,
  ...infer Tail extends PropertyKey[]
]
  ? Head extends keyof Obj
    ? Obj[Head] extends object
      ? GetTypeFromPath<Obj[Head], Tail>
      : Obj[Head]
    : never
  : Obj;

...当给出具体的论证时,它也起作用.

type Test2 = GetTypeFromPath<typeof obj, ["nested", "deepNested", "deepNested2"]> //number

当给定具体类型时,这些实用程序独立工作,并且它们是高性能的.

现在,如果我试图在一个带有泛型类型参数的函数中使用它们,tsserver会挂起一点,然后给出"类型实例化过度深... "或"堆栈深度比较类型... "错误取决于我如何设置泛型.无论我做什么,我都无法避免触发限制器.有没有合理的方法来实现这一点?

declare function access<
  Obj extends object,
  //providing the default only does not work without extends for some reason
  Paths extends AllPaths<Obj> = AllPaths<Obj>,
  Ret = GetTypeFromPath<Obj, Paths>
>(
  obj: Obj,
  ...path: Paths
): Ret;

const res = access(obj, "nested", "deepNested");

在上面,泛型类型Ret无法计算,因为它太深了.

Playground link

这一切都是为了API边界,所以如果输入了错误的对象键路径,那么在access调用点有错误,并且在输入函数键时拥有适当的智能感知就足够了.

推荐答案

这些深度递归和嵌套类型总是会有奇怪的边缘情况,并且不可避免地,它们的某些使用将触发循环或深度限制警告. 因此,虽然我将为这个例子提供一些我认为您希望的东西,但我不能保证它在所有用例中都会以这种方式运行.


让我们这样写函数

declare function access<T, const KS extends PropertyKey[]>(
  obj: T, ...ks: ValidPathMap<T, KS>): DeepIdx<T, KS>

其中,我们必须将DeepIdx<T, KS>定义为类型T在由KS表示的路径处的嵌套属性类型,并将ValidPathMap<T, KS>定义为判断TKS并确保KST的有效路径的东西.如果它有效,则ValidPathMap<T, KS>的计算结果应为KS.如果它是无效的,它的计算结果应该是"接近"KS的有效值,也就是说错误消息会让用户知道哪里出了问题.理想情况下,如果KS只是部分路径,则ValidPathMap<T, KS>还将让用户知道下一个有效密钥.


简单的部分是DeepIdx<T, KS>:

type DeepIdx<T, KS extends PropertyKey[]> =
  KS extends [infer K0 extends keyof T, ...infer KR extends PropertyKey[]] ?
  DeepIdx<T[K0], KR> : T;

在这里,我们并不真正关心KS是否是有效路径.如果KS是以有效键T开头的tuple,则对其进行索引和递归.否则,我们只需按原样返回T.如果KS最终是有效路径,则DeepIdx<T, KS>是其中的属性.如果不是,则DeepIdx<T, KS>KS中最长有效前缀的属性.


ValidPathMap<T, KS>章更麻烦,更麻烦. 为了得到你想要的access()上的推论,你希望ValidPathMap<T, KS>KS上的homomorphic mapped type(见What does "homomorphic mapped type" mean?).这使得编译器可以从ValidPathMap<T, KS>推断出KS.

但是底层实现可能是类似于DeepIdxrecursive conditional type,所以我们需要一个同态的底层实现的包装器:

type ValidPathMap<T, KS extends PropertyKey[]> = {
  [I in keyof KS]: KS extends ValidatePath<T, KS> ?
  KS[I] : I extends keyof ValidatePath<T, KS> ? ValidatePath<T, KS>[I] : never
}

映射超过keyof KS,所以它是同态的.底层实现是ValidatePath<T, KS>.如果你分析一下,你会发现,如果KS是可以的,ValidatePathMap<T, KS>最终将只是KS,或者如果不是ValidatePath<T, KS>,那就是ValidatePath<T, KS>.

所以我们仍然需要实施ValidatePath<T, KS>.这里有一种方法:

type ValidatePath<T, KS extends PropertyKey[], A extends PropertyKey[] = []> =
  KS extends [infer K0 extends PropertyKey, ...infer KR extends PropertyKey[]] ?
  K0 extends keyof T ?
  ValidatePath<T[K0], KR, [...A, K0]> : [...A, keyof T] : A

这是一个tail recursive conditional type,我们将KS的有效初始部分累加(成A).如果整个事件是有效的,我们最终返回A(这将是KS).如果我们发现无效的部分,我们返回有效的初始部分,用keyof T替换第一个无效部分.


好的,让我们来测试一下:

const res = access(obj, "nested", "deepNested");
//    ^? const res: { deepNested1: string; deepNested2: number; deepNestedArr: string[]; }
const res2 = access(obj, "nested", "nestedOther");
//    ^? const res2: string
const res3 = access(obj, "nested", "deepNested", "deepNestedArr");
//    ^? const res3: string[]
const res4 = access(obj, "nested", "deepNested", "deepNestedArr", 0);
//    ^? const res4: string

这些东西都按照需要工作,具有正确的类型,没有实例化深度错误. 当涉及到错误和智能感知时,它的行为也符合预期:

access(obj, "foo"); // error!
//          ~~~~~
// Argument of type '"foo"' is not assignable to parameter 
// of type '"nested" | "something" | "otherThing"'.

access(obj, "nested", "oops");
//                    ~~~~~~
// Argument of type '"oops"' is not assignable to parameter 
// of type '"deepNested" | "nestedSomething" | "nestedOther"'.(2345)

您可以看到第一个坏键上有错误,并且错误表明了它期望看到的内容.

Playground link to code

Typescript相关问答推荐

类型脚本不会推断键的值类型

TypScript手册中的never[]参数类型是什么意思?

参数类型undefined不能分配给参数类型字符串|未定义

typescribe警告,explate—deps,useEffect依赖项列表

为什么tsx不判断名称中有"—"的属性?'

在TypeScrip中分配回调时,参数的分配方向与回调本身相反.为什么会这样呢?

使用动态主体初始化变量

类型,其中一条记录为字符串,其他记录为指定的

TypeScrip 5.3和5.4-对`Readonly<;[...number[],字符串]>;`的测试版处理:有什么变化?

如何在Reaction路由v6.4加载器和操作中获得Auth0访问令牌?

在函数中打字推断记录的关键字

Angular 17子路由

如何在ANGLE中注册自定义验证器

TYPE FOR ARRAY,其中每个泛型元素是单独的键类型对,然后在该数组上循环

从类型脚本中元组的尾部获取第n个类型

如何在Angular /字体中访问API响应的子元素?

在REACT查询中获取未定义的isLoading态

如何创建一个将嵌套类属性的点表示法生成为字符串文字的类型?

React HashRouter,单击导航栏时页面重新加载出现问题

可选字段和联合之间有什么区别吗?