The answer given by @Gerrit0 is really cool, but like he said, his answer could easily lead to looping because the nature of his method is to navigate all possible substructure at once (so if any type repeats somewhere along the line, you fall into endless loop). I came up with a different approach with the following nameHelper
class. To use it, you call new nameHelper<myClassName>()
and start chaining .prop("PropertyName" | index)
, and finally use .name
to get the names joined by dots or brackets. TypeScript will detect any wrong property name if there's one. I hope this is what you're looking for.
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
Update
I shall put your modified version here for future reference. I made two further modification:
- I defined the
INameHelper
interface, just to prevent VS Code from showing a lot of unreadable stuffs when I hover my mouse above the key()
method.
- I change
path()
to a getter just to save two more characters of typing.
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
The tricky part of your latest request is that, as far as I know, there is no way to extract the string literal type from a give string variable without using generics, and we cannot have a function that looks like function<T, K extends keyof T>
but also use it in such a way that only T
is specified; either we don't specify both T
and K
and let TypeScript to infer them automatically, or we have to specify both.
Since I need the string literal type, I use the former approach and write the following pathfinder
function you wish for. You can pass either an object instance or a constructor function to it as the first parameter, and everything else works as desired. You cannot pass random parameters to it.
Also I have changed the definition of INameHelper so that even .key()
is not needed, and now the nameHelper
function is scoped and cannot be access directly.
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
There is however one imperfection still: by writing things this way, pathfinder
cannot be used to test static members of a class, because if you pass a class name to it, TypeScript will automatically treat it as a class constructor function instead of an object.
If you need to use it also on static class members, then you should remove | (new () => S)
in the definition, and whenever you are testing non-static members, you'll need to pass either an instance or SampleClass.prototype
to it.