Not a long time ago I discovered type-challenges for myself. Today I'll show not only the implementation of
Get, but also some common issues with the implementation, improvements and its usage in production.
If you want to learn TypeScript concepts first, please take a look at the Summary ⚓️
As I said, there's a repo on GitHub: type-challenges. The current challenge is located in the "hard" category.
Here we work only with objects (as the solution doesn't require accessing arrays and tuples) and also we always can access object keys as they are defined in test cases.
So what should we start then from?
path.split('.'). In Typescript, we also need to get the keys from the path string.
Thankfully, since TypeScript 4.1, we have Template Literal types. We can infer the keys by removing dots.
We can use the
Path type to do so:
It looks very simple and short. However once we write tests, we understand what was missed, as seen in the Playground validation. We forgot the case with only one single key left. Let's add it:
To try it out, we can have a look at the Playground with tests cases.
After having the keys, we can finally call
keys.reduce. To do so, we use another type
GetWithArray so we already know that keys are a string tuple:
To explain it in more detail:
K extends [infer Key, ...infer Rest]checks that we have at least one element in a tuple
Key extends keyof Olets us use
O[Key]to move recursively to the next step
Let's test it again on the Playground. We again forgot the last case with a tuple without elements. Let's add it:
Let's test it together to clarify everything's working as expected: Playground
Great, we did it ✅
When we work with real data in production, we don't always know if the data is valid or not. In this case we have optional paths all over the project.
Let's add test cases with optional paths and see what happens in the Playground.
Optional paths are not working properly. The reason behind it is simple. Let's go through one example to find the problem:
We cannot extract a key from an object if it can be
Let's fix it step by step:
First, we declare 3 simple filters:
We detect if
null exist within the union type and, if so, delete it from the union. At the end we work only with the rest.
You can find the test cases again on the Playground
Remember what we did for the type challenge. Let's extend our solution to support optional paths:
- We need to make sure the key exists in the keys of an optional object
- Otherwise, we assume it doesn't exist
Let's add tests and check if it's working in the Playground
Good job ✅
The next desired step for us is to support arrays and tuples:
Here, a key can be either a
string or a
number. We already know how to get the keys with
As for objects, we can similarly call
keys.reduce for arrays. To do so, we will implement the same type
GetWithArray but for arrays and then combine two solutions of
GetWithArray in one.
First, let's take a basic implementation for objects and adapt it for arrays. We use
A instead of
O for semantic reasons:
After testing in the Playground, we found several gaps:
- Arrays cannot have a
'1' extends keyof string is
false therefore it returns
- Similarly for readonly arrays:
- Tuples (e.g.
[0, 1, 2]) return
Let's fix that as well! 🚀
For arrays we want to get
T | undefined, depending on the values inside the array. Let's infer that value:
A extends readonly (infer T) as all arrays extend readonly arrays.
Only need to fix the final case with tuples. Please check the tests in the Playground.
At the moment, if we try to extract value by non-existing index from tuples, we will get the following:
To fix it, we need to distinguish tuples from arrays. Let's use
ExtendsTable to find correct condition:
Let's use it for different types:
Let me create the table to clarify what is located inside
We just created the table of
extends for TypeScript types.
If you see ✅ for the row and the column, it means the row type extends the column type. Several examples:
 extends 
number extends readonly number
On the other hand if it's ❌, the row type doesn't extend the column type. More examples:
number extends 
readonly number extends number
Let's take a closer look at row
any: for column
 it's ❌, but for other types it's ✅
This is actually an answer! 🔥🔥🔥
GetWithArray using the condition
any extends A:
- We distinguish arrays from tuples using
any extends A
- For arrays we infer
T | undefined
- For tuples, we extract their value if index exists
- Otherwise, we return
If you want to see it all in one place, don't forget to check out the Playground ✅
Now we have 2 solutions:
- For objects
- For arrays and tuples
Let's move the details of the implementation to functions
As you can see, we’ve added restrictions to both functions:
O extends Record<PropertyKey, unknown>. It means that it accepts only general objects
A extends readonly any, which means that it accepts only general arrays and tuples
This helps to distinguish cases and avoid mistakes while passing types. Let's reuse them in
I covered this refactoring with tests. Another Playground is waiting for you 🚀.
At the moment we use
lodash in our project, e.g. function
get. If you check common/object.d.ts in
@types/lodash, you'll see that it's quite straightforward. The
get call in playground returns
any for most of the cases: typescript-lodash-types
reduce with any
for loop (e.g.
for-of) to have early exit in case
Let's try to cover the
get function with types we just wrote. Let's divide it into 2 cases:
Gettype can be used iff (if and only if) all the restrictions can be applied and the type is correctly inferred
- A Fallback type is applied iff the validation is not passed (e.g. we pass
To have 2 type overloads we need to use
The implementation is ready ✅
But we still need to use our
Get type, let's add it:
Please check the final solution in Codesandbox 📦:
- We added the implementation of get with types 🔥
- We covered the types with tests 🧪
- We covered the get function with tests 🧪
To solve the challenge we needed to know several TypeScript concepts:
- Tuples were introduced in TypeScript 1.3, but Variadic Tuple Types were only released in TypeScript 4.0 so we can use spread inside them:
- Conditional types which were introduced in TypeScript 2.8
inferkeyword in conditional types which was also introduced in TypeScript 2.8
- Recursive conditional types, which were introduced in TypeScript 4.1
- Template Literal types, which were also introduced in TypeScript 4.1