Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type safe version of lodash _.get - deconstruct array type conditionally

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.

like image 625
caesay Avatar asked Nov 13 '17 03:11

caesay


1 Answers

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.

like image 74
caesay Avatar answered Oct 17 '22 05:10

caesay