对深层对象键/路径作为函数参数进行正确的智能感知和类型检查,而不会触发递归类型限制器

问题描述 投票:0回答:1

假设我有一本如下的字典:

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 无法计算,因为它太深了。

游乐场链接

这一切都是针对 API 边界的,因此,如果输入了错误的对象键路径,则在

access
调用站点会出现错误,并且在输入函数键时具有正确的 IntelliSense 足以满足我的目的。

typescript intellisense
1个回答
0
投票

这些深度递归和嵌套类型总是会出现奇怪的边缘情况,并且它们的某些使用不可避免地会触发循环或深度限制警告。因此,虽然我将在这个示例中展示一些按照我认为您想要的方式工作的东西,但我不能保证它对于所有用例都会如此。


我们这样写函数吧

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>
定义为检查
T
KS
并确保
 KS
T
的有效路径。如果有效,则
ValidPathMap<T, KS>
应评估为
KS
。如果它无效,它应该评估为“接近”
KS
的有效值,因为错误消息会让用户知道出了什么问题。理想情况下,如果
ValidPathMap<T, KS>
只是部分路径,那么
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 上的 同态
映射类型
(请参阅 “同态映射类型”是什么意思?)。这让编译器可以从
KS
推断
ValidPathMap<T, KS>

但是底层实现可能是像DeepIdx一样的

递归条件类型
,所以我们需要一个同态底层实现的包装器:

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>
。如果您进行分析,您会发现,如果
ValidatePathMap<T, KS>
没问题,那么
KS
最终将只是
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

这是一个尾递归条件类型,我们将

A
的有效初始部分累积(到
KS
中)。如果整个事情都是有效的,我们最终会返回
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

这些东西都按预期工作,具有正确的类型并且没有实例化深度错误。当涉及到错误和 IntelliSense 时,它的行为也符合预期:

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 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.