如何匹配打字稿中的嵌套键

时间:2019-06-14 21:30:46

标签: typescript

我创建了一个简单的nameOf助手,用于打字稿。

function nameOf<T>(name: Extract<keyof T, string>) {
  return name;
}

在某个函数需要一个表示属性键的字符串但没有正确键入的地方,我可以这样使用它:expectsKey(nameOf<MyObject>("someMyObjectProperty"))。这意味着即使我不控制expectsKey(key: string),也可以对传递给它的字符串进行某种类型检查。这样,如果MyObject上的属性被重命名,则nameOf()调用将显示一个错误,该错误直到执行后普通函数才能检测到。

是否可以将其扩展到嵌套元素?

ie,是否可以通过某种方式对nameOf<MyComplexObject>('someProperty[2].someInnerProperty')进行类型检查以确保其与MyComplexObject类型的结构相匹配?

2 个答案:

答案 0 :(得分:4)

直接吗?不能。您无法创建连接属性以在TS中创建新字符串,而此功能将需要此字符串。

但是,您可以通过映射类型获得类似的功能。

interface MyObject {
  prop: {
    arr?: { inner: boolean }[]
    other: string
    method(): void
  }
}

// Creates [A, ...B] if B is an array, otherwise never
type Join<A, B> = B extends any[]
  ? ((a: A, ...b: B) => any) extends ((...args: infer U) => any) ? U : never
  : never

// Creates a union of tuples descending into an object.
type NamesOf<T> = { 
  [K in keyof T]: [K] | Join<K, NamesOf<NonNullable<T[K]>>>
}[keyof T]

// ok
const keys: NamesOf<MyObject> = ['prop']
const keys2: NamesOf<MyObject> = ['prop', 'arr', 1, 'inner']

// error, as expected
const keys3: NamesOf<MyObject> = [] // Need at least one prop
const keys4: NamesOf<MyObject> = ['other'] // Wrong level!
// Technically this maybe should be allowed...
const keys5: NamesOf<MyObject> = ['prop', 'other', 'toString']

您不能在nameOf函数中直接使用它。这是一个错误,因为类型实例化将被检测为可能是无限的。

declare function nameOf<T>(path: NamesOf<T>): string

但是,如果使TypeScript的分辨率推迟到实际使用该函数之前,则可以使用NamesOf。您可以通过将其包含为通用默认值,或通过将参数类型包装在条件中来轻松实现此目的(这提供了在类型为原始类型时防止使用nameOf的其他好处)

interface MyObject {
  prop: {
    arr?: { inner: boolean }[]
    other: string
    method(): void
  },
  prop2: {
    something: number
  }
}

// Creates [A, ...B] if B is an array, otherwise never
type Join<A, B> = B extends any[]
  ? ((a: A, ...b: B) => any) extends ((...args: infer U) => any) ? U : never
  : never

// Creates a union of tuples descending into an object.
type NamesOf<T> = { 
  [K in keyof T]: [K] | Join<K, NamesOf<NonNullable<T[K]>>>
}[keyof T]

declare function nameOf<T>(path: T extends object ? NamesOf<T> : never): string

const a = nameOf<MyObject>(['prop', 'other']) // Ok
const c = nameOf<MyObject>(['prop', 'arr', 3, 'inner']) // Ok
const b = nameOf<MyObject>(['prop', 'something']) // Error, should be prop2

如果您走另一条路线并将该路径包括在通用约束中,请确保将路径标记为默认路径(因此在使用该函数时不必指定该路径)和扩展{ 1}}(这样NameOf<T>的用户就不会说谎)

nameOf

答案 1 :(得分:1)

@ Gerrit0给出的答案确实很酷,但是就像他说的那样,他的答案很容易导致循环,因为他的方法的本质是一次浏览所有可能的子结构(因此,如果有任何类型重复出现,您陷入无尽循环)。我在下面的nameHelper类中提出了另一种方法。要使用它,您需要调用new nameHelper<myClassName>()并开始链接.prop("PropertyName" | index),最后使用.name来获得由点或括号连接的名称。如果存在,TypeScript将检测到任何错误的属性名称。我希望这就是您要寻找的。

class nameHelper<T> {
    private trace: PropertyKey[];

    constructor(...trace: PropertyKey[]) {
        this.trace = trace;
    }

    public prop<K extends keyof T>(name: K & PropertyKey) {
        this.trace.push(name);
        return new nameHelper<T[K]>(...this.trace);
    }

    public get name() { return this.trace.join(".").replace(/\.(\d+)\./g, "[$1]."); }
}

class SampleClass {
    public Prop: InnerClass;
    public Arr: InnerClass[];
}

class InnerClass {
    public Inner: {
        num: number
    }
}

console.log(new nameHelper<SampleClass>().prop("Prop").prop("Inner").prop("num").name);
// "Prop.Inner.num"

console.log(new nameHelper<SampleClass>().prop("Arr").prop(2).prop("Inner").prop("num").name);
// "Arr[2].Inner.num"

console.log(new nameHelper<SampleClass>().prop("Prop").prop("Other").name);
// error here

更新

我将把您的修改版本放在这里,以备将来参考。我做了两个进一步的修改:

  1. 我定义了INameHelper接口,只是为了防止VS Code在将鼠标悬停在key()方法上方时显示很多不可读的内容。
  2. 我将path()更改为吸气剂只是为了节省另外两个键入字符。
interface INameHelper<T> {
    readonly path: string;
    key: <K extends keyof T>(name: K & PropertyKey) => INameHelper<T[K]>;
}

function nameHelper<T>(...pathKeys: PropertyKey[]): INameHelper<T> {
    return {
        get path() { return pathKeys.join('.').replace(/\.(\d+)\./g, '[$1].')},
        key: function <K extends keyof T>(name: K & PropertyKey) {
            pathKeys.push(name);
            return nameHelper<T[K]>(...pathKeys);
        },
    };
}

class SampleClass {
    public Prop: InnerClass;
    public Arr: InnerClass[];
}

class InnerClass {
    public Inner: {
        num: number
    }
}

console.log(nameHelper<SampleClass>().key("Prop").key("Inner").key("num").path);
// "Prop.Inner.num"

console.log(nameHelper<SampleClass>().key("Arr").key(2).key("Inner").key("num").path);
// "Arr[2].Inner.num"

console.log(nameHelper<SampleClass>().key("Prop").key("Other").path);
// error here

Update2

最新请求的棘手部分是,据我所知,如果不使用泛型,就无法从给定字符串变量中提取字符串文字类型,而且我们无法拥有看起来像{{1 }},但也应以仅指定function<T, K extends keyof T>的方式使用;我们要么没有同时指定TT,而是让TypeScript自动推断它们,要么必须同时指定两者。

由于我需要字符串文字类型,因此我使用前一种方法,并编写了所需的以下K函数。您可以将对象实例或构造函数作为第一个参数传递给它,其他所有东西都可以按需工作。您不能向其传递随机参数。

我还更改了INameHelper的定义,因此甚至不需要pathfinder,现在.key()函数的作用域已确定,无法直接访问。

nameHelper

然而,仍然存在一个缺陷:通过这种方式编写代码,interface INameHelper<T> { readonly path: string; <K extends keyof T>(name: K & PropertyKey): INameHelper<T[K]>; } function pathfinder<S, K extends keyof S>(object: S | (new () => S), key: K) { function nameHelper<T>(pathKeys: PropertyKey[]): INameHelper<T> { let key = function <K extends keyof T>(name: K & PropertyKey) { pathKeys.push(name); return nameHelper<T[K]>(pathKeys); }; Object.defineProperty(key, "path", { value: pathKeys.join('.').replace(/\.(\d+)/g, '[$1]'), writable: false }); return <INameHelper<T>>key; } return nameHelper<S[K]>([key]); } class SampleClass { public Prop: InnerClass; public Arr: InnerClass[]; } class InnerClass { public Inner: { num: number } } var obj = { test: { ok: 123 } }; console.log(pathfinder(SampleClass, "Prop")("Inner")("num").path); // "Prop.Inner.num" console.log(pathfinder(SampleClass, "Arr")(2)("Inner")("num").path); // "Arr[2].Inner.num" console.log(pathfinder(obj, "test")("ok").path); // "test.ok" // works with instance as well console.log(pathfinder(SampleClass, "Prop")("Other").path); // error here console.log(pathfinder(InnerClass, "Prop").path); // also error here 不能用于测试类的静态成员,因为如果将类名传递给它,TypeScript会自动将其视为类构造函数而不是对象。

如果还需要在静态类成员上使用它,则应删除定义中的pathfinder,并且每当测试非静态成员时,都需要传递实例或{{ 1}}。