3

In TypeScript is there a way to get the type of a nested property based on some tupel? Given the following example let's say the tupel is ["bs", 0, "c"], then the type should be boolean (or ["bs", 0, "ds", 0, "f"] then number etc.).

interface Foo {
  a: string;
  bs: {
    c: boolean;
    ds: {
      e: null;
      f: number;
    }[];
  }[];
}

For some context, I want to type a function that takes two paramenters, a path and a value. For some object if the value at the given path is an array, it will push the value parameter. The implementation of this function can be found in this Playround. I was already looking at some solutions, for example in this issue, but I think my problem is a little bit different.

3 Answers 3

5

In TS4.1 there will be support for recursive conditional types which makes this somewhat straightforward:

type Keys = readonly (string | number)[];
type DeepIndex<T, KS extends Keys, Fail = undefined> =
    KS extends [infer F, ...infer R] ? F extends keyof T ? R extends Keys ?
    DeepIndex<T[F], R, Fail> : Fail : Fail : T;

Armed with DeepIndex, you could give add() the following type signature:

function add<KS extends Keys>(
    pathToArray: [...KS],
    value: DeepIndex<Foo, KS> extends Array<infer T> ? T : never
) {
    const xs = path(pathToArray, foo);

    if (Array.isArray(xs)) {
        xs.push(value);
    }
}

Leading to what you said is your desired behavior:

add(["bs", 0, "ds"], { e: null, f: 1 });
/* function add<["bs", 0, "ds"]>(pathToArray: ["bs", 0, "ds"], value: {
    e: null;
    f: number;
}): void */

add(["bs"], {});
// Argument of type '{}' is not assignable to parameter of 
// type '{ c: boolean; ds: { e: null; f: number; }[]; }'.

add(["a"], {});
// Argument of type '{}' is not assignable to parameter of type 'never'.(2345)
add(["bs", 0, "c"], {});
// Argument of type '{}' is not assignable to parameter of type 'never'.(2345)

Playground link to code

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks, this is helpful! Could you explain why pathToArray has to be [...KS] instead of just KS?
Generally given a value like ["foo", "bar"], the compiler will infer string[] and not a tuple. Using a type like [...KS] is a hint to the compiler that we want a tuple type and not an unordered array type.
3

You can do this with a recursive conditional type. which currently requires the typescript 4.1 beta.

type First<T extends any[]> = T extends [infer U, ...any[]] ? U : never
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never

type Drill<T, Path extends any[]> =
  First<Path> extends never               // Are there zero items in Path?
    ? T                                   // Zero items in Path, return the type passed in.
    : First<Path> extends keyof T         // Is the first item in Path a key of T?
      ? Drill<T[First<Path>], Rest<Path>> // Drill in one level, pass the rest of Path.
      : never                             // Not a keyof T, return never

Playground


The idea here is drill in to the type by the first entry in the key path. Then call that same type with the next entry in the key path until there are no items left. Then return the type at that point.

Before Typescript 4.1, a conditional type could not reference itself in its own definition. There are probably some cryptic and verbose workarounds in typescript 4.0, but that's gonna get hairy.

1 Comment

Ah yes thank you and @jcalz. TypeScript 4.1 will bring some cool new features, I was thinking there will be straightforward solution in the next version. Would love to accept both answers :)
0

You want to check for the function add(["bs", 0, "ds"], { e: null, f: 1 }); if the second parameter matches the type based on the first "path" parameter.

This path parameter is only known at runtime (e.g. add(path, { e: null, f: 1 }); would be also valid) so the compiler can't check this for you.

Therefore there is the need to check this at runtime and unfortunately, Typescript dose does not support a "reflection" API like e.g. Java for interfaces.

If you need this kind of flexibility you should consider implementing an input validation at runtime and using classes instead of interfaces, have a look into the https://www.npmjs.com/package/reflect-metadata package.

1 Comment

Well with a generic on the pathToArray parameter and the interface above you should have all the information you need. For example this Playground.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.