Skip to content

Instantly share code, notes, and snippets.

@esmevane
Last active September 13, 2021 20:31
Show Gist options
  • Save esmevane/c2a85eaff8fd0589279b6cc6f7893ef9 to your computer and use it in GitHub Desktop.
Save esmevane/c2a85eaff8fd0589279b6cc6f7893ef9 to your computer and use it in GitHub Desktop.
[ Typescript ]: Strongly typed, filterable keypaths
type GetKeypaths<
GivenType,
Filter extends string = '___!!!PLACEHOLDER!!!___'
> = GivenType extends object
? keyof GivenType extends string
? keyof GivenType extends Filter
? GetKeypaths<GivenType[keyof GivenType], Filter>
:
| keyof GivenType
| `${keyof GivenType}.${GetKeypaths<
GivenType[keyof GivenType],
Filter
>}`
: never
: never;
type GetStates<GivenType> = GetKeypaths<GivenType, 'states'>;
type Schema = {
states: {
one: {
states: {
two: {};
three: {
states: {
four: null;
};
};
five: {
states: {
six: {
states: {
seven: 'spork';
};
};
};
};
};
};
};
};
type K001 = GetStates<Schema>;
// Right now, this returns the following:
// type K001 =
// | "one"
// | "one.two"
// | "one.three"
// | "one.five"
// | "one.two.four"
// | "one.two.six"
// | "one.two.six.seven"
// | "one.three.four"
// | "one.three.six"
// | "one.three.six.seven"
// | "one.five.four"
// | "one.five.six"
// | "one.five.six.seven"
// But, it _should_ return this:
// type K001 =
// | "one"
// | "one.two"
// | "one.three"
// | "one.five"
// | "one.three.four"
// | "one.five.six"
// | "one.five.six.seven"
@esmevane
Copy link
Author

Looks like the correct type is:

type GetKeypaths<GivenType, Filter extends string = '__!!PLACEHOLDER!!__'> = {
  [Key in keyof GivenType & string]: GivenType[Key] extends object
    ? Key extends Filter
      ? GetKeypaths<GivenType[Key], Filter>
      : Key | `${Key}.${GetKeypaths<GivenType[Key], Filter>}`
    : Key;
}[keyof GivenType & string];

export type GetStates<GivenType> = GetKeypaths<GivenType, 'states'>;

This changes what happens a bit internally. Instead of just distributing out keys, it goes into the object at each level and updates the object properties to contain the keypath, then unravels that object and returns the values. This makes it impossible to get that odd distribution bug (since we're only modifying things that are proven to exist via the mapped type logic).

Found the way to this answer by adapting the response to this overflow ticket: https://stackoverflow.com/questions/65332597/typescript-is-there-a-recursive-keyof

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment