I wish to create a function that retrieves a value from an object using an array of property keys. Here's an example implementation:
function getValue<O, K extends ObjKeys<O>>(obj: O, keys: K): ObjVal<O,K> {
let out = obj;
for (const k in keys) out = out[k]
return out
}
This function should operate as follows:
type Foo = {
a: number
b: string
c: {
lark: boolean
wibble: string
}
}
let o: Foo = {
a: 1,
b: "hi",
c: {
lark: true,
wibble: "there"
}
}
// The following calls should type check and return the expected values:
getValue(o, ['a']) // returns 1
getValue(o, ['b']) // returns "hi"
getValue(o, ['c','lark']) // returns true
// These calls should not type check:
getValue(o, ['a','b'])
getValue(o, ['d'])
It is important to have a type available (like ObjKeys<O>
) so that this function can be easily utilized in other functions while maintaining typing integrity. For instance, one might want to do something like:
function areValuesEqual<O>(obj: O, oldObj: O, keys: ObjKeys<O>) {
let value = getValue(obj, keys)
let oldValue = getValue(oldObj, keys)
return value === oldValue ? true : false
}
This function takes some keys and passes them to our getValue
function above, and ideally, it would all pass type checking because the object O
and keys ObjKeys<O>
are valid arguments for the getValue
function being called.
This concept extends to returning the value obtained by getValue
; one might also want to do something like this:
function doSomethingAndThenGetValue<O>(obj: O, oldObj: O, keys: ObjKeys<O>): ObjVal<O> {
let value = getValue(obj, keys)
console.log("Value obtained is:", value)
return value
}
This also uses something like ObjVal<O>
to determine the return type, ensuring full typechecking.
Is there a solution to this challenge, or is it currently impossible in TypeScript (as of version 4)?
The best approach I've found so far:
I can define a function that facilitates nested access with code similar to the following:
function getValue<
O extends object,
K1 extends keyof O
>(obj: O, keys: [K1]): O[K1]
function getValue<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1]
>(obj: O, keys: [K1,K2]): O[K1][K2]
function getValue<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1],
K3 extends keyof O[K1][K2]
>(obj: O, keys: [K1,K2,K3]): O[K1][K2][K3]
function getValue<O>(obj: O, keys: Key | (Key[])): unknown {
let out = obj;
for (const k in keys) out = out[k]
return out
}
type Key = string | number | symbol
With this method, proper type checking occurs when accessing values up to three layers deep.
However, I encounter difficulties when trying to use that function while preserving type safety in another context:
function areValuesEqual<O>(obj: O, oldObj: O, keys: ????) {
let value = getValue(obj, keys)
let oldValue = getValue(oldObj, keys)
return value === oldValue ? true : false
}
function doSomethingAndThenGetValue<O>(obj: O, oldObj: O, keys: ????): ???? {
let value = getValue(obj, keys)
console.log("Value obtained is:", value)
return value
}
I'm unsure what to substitute for ????
to inform TypeScript about the relationships between types for successful type checking. Is there a way to avoid having to redefine the list of overloads every time functions like these are written, yet still achieve the desired type checking?