我创建了一个简单的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
类型的结构相匹配?
答案 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将检测到任何错误的属性名称。我希望这就是您要寻找的。 p>
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
我将把您的修改版本放在这里,以备将来参考。我做了两个进一步的修改:
INameHelper
接口,只是为了防止VS Code在将鼠标悬停在key()
方法上方时显示很多不可读的内容。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
最新请求的棘手部分是,据我所知,如果不使用泛型,就无法从给定字符串变量中提取字符串文字类型,而且我们无法拥有看起来像{{1 }},但也应以仅指定function<T, K extends keyof T>
的方式使用;我们要么没有同时指定T
和T
,而是让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}}。