#31968•Medium

PathValue

Resolve a dotted path string such as 'user.address.city' to the type sitting at that spot, with template literal inference and recursion. Bad paths give never.

PathValue<T, P> takes an object type and a string like 'user.address.city', then hands back the type parked at the end of that path. If the path does not exist, the answer is never.

Config readers, form libraries and i18n helpers all do this at runtime. Doing it in the type system means the compiler can tell you that 'user.nmae' is a typo before the code ever runs. You will walk the string one segment at a time, and you will index into the object with whatever you pull off the front.

const data = {
  user: {
    name: 'Alice',
    age: 30,
    address: {
      city: 'Wonderland'
    }
  }
} as const
 
// The PathValue type should return the type of the value at the specified path.
type UserNameType = PathValue<typeof data, 'user.name'> // Type: 'Alice'

Challenge Instructions: PathValue

Medium

In this challenge, you are tasked with creating a type PathValue that will take an object and a string representing the path to a certain value within the object. Your job is to return the type of the value at that path. This will test your skills in generics, conditional types, and type inference.

Here's an example to get you started:

const data = {
user: {
name: 'Alice',
age: 30,
address: {
city: 'Wonderland'
}
}
} as const;
 
// The PathValue type should return the type of the value at the specified path.
type UserNameType = PathValue<typeof data, 'user.name'>; // Type: 'Alice'

View on GitHub: https://tsch.js.org/31968

Change the following code to make the test cases pass (no type check errors).

ChallengeSolution
/* _____________ Your Code Here _____________ */

type PathValue<T, P extends string> = any

/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'

type cases = [
  Expect<Equal<PathValue<typeof data, 'user.name'>, 'Alice'>>,
  Expect<Equal<PathValue<typeof data, 'user.address.city'>, 'Wonderland'>>,
  Expect<Equal<PathValue<typeof data, 'user'>, typeof data.user>>,
  Expect<Equal<PathValue<typeof data, ''>, never>>,
  Expect<Equal<PathValue<typeof data, 'user.number'>, never>>,
]

const data = {
  user: {
    name: 'Alice',
    age: 30,
    address: {
  

Pro Challenge

Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.

Monthly subscription. Cancel anytime.

Detailed Explanation

The whole thing fits in seven lines:

type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? PathValue<T[Key], Rest>
    : never
  : P extends keyof T
    ? T[P]
    : never

Two questions drive it. Does the path still have a dot in it? And is the segment I am looking at actually a key of the object I am holding?

Splitting off the first segment

Template literal patterns can be used with infer to take a string type apart. The pattern `${infer Key}.${infer Rest}` asks TypeScript to find a dot and bind everything before it to Key, everything after it to Rest. Matching is lazy, so Key stops at the first dot rather than the last one:

// P = 'user.address.city'
// Key  = 'user'
// Rest = 'address.city'

That is exactly the shape a recursive walk wants. Each step peels one segment off the left and keeps the tail for the next round.

Descending into the object

Having Key = 'user' is not enough to write T[Key]. TypeScript will reject the index unless it knows Key is a key of T, and Key at this point is just some string that happened to sit before a dot. The inner conditional does that narrowing:

[object Object]

Inside the true branch, Key is narrowed to a member of keyof T, so T[Key] type-checks and gives the nested object. We then call PathValue again with that nested object and the leftover path. For our example the next call is PathValue<typeof data.user, 'address.city'>, and the one after that is PathValue<typeof data.user.address, 'city'>.

The false branch is the typo guard. A path segment that names nothing collapses the whole lookup to never instead of leaking an error somewhere deeper.

Landing on the last segment

Once Rest has no dot left, the outer pattern stops matching and we fall into the second half:

[object Object]

Same narrowing trick, no recursion. PathValue<typeof data.user.address, 'city'> returns 'Wonderland', and that literal bubbles back up through every pending recursive call.

This branch is also what makes single-segment paths work without any special casing. PathValue<typeof data, 'user'> never enters the recursion at all: there is no dot, so it goes straight to T['user'].

Note that the literals survive because the test data is declared as const. Without it, name would widen to string and the challenge would be far less interesting.

Termination is easy to see here. Every recursive call consumes a dot, and a string type has a finite number of them, so the walk always reaches the dotless branch.

One free bonus: P sits naked on the left of extends, which makes the conditional type distributive. Pass a union of paths and you get a union of results, with each member resolved on its own.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges