- Check if a string can be used as a path for an instance of a type
- Provide an easy-to-use generic type for your libraries
- (Ab)use your IDE’s autocompletion to provide the best developer experience
- Gradual typing means less resource usage
- Does not compute every single path on every keystroke
- Use numeric indices to access array elements
- Compatible with weird types like
any
# NPM
npm i -D intellipath
# Yarn
yarn add --dev intellipathThis module has one named export, IntelliPath.
All the internal types are documented and exported as the default export object if you wish to use them.
By itself, IntelliPath is not that useful:
import { IntelliPath } from "intellipath";
type T = {
a: {
b: number;
c: string[];
};
d: string;
};
type a = IntelliPath<T, "">; // a = "" | "a" | "d"
type b = IntelliPath<T, "a">; // b = "a" | "a.b" | "a.c"
type c = IntelliPath<T, "a.c.hi">; // c = "a.c" | "a.c.<index>"Its true utility shines when you create a “feedback loop” between its string type argument, a function type parameter, and a function parameter:
import { IntelliPath } from "intellipath";
type Test = {
nested: {
result?: unknown;
error?: {
description: string;
code: number;
};
};
array: { one: 1; two: 2; three: 3 }[];
};
function get<P extends string>(path: IntelliPath<Test, P>) {}
get("")
// ^ Place your cursor here and ^SpacePaste this code in a .ts file in your project and try out the autocompletion!
- Autocompletion is a bit finnicky, it works best when the string you’re trying to autocomplete is terminated.
get(" // ^ Trying to autocomplete this will do weird things
- TypeScript’s recursion limit for the path seems to be at around 10 children.
For a type T and a path P, the algorithm is:
- Split P as in
P.split("."), assign it toP - Let
Validbe""andCurrentTbeT -
- If
Pis empty, return[CurrentT, Valid] - Else let
Hdbe the head ofPas inP.shift()- If
Hdis a valid key ofCurrentT- Assign
T[Hd]toCurrentT - Join
Hdto the end ofValid, with a"."ifValidis not empty - Goto 3.
- Assign
- Else return
[CurrentT, Valid]
- If
- If
- Let
Keysbe the keys of the objectCurrentT - Return the union of
ValidandValid × Keys
The returned union contains the original P if it is a valid path.
AutocompleteHelper is a no-op that forces the IntelliSense engine to reevaluate both of its operands. This is the black magic that provides dot notation-like autocompletion when you press . on your keyboard.
Since metaprogramming in TypeScript is a pure functional language, it is easy to rewrite it for values instead of types. The following code block may be easier to understand than the original .d.ts file.
IntelliPath rewritten in JS
const Digits = "0123456789";
function IsNumberImpl(S) {
if (S === "") {
return true;
}
const m = S.match(/^(.)(.*)$/);
if (m) {
const [, _Hd, _Tl] = m;
if (Digits.includes(_Hd)) {
return IsNumberImpl(_Tl);
}
return false;
}
return false;
}
const IsNumber = S => S === "" ? false : IsNumberImpl(S);
function Split(S, _Acc = []) {
const m = S.match(/^(.*?)\.(.*)$/);
if (m) {
const [, _Hd, _Tl] = m;
return Split(_Tl, [..._Acc, _Hd]);
}
return [..._Acc, S];
}
const SafeDot = S => S === "" ? "" : `${S}.`;
function ExistingPath(T, _Path, _Valid = "") {
const [_Hd, ..._Tl] = _Path;
if (_Hd !== undefined) {
if (Array.isArray(T)) {
if (IsNumber(_Hd) === true) {
return ExistingPath(T[0], _Tl, `${SafeDot(_Valid)}${_Hd}`);
}
return [T, _Valid];
}
if (Object.keys(T).includes(_Hd)) {
return ExistingPath(T[_Hd], _Tl, `${SafeDot(_Valid)}${_Hd}`);
}
return [T, _Valid];
}
return [T, _Valid];
}
function SafeKeyof(T) {
if (Array.isArray(T)) {
return "<index>";
}
if (typeof T === "object") {
return Object.keys(T);
}
return "";
}
function GenerateValidPaths(T, _Path) {
const [_CurrentT, _ValidPath] = ExistingPath(T, _Path);
const _Keys = SafeKeyof(_CurrentT);
if (_Keys === "") {
return [_ValidPath];
}
return [
_ValidPath,
...[_Keys].flat().map(k => `${SafeDot(_ValidPath)}${k}`),
];
}
function IntelliPath(T, _Path) {
return GenerateValidPaths(T, Split(_Path)).filter(v => v !== "");
}You can copy-paste this code in any modern REPL (like your browser) to get the same behaviour as the type version:
> const T = {
a: {
b: 12,
c: ["a", "b"],
},
d: "string",
};
undefined
> IntelliPath(T, "")
[ "a", "d" ]
> IntelliPath(T, "a")
[ "a", "a.b", "a.c" ]
> IntelliPath(T, "a.c.hi")
[ "a.c", "a.c.<index>" ]Don’t hesitate to open an issue or a PR if you’d want more features, or if you see that something’s missing (even if it’s a typo).
There is no format for issues or commit messages. Just try to stay within the 60 character limits for commit titles, and write them in an imperative sentence.
Tests are in the t/ folder. If Perl isn’t installed on your system, you can open the .ts files there and check if there are compilation errors in your IDE.
