#Convert leaf nodes of a ragged object to their paths

73 messages · Page 1 of 1 (latest)

tired quiver
#

Given a sample object like this:

const x = {
  outer: {
    second: {
      one: 'a',
      two: 'b'
    }
  },
  another: {
    three: 'c'
  },
  four: 'd'
}

how can I create a type helper KeyPaths<T> that when used like KeyPaths<typeof x> it returns

'outer.second.one' | 'outer.second.two' | 'another.three' | 'four'

?

#

Note that any value that is an object is omitted from the list.

cerulean stone
#

!:autopath

hollow tartanBOT
#
T6#2591

Preview:```ts
...
type User = {
id: string,
name: string,
age: number,
friends: User[],
parents: [User, User],
}

get(user, "friends.0") // returns string
get(user, "friends.0.age") // returns number
get(user, "this.does.not.exist") // error
get(user, "parents.3") // error, out of bounds

getString(user, "friends.0.id") // returns string
getString(user, "friends.0.age") // error, not a string```

cerulean stone
#

plus some minor modifications to exclude objects

tired quiver
# cerulean stone plus some minor modifications to exclude objects

Wow, complicated.

Conceptually, I was thinking of this, but I don't know how to do a "mapped array" that doesn't produce an object...

type Combine<TLeft extends string, TRight extends string> = TLeft extends '' ? TRight : `${TLeft}.${TRight}`
type KeyPaths<T, TBase extends string = ''> = T[P in keyof T] extends string
  ? Combine<TBase, P>
  : KeyPaths<T[P], Combine<TBase, P>>
#

I mean, obviously the above is incorrect.

cerulean stone
#

!hb discrim

hollow tartanBOT
cerulean stone
#

keyof T extends infer P ? P extends infer P ? use P here : never : never

#

uhhh

#

or rather

cerulean stone
#

!hb dist

hollow tartanBOT
cerulean stone
#

but also idk if you even need distribution

#

i think you do

tired quiver
#

I just didn't know how to logically do the inference P of each item in the union

hollow tartanBOT
cerulean stone
#

!ts

hollow tartanBOT
#
const x = {
  outer: {
    second: {
      one: 'a',
      two: 'b'
    }
  },
  another: {
    three: 'c'
  },
  four: 'd'
};


type Combine<TLeft extends string, TRight extends string> = TLeft extends '' ? TRight : `${TLeft}.${TRight}`
type KeyPaths<T, TBase extends string = ''> = keyof T extends infer P extends keyof T & string ? P extends P ? T[P] extends string
  ? Combine<TBase, P>
  : KeyPaths<T[P], Combine<TBase, P>> : never : never;

type What = KeyPaths<typeof x>;
//   ^? - type What = "four" | "outer.second.one" | "outer.second.two" | "another.three"```
hollow tartanBOT
#
Chen Sida#0813

Preview:```ts
const x = {
outer: {
second: {
one: "a",
two: "b",
},
},
another: {
three: "c",
},
four: "d",
}

type _KeyPaths<
T,
Prefix extends string = ""

= `${Prefix}.${{
[K in keyof T]: K extends string
? T[K] extends object
? _KeyPaths<T[K], K>
: K
: neve
...```

trim crescent
#

My stab

tired quiver
#

Is there a way to make a TS Playground line show the error message at a location, similar to \\ ^? but to show an error message instead of the type at that location?

trim crescent
#

No

tired quiver
#

Ok. Thanks! Would be helpful.

#

Why is the P extends P necessary?

cerulean stone
#

typescript iterates through unions instead of passing the entire union, when the left side of an extends is a bare type parameter

tired quiver
# trim crescent My stab

That is clever, and I was thinking about how an object could easily be turned into its values using { }[key of T] like you did.

cerulean stone
#

so T, P, K etc

#

:D

tired quiver
#

Thank you both!

#

The complexity of TypeScript is kind of cool, even though sometimes I get stuck, but I know I can master it eventually...

cerulean stone
hollow tartanBOT
#
n_n#2622

Preview:ts ... type KeyPaths<T, Prefix extends string = never> = `${[ Prefix ] extends [never] ? "" : `${Prefix}.`}${{ [K in keyof T & string]: T[K] extends object ? KeyPaths<T[K], K> : K }[keyof T & string]}` ...

trim crescent
#

Fair

#

Last time I tried = never, I distributed over it so the entire thing became never

tired quiver
#

See, little tricks like [K in keyof T & string] are cool. 🙂

#

it's [never], not never, which changes how it distributes

#

... right, n_n? 😄

cerulean stone
#

wops x 2

hollow tartanBOT
#
n_n#2622

Preview:ts ... type KeyPaths<T> = { [K in keyof T & string]: T[K] extends object ? `${K}.${KeyPaths<T[K]>}` : K }[keyof T & string] ...

cerulean stone
cerulean stone
trim crescent
cerulean stone
#

yeah

#

regular recursion > prolog recursion, lol

tired quiver
#

I'm creating some config and want to be able (for now) to require one part of the config to only mention paths from another part of the config.

trim crescent
#

Mad people stuck with recursion

cerulean stone
trim crescent
tired quiver
#

The codebase I inherited for this project has secrets committed to code, which is horrible, but I can't fix it all at once. So the first step is to put all the secrets into their own object, and refer to them in the regular config. Then I can migrate the secrets into proper secret storage.

#

For now it's all just literals, so I can just do const secrets = {} (my x object above) and then in the const config = { } I can type some keys as secret-name references

#

And if a secret is compound, I can figure out how to jimmy the paths computation to include the legitimate secrets that are objects, e.g. { username: string, password: string } or something like that.

#

!resolved

#

Thank you both!

#

... definitely loving the type intersection trick to filter the keys of T into their string values only

tired quiver
cerulean stone
cerulean stone
#

i think Extract works better to make unresolved generics satisfy constraints?

cerulean stone
tired quiver
#

@cerulean stone Is there an easy way to get your last version of KeyPaths to stop at string types and not go into the methods on string such as anchor, [Symbol.iterator], at, big, blink, bold, charAt, charCodeAt, ...?

#

It's creating very many more types in the union than I expected.

#

My attempt failed

type KeyPaths<T> =
  T extends string
    ? T
    : { ... }
#

But string extends object is false, so why is it traversing into String prototype properties?

#

Also, I upgraded my IDE (WebStorm/IntelliJ) to the latest version and now KeyPaths is not recursing (in IDE intellisense) into the children.

cloud quarry
tired quiver
cloud quarry
#

did you change KeyPaths or the input?

tired quiver
tired quiver