intellipath
v0.0.1
Published
IntelliSense-like autocompletion features for string literals in TypeScript
Downloads
4
Maintainers
Readme
IntelliPath
IntelliSense-like autocompletion features for string literals in TypeScript
Features
- 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
- Allows numeric indices to access array elements
Installation
# NPM
npm i -D intellipath
# Yarn
yarn add --dev intellipath
Usage
This 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 ^Space
Paste this code in a .ts file in your project and try out the autocompletion!
Caveats
- 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.
How does it work?
For a type T
and a path P
, the algorithm is:
- Split P as in
P.split(".")
, assign it toP
- Let
Valid
be""
andCurrentT
beT
- If
P
is empty, return[CurrentT, Valid]
- Else let
Hd
be the head ofP
as inP.shift()
- If
Hd
is a valid key ofCurrentT
- Assign
T[Hd]
toCurrentT
- Join
Hd
to the end ofValid
, with a"."
ifValid
is not empty - Goto 3.
- Assign
- Else return
[CurrentT, Valid]
- If
- If
- Let
Keys
be the keys of the objectCurrentT
- Return the union of
Valid
andValid × 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.
Equivalent value version
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.
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>" ]
Contributing
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.
Testing
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.