分两步使用类型安全属性访问递归解析对象

ama*_*ann 3 typescript

我尝试将以下函数中的字符串类型替换为更具体的类型,以确保类型安全的属性访问:

import {get} from 'lodash';

const obj = {
  foo: 'foo',
  bar: {
    a: 'Hello',
    b: {c: 'World'}
  }
};

function factory(namespace?: string) {
  return function getter(key: string) {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}

const getter = factory('bar');
getter('b.c'); // 'World'
Run Code Online (Sandbox Code Playgroud)

点符号表示嵌套的属性访问。它既可以出现在 中namespace,也可以出现在 中key

到目前为止,我发现我可以namespace使用这个实用程序输入:

type NestedKeyOf<ObjectType extends object> =
  {
    [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
      ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
      : `${Key}`
  }[keyof ObjectType & (string | number)];
Run Code Online (Sandbox Code Playgroud)

用法:namespace?: NestedKeyOf<typeof obj>

然而,我正在努力想出一个自动分配给key.

另外两个要求是:

  1. 命名空间应该只解析为对象(没有叶字符串)。
  2. getter 应始终解析为叶字符串(无对象)。

可以假设对象中只存在对象和字符串,没有其他内容。

// Test cases

// Should pass
factory()('foo')
factory('bar')('a')
factory('bar')('b.c')

// Invalid property access
// @ts-expect-error 
factory('baz')
// @ts-expect-error
factory('bar')('d')

// Only partial namespaces are allowed
// @ts-expect-error 
factory('foo')

// Getter calls need to resolve to a leaf string
// @ts-expect-error
factory('bar')('b')
Run Code Online (Sandbox Code Playgroud)

任何帮助将非常感激!非常感谢!

moc*_*cha 5

我不敢相信这个可恶的东西真的有效:

function factory<NestedKey extends NestedKeyOf<typeof obj>>(namespace?: NestedKey) {
  return function getter<
    TargetKey extends 
      (NestedKey extends undefined
        ? NestedKeyOf<typeof obj>
        : NestedKeyOf<Get<typeof obj, NestedKey>>)
  >(key: TargetKey): NestedKey extends undefined ? Get<typeof obj, TargetKey> : Get<typeof obj, `${NestedKey}.${TargetKey}`> {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}
Run Code Online (Sandbox Code Playgroud)

请允许我解释一下。首先,我在执行 NestedKeyOf 时遇到了很多麻烦:(

我必须重写它,因为它报告类型实例化太深,所以这是我的版本:

type NestedKeyOf<O> = O extends object ? {
    [K in keyof O]: `${K & string}` | `${K & string}.${NestedKeyOf<O[K]>}`;
}[keyof O] : never;
Run Code Online (Sandbox Code Playgroud)

它做同样的事情;只是写法不同。接下来我们需要能够“深度获取”一个属性(模仿 lodash 的 get 函数):

type Get<O, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof O ? Get<O[Key], Rest> : never
    : P extends keyof O ? O[P] : never;
Run Code Online (Sandbox Code Playgroud)

额外的功能extends keyof O是为了防止无效属性访问时出现错误。

最后就是我上面展示的怪物。

我们需要存储实际namespace内容,因此我们使用泛型。通过这个恰当命名的泛型,NestedKey我们现在可以在 的定义中使用它getter

getter还需要一个嵌套键。namespace但是,如果未提供,其类型会有所不同。

这就是为什么NestedKey extends undefined存在。如果不存在,则key应该是原始对象的嵌套键。namespace否则,它是指向的值的嵌套键,使用Get.

最后在返回值中,我们做了同样的事情。如果NestedKey不存在,则我们深度获取目标键,否则,我们深度获取嵌套键和目标键。

操场

断言as const是为了验证它实际上是深度获取正确的值。


根据新要求进行更新。我们需要一些新类型来告诉我们哪些键是对象,哪些是字符串:

type GetObjectKeys<O, K extends string> = {
  [P in K]: Get<O, P> extends string ? never : P;
}[K];

type GetStringKeys<O, K extends string> = {
  [P in K]: Get<O, P> extends string ? P : never;
}[K];
Run Code Online (Sandbox Code Playgroud)

它们采用对象类型和一些潜在的键。它检查每个键的类型。对于对象,如果它是字符串,则为never,否则为P。我们之所以使用 ,never是因为下面我们将得到所有剩余键的并集[K]T | never简化为T

然后由于namespace是可选的它引入了一些困难。我的一个错误是相信如果namespace没有提供,NestedKey将是未定义的(所以前面的答案实际上是不正确的)。稍后将对此进行更正。

为了解决可选的问题namespace,我们将原始factory函数的参数设置namespace 为非可选,并将其重命名为_factory(internal/private)。然后我们创建一个新factory函数,如下所示:

function factory<NestedKey extends GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>>>(namespace?: NestedKey) {
  return _factory<
    { __private: typeof obj },
    GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>> extends NestedKey ? "__private" : `__private.${NestedKey}`
  //@ts-ignore Unfortunately I don't think there is a good way to prevent this error
  >({ __private: obj }, namespace ? `__private.${namespace}` : "__private");
}
Run Code Online (Sandbox Code Playgroud)

它需要任何对象键,但如果未提供命名空间,它将创建默认值__private,因为我们使用属性将目标对象包装在另一个对象中,__private以避开命名空间是可选的这一事实。将其视为委托者。

现在修改后的_factory函数:

function _factory<Obj extends unknown, NestedKey extends NestedKeyOf<Obj>>(obj: Obj, namespace: NestedKey) {
  return function getter<
    TargetKey extends GetStringKeys<Get<Obj, NestedKey>, NestedKeyOf<Get<Obj, NestedKey>>>
  >(key: TargetKey): Get<Obj, `${NestedKey}.${TargetKey}`> {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}
Run Code Online (Sandbox Code Playgroud)

该函数与原始函数基本相同,只是现在它只需要生成字符串的键。处理可选的部分namespace被移至新factory函数中。

我本可以选择更好的名称来factory避免_factory混淆,但希望您能很好地跟随我完成这一切。

操场

顺便说一句,这对****来说是过度设计的;当你开始让类型像真正的代码一样工作时总是如此