首页 > 解决方案 > 将路径段作为参数并返回路径末尾的类型的方法类型

问题描述

我正在为网络工作者编写一个小的 RPC 库,它需要消费者遍历远程引用。要访问参考,您必须使用IReference.property(...path: string[]).

例如,如果我有一个看起来像的源对象{ foo: { bar: { value: 'foobar' }}}

然后我将访问内部值await ref.property('foo', 'bar', 'value').value()

我想要的是返回值.value()是一个Promise值。

我已经设法编写了一种类型,允许我在property方法中拥有一个路径段,但是如何添加更多?

export interface IReference<T> {
  property<K extends keyof T | ((...args: any) => any)>(key: K): K extends keyof T ? IReference<T[K]> : any;
  value(): T extends (...args: any) => any ? any : Promise<T>;
}

const data = { foo: { bar: { value: 'foobar' }}}
declare const ref0: IReference<typeof data>

const ref1 = ref0.property('foo', 'bar', 'value')
const value = await ref1.value() // should be string

打字稿游乐场

标签: typescript

解决方案


您可以使用递归条件类型来遍历路径元组并检索目标 - 递归直到我们没有路径。

我不太确定 value() 在您的示例中的签名是什么 - 我相信这可能只是 Promise,但也许我遗漏了一些东西。

编辑 - 更新以添加自动完成功能。根据@jcalz 对他对这些问题的回答给出的一些回复,我已经对其进行了调整以添加参数的注释和名称。

对象属性路径的 TypeScript 类型定义

这里的要点是我们需要建立一个包含有效属性访问器序列的元组联合 - 即

{ a: { b: { c: 'foo' } } }

您有以下元组:

['a']
['a', 'b']
['a', 'b', 'c']

需要注意的另一件事是,任何递归类型都需要某种退出标准,因此编译器知道您不会无限遍历。鉴于我们正在挖掘一个对象——而不是迭代一个数组——我们可以使用一个计数器并在每次递归时递减它。当计数器达到最终值时never- 我们放弃递归。

如果未提供,则此处的默认限制设置为 10。 ( RecursiveDepthCounter extends number = 10)。


type PreviousNumber = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]

type Paths<Target extends object, RecursiveDepthCounter extends number = 10> =
    // Check if we've hit recursive depth, if so, bail
    [RecursiveDepthCounter] extends [never]
        ? never
        : {
            [key in keyof Target]: Target[key] extends infer TargetChild
                ? TargetChild extends object
                // If we have an object at TargetChild, then we can either access this object ([key])
                // or we need to recurse by calling Paths again, decrementing our recursive depth counter and appending
                // to the tuple of acceptable keys
                    ? [key] | [key, ...Paths<TargetChild, PreviousNumber[RecursiveDepthCounter]>]
                    // If we don't have an object, only key is permissable
                    : [key]
                // If we can't infer Target[key], only allow [key]
                : [key]
            // Access resulting object via keyof Target to remove nesting
        }[keyof Target];

type PropertyAtPath<Target extends unknown, Path extends readonly unknown[]> =
    // Base recursive case, no more paths to traverse
    Path extends [] 
        // Return target
        ? Target
        // Here we have 1 or more paths to access
        : Path extends [infer TargetPath, ...infer RemainingPaths]
            // Check Target can be accessed via this path
            ? TargetPath extends keyof Target 
                // Recurse and grab paths
                ? PropertyAtPath<Target[TargetPath], RemainingPaths>
                // Target path is not keyof Target
                : never
            // Paths could not be destructured
            : never;

export type IReference<T extends object> = {
    property<P extends Paths<T>>(...paths: [...P]): IReference<PropertyAtPath<T, P>>;
    value(): Promise<T>;
}

const data = { foo: { bar: { value: 'foobar' } } };
declare const ref0: IReference<typeof data>;

const ref1 = ref0.property('foo', 'bar');
const value = await ref1.value() // is string

TS 游乐场:hhttps://tsplay.dev/WGkyoW


推荐阅读