Reputation: 23
I'm trying to add index signature typings to a nested object so I can access some of its properties via computed values.
For example, given this object:
export const english = {
home: {
title: 'Title',
characters_list: 'Characters list',
moves: 'Moves',
header: 'Home',
search: 'Search',
},
character: {
detail: {
about: {
title: 'About',
info: 'Info',
full_name: 'Full name',
status: 'Status',
gender: 'Gender',
},
origin: {
title: 'Origin',
origin: 'Origin',
current_location: 'Current location',
},
},
},
}
I'd like to access its properties like this:
const key = 'moves';
english.home[key]
But I can't get it right. So far I've been able to type the first layer
export type Translation = {
[key: string]: typeof english[keyof typeof english]
}
I think this needs some sort of recursive approach, but I have no idea how to implement it
Upvotes: 2
Views: 745
Reputation: 7574
OK, I think I know what we need to do. The problem is that objects allow keys to be of every value in type string
, but once you define a type, you work with a subset of string
. Hence TypeScript throws -- rightfully so -- an error that you can't just access with every string, but only the strings that are valid keys (e.g. moves
, title
, etc.)
So, at one point in time, you have to make sure that the key you want to access is valid.
Here are the possibilities
a) A string literal w const context
The moment you assign
const key = 'moves'
key
is not of type string
, but of type 'moves'
, a very specific value type (or literal type) and sub-type of string
. This access should work
const key = 'moves'
english.home[key] // 👍
Alternatively, define them as const
to get the same effect
let key = 'moves' as const // also 'moves', not string
Same behavior with a generic pick function
function pick<T extends Object, K extends keyof T>(o: T, k: K) {
return o[k]
}
pick(english.home, 'moves')
pick(english, 'home')
pick(pick(english, 'home'), 'moves')
This is basically the same as direct property access, but you let the generic constraints help you finding the right types.
b) Type predicates to narrow down
But you are not working with string literals, you work with computed keys. Which means you have to somehow come from the big set of strings to the smaller set of actual keys. You can do this with a function that checks if this key is valid (which you should do anyway), and a type predicate to narrow down the type in control flow (you see what that is in a second)
So imagine a helper function like this:
function isKeyOf<O extends Object>(o: O, k: string | number | symbol): k is keyof O {
return typeof k === "string" && Object.keys(o).indexOf(k) >= 0
}
There is a lot going on in this function.
k
passed in which can be of all possible object key typeskeyof O
Type predicates work for functions that return a boolean value. If this condition is true, then the type becomes something different (namely, keyof O
)
You can use this to create a pickSafe
function, where you can pass any key
function pickSafe<T extends Object>(o: T, k: string) {
if (isKeyOf(o, k)) {
return o[k]
}
throw new Error('Not accessible')
}
pickSafe(english.home, 'moves')
pickSafe(english, 'home')
pickSafe(pickSafe(english, 'home'), 'moves')
The problem is, that while this access works, you lose information on the object you are accessing. You would need to do runtime type-checks, then. Do
const res = pickSafe(english, 'home')
.. and hover over res
, you see what possible types can come out of this
This is actually desired behavior, as it tells you the places in your app where things can get unclear.
If you really know that your app works and computed keys are what they are, you can always do a type-cast to override the uncertainty of TypeScript
const keyey = myRandomString as keyof typeof english.home
pick(english.home, keyey)
Here's a playground for you to fiddle around.
I hope this helped!
Upvotes: 1
Reputation: 209
You can use simple javascript function reduce
const key = 'home.moves';
const value = key.split('.').reduce((obj,prop) => obj[prop], english);
Upvotes: 1