Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Infer type of nested keyof Properties

I want to define an Array type that must contain a chain of nested property names of a given type.

Let's say I have a type:

    type Foo = {
        outer: {
            inner: any;
        }
    }

Now I want to define an Array type with 2 elements:

type PropertyList<T, K1 extends keyof T, K2 extends keyof T[K1]> = [K1, K2];

I want to use it like this:

let myList:PropertyList<Foo> = ["outer", "inner"]

So I want the compiler to check if the 2 contained Property names are nested property names of Foo.

But I can't define PropertyList with only 1 generic parameter, I get this error then:

TS2314: Generic type 'PropertyList' requires 3 type argument(s)

Any idea how i can infer the nested keyof types without having to specify them?

like image 758
Reducer Avatar asked Feb 27 '18 09:02

Reducer


2 Answers

Edit

I think I misunderstood your question and don't think that's possible (yet). Maybe inferring the positional types will be possible with type inference for conditional types that will be included in the upcoming version 2.8 of TypeScript.

I will leave the original answer below just in case.


Type parameters can have default values:

type PropertyList<T, K1 extends keyof T = keyof T, K2 extends keyof T[K1] = keyof T[K1]> = [K1, K2];

But maybe in your case it makes more sense to not specify K1 and K2 as type parameters:

type PropertyList<T> = [keyof T, keyof T[keyof T]];
like image 41
Tao Avatar answered Nov 13 '22 19:11

Tao


A first approximation of how to do this is in Tao's answer, the problem is that if you add more properties it does not work as expected:

type Foo = {
    outer: {
        inner: any;
    }
    outer2: {
        inner2: any;
    }
}

type PropertyList<T, K1 extends keyof T = keyof T, K2 extends keyof T[K1] = keyof T[K1]> = [K1, K2];

let myList:PropertyList<Foo> = ["outer", "inner"] // error, since K2 will have to be a property of both outer and outer2

You can use function to help with inferring the corect type based on the actual parameters passed. We need to use a two function approach because we need to have generic parameters for T, K1 and K2, but we only want to specify T and if we specify one we must specify all parameters:

function pathFor<T>() {
    return  function <K1 extends keyof T, K2 extends keyof T[K1]>(outer: K1, inner: K2): [K1,K2]{
        return  [outer, inner];
    }
}
// Usage 
let myList = pathFor<Foo>()("outer", "inner"); // typed as ["outer, "inner"]
let myList2 = pathFor<Foo>()("outer2", "inner"); // error, inner is not part of outer2
let myList3 = pathFor<Foo>()("outer2", "inner2"); // typed as ["outer2, "inner2"]

Edit

You can also expand the function to take paths up to a finite length (4 in the example, but add more as needed):

function keysFor<T>() {
    function keys<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(outer: K1, inner: K2, innerInner: K3, innerInnerInner: K4): [K1,K2, K3, K4]
    function keys<K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(outer: K1, inner: K2, innerInner: K3): [K1,K2, K3]
    function keys<K1 extends keyof T, K2 extends keyof T[K1]>(outer: K1, inner: K2): [K1,K2]
    function keys(): string[]{
        return [...arguments];
    }
    return keys
}
like image 166
Titian Cernicova-Dragomir Avatar answered Nov 13 '22 19:11

Titian Cernicova-Dragomir