#270β€’Hard

Typed Get

A fully typed version of lodash's get: parse a dot-separated path string with template literal inference and resolve it to the exact value type.

A path string like 'foo.bar.count' carries enough information for the compiler to work out the exact type sitting at that path.

The get function in lodash is a convenient helper for accessing nested values in JavaScript, but a naive TypeScript signature loses all type information and returns any. Get<T, K> fixes that: it parses a dot-separated path string at the type level and resolves it against a nested object type. The main ingredients are template literal inference with infer and recursion through object types; the trap is the order of your conditional branches.

For example,

type Data = {
  foo: {
    bar: {
      value: 'foobar',
      count: 6,
    },
    included: true,
  },
  hello: 'world'
}
 
type A = Get<Data, 'hello'> // 'world'
type B = Get<Data, 'foo.bar.count'> // 6
type C = Get<Data, 'foo.bar'> // { value: 'foobar', count: 6 }

Accessing arrays is not required in this challenge.

Challenge Instructions: Typed Get

Hard

The get function in lodash is a quite convenient helper for accessing nested values in JavaScript. However, when we come to TypeScript, using functions like this will make you lose the type information. With TS 4.1's upcoming Template Literal Types feature, properly typing get becomes possible. Can you implement it?

For example,

type Data = {
foo: {
bar: {
value: 'foobar',
count: 6,
},
included: true,
},
hello: 'world'
}
 
type A = Get<Data, 'hello'> // 'world'
type B = Get<Data, 'foo.bar.count'> // 6
type C = Get<Data, 'foo.bar'> // { value: 'foobar', count: 6 }

Accessing arrays is not required in this challenge.

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

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

πŸ‘‹ Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

The solution fits in six lines:

type Get<T, K> = K extends keyof T
  ? T[K]
  : K extends `${infer Head}.${infer Rest}`
    ? Head extends keyof T
      ? Get<T[Head], Rest>
      : never
    : never

This is a classic recursive descent: consume one path segment, index into the object, repeat on the remainder. The order of the checks is where the challenge hides its trap.

Branch 1: is the whole path a direct key?

K extends keyof T ? T[K] : ... tries the entire path string as a literal key first. For Get<Data, 'hello'> this hits immediately: 'hello' is a key of Data, so the result is Data['hello'], which is 'world'. Recursion done before it started.

This branch coming first is not a stylistic choice. Look at the test data:

type Data = {
  foo: { ... }
  'foo.baz': false
  hello: 'world'
}

Data contains a property whose name is literally 'foo.baz', dot included. The test expects Get<Data, 'foo.baz'> to be false, the value of that literal key, not an attempt to find baz inside foo (which doesn't exist). Because the direct-key check runs before any splitting, the literal key wins. Flip the branches and this test breaks. Whole-match-before-split is a good habit for any type-level parser.

Branch 2: split off the first segment

If the whole string isn't a key, K extends `${infer Head}.${infer Rest}` tries to split it. Template literal inference is lazy: Head matches the shortest possible prefix, so the split happens at the first dot:

[object Object]

Rest keeps its remaining dots. It's the rest of the path, ready to be parsed by the next recursive call. If the string contains no dot at all (and branch 1 already said it's not a key), there's nothing left to try and the result is never.

Branch 3: validate and recurse

Before indexing, Head extends keyof T confirms the segment actually exists on T. Without this guard, T[Head] wouldn't even compile, since TypeScript rejects indexing with a key it can't prove is valid. If the check passes, we recurse: Get<T[Head], Rest> continues the walk one level deeper. Tracing Get<Data, 'foo.bar.count'>:

// Get<Data, 'foo.bar.count'>  β†’ split: 'foo' + 'bar.count' β†’ Get<Data['foo'], 'bar.count'>
// Get<Data['foo'], 'bar.count'> β†’ split: 'bar' + 'count'   β†’ Get<..., 'count'>
// Get<{ value: 'foobar'; count: 6 }, 'count'>              β†’ direct key β†’ 6

Note that Get<Data, 'foo.bar'> stops one level earlier and returns the whole object { value: 'foobar'; count: 6 }: the recursion returns whatever type it lands on, object or primitive alike.

Why never for misses

Both failure paths (a head segment that isn't a key, and a dotless leftover that isn't a key) resolve to never. The test Get<Data, 'no.existed'> expects exactly that: 'no' is not a key of Data, so the guard in branch 3 fails on the very first step. never is the natural "no such path" answer in the type system, and it composes well: any code that consumes the result stays maximally strict instead of silently degrading to any.

Edge cases the tests cover

Once this clicks, you can extend the same skeleton with tuple-index handling or a custom fallback type. It's the foundation used by typed configuration lookups and form libraries everywhere.

This challenge is originally from here.

Share this challenge

Learn the Concepts