For a long time we've been having a problem that the only way to access a nested property safely and easily is to use _.get
. Ex:
_.get(obj, "Some.Nested[2].Property", defaultValue);
This works great, but doesn't stand up to property renames as frequently happens. Theoretically, it should be possible to transform the above into the following and allow TypeScript to implicitly type check it:
safeGet(obj, "Some", "Nested", 2, "Property", defaultValue);
I've been successful in creating such a typing for everything except for array types:
function getSafe<TObject, P1 extends keyof TObject>(obj: TObject, p1: P1): TObject[P1];
function getSafe<TObject, P1 extends keyof TObject, P2 extends keyof TObject[P1]>(obj: TObject, p1: P1, p2: P2): TObject[P1][P2];
This properly checks for items at depth (I will auto generate these statements to 10 levels or so). It fails with array properties because the type passed into the next parameter is T[]
and not T
.
The complexity or verbosity of any solution is of no consideration as the code will be auto generated, the problem is that I can't seem to find any combination of type declarations which will allow me to accept an integer parameter and deconstruct the array type moving forward.
You can deconstruct an array (where T
is an array) using T[number]
. The problem is that I have no way to constrain where T
is an array on a nested property.
function getSafe<TObject, P1 extends keyof TObject, P2 extends keyof TObject[P1][number]>(obj: TObject, p1: P1, index: number, p2: P2): TObject[P1][number][P2];
^^^^^^ ^^^^^^
const test2 = getSafe(obj, "Employment", 0, "Id"); // example usage
That actually works at the call-site (no errors there, correctly gives us param and return types), but gives us an error in the declaration itself because you can't index TObject[P1]
with [number]
as we can't guarantee TObject[P1]
is an array.
(Note: TType[number]
is a viable way to get the element type from an array type, but we need to convince the compiler that we're doing this to an array)
The question is really, would there be away to add a array constraint to TObject[P1]
or is there another way to do this that I'm missing.
I've since figured this out and published an npm package here: ts-get-safe
The key point was figuring out how to conditionally restructure the array into it's element type. In order to do that, you first have to assert that all the properties are either an array or never
. The type that solved the equation was:
type GSArrEl<TKeys extends keyof TObj, TObj> = { [P in TKeys]: undefined[] & TObj[P] }[TKeys][number];
The magic is in { [P in TKeys]: undefined[] & TObj[P] }
where we essentially union each property of TObj
to undefined[]
. Because we are then sure that each property is either an array or never
(it will be never
on each property which is not an array), we can then do the destructuring expression [number]
to get the element type.
Here is an example of two array destructuring happening at the same time:
function getSafe<TObject, P0 extends keyof TObject, A1 extends GSArrEl<P0, TObject>, P2 extends keyof A1, P3 extends keyof A1[P2], A4 extends GSArrEl<P3, A1[P2]>>(obj: TObject, p0: P0, a1: number, p2: P2, p3: P3, a4: number): A4;
Hundreds of combinations of arrays and object properties have been generated in my ts-get-safe
library and are ready to use, however I'm still open to ways to improve this in a generic way such that we can use a dynamic number of parameters in the same declaration. Even a way to combine Array and Property navigation into the same type constraint so we don't have to generate every variation of array and property access.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With