I'd like to write a function which gets a value from an object given an array of property keys. It would look something like this:
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
}
I'd like this function to work like so:
type Foo = {
a: number
b: string
c: {
lark: boolean
wibble: string
}
}
let o: Foo = {
a: 1,
b: "hi",
c: {
lark: true,
wibble: "there"
}
}
// I'd like these to type check (and return the expected values):
getValue(o, ['a']) // returns 1
getValue(o, ['b']) // returns "hi"
getValue(o, ['c','lark']) // returns true
// I'd like these _not_ to type check:
getValue(o, ['a','b'])
getValue(o, ['d'])
Importantly, I'd like to have a type (like ObjKeys<O>
in the above example) available so that I can make use of this function easily from other functions, while preserving the typing. For instance I might want to do something like this:
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 through to our getValue
above, and ideally it would all type check because the object O
and keys ObjKeys<O>
are both valid arguments to the getValue
function called within.
This extends to returning the value given back by getValue
; I might want to also 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 know what the return type will be, and so would fully typecheck.
Is there a solution to this, or is there simply no way to do this sort of thing in TypeScript as it stands (version 4 at time of writing)?
The best I have so far:
I can define a function that allows for nested access with something like 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
And then this typechecks properly as I try to access values (up to 3 layers deep in this case).
However, I get a bit stuck when I want to use that function from another while preserving type safety:
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 not sure what I could put in place of ????
to tell TypeScript how the types relate to each other so that this would type check. Can I avoid having to rewrite my big list of overloads each time I want to write functions like the above, but still ge tthe type checking I want?