Drarig29
Drarig29

Reputation: 2245

Get type's keys based on the type of one of its values

I have a global type definition which is an object with keys (what I call a prop) that the user has to pass as parameter to myFunction(), and values which are used only by my code to do type checking.

Values can be of only 3 "possible types" (contained in Possible). Each possible type has a different type definition.

I would like myFunction() to accept as parameter only props whose value's test property is "possible1".

The generic type OnlyPossible1Props<T> shows the way I would implement this, but it doesn't work.

First, an error is shown:

Type 'prop' cannot be used to index type 'T'. (2536)

And in addition, it returns string | number instead of "abc" | "def" in myFunction().

The closest thing I've been able to do is the piece of code I commented out. However, it loses the information of the key along the way because I don't keep it somewhere.

For an easiest way to debug, here's a link to the TypeScript Playground.

type Possible = "possible1" | "possible2" | "possible3";

type ValueBase<T extends Possible, U, V> = {
    test: T,
    other1: U,
    other2: V
};

type Value1<T> = ValueBase<"possible1", T, T>;
type Value2<T> = ValueBase<"possible2", T, [T, T]>;
type Value3<T> = ValueBase<"possible3", T, T[]>;

type Definition = {
    [prop: string]: Value1<any> | Value2<any> | Value3<any>
};

interface Example extends Definition {
    "abc": {
        test: "possible1",
        other1: string
        other2: string
    },
    "def": {
        test: "possible1",
        other1: string
        other2: string
    },
    "ghi": {
        test: "possible2",
        other1: string
        other2: [string, string]
    },
    "jkl": {
        test: "possible3",
        other1: string
        other2: string[]
    }
}

// type OnlyPossible1Props<T> = T extends object ?
//     T[keyof T] extends infer A ?
//     A extends { test: "possible1" }
//     ? A : never : never : never;

type OnlyPossible1Props<T> = T extends object ?
    keyof T extends infer prop ?
    T[prop] extends infer A ?
    A extends { test: "possible1" }
    ? prop : never : never : never : never;

function myFunction<Prop extends OnlyPossible1Props<Example>>(prop: Prop) { }

Upvotes: 0

Views: 31

Answers (1)

LeoBH
LeoBH

Reputation: 71

The following works, but only if you remove extends Definition from Example:

type OnlyPossible1Props<T> = {
    [K in
        { [J in keyof T]: T[J] extends { test: "possible1" } ? J : never }[keyof T]
    ]?: T[K]
};

This results in:

type ExampleOnlyPossible1Props = OnlyPossible1Props<Example>;

// equivalent to:

type ExampleOnlyPossible1Props = {
    abc?: {
        test: "possible1";
        other1: string;
        other2: string;
    };
    def?: {
        test: "possible1";
        other1: string;
        other2: string;
    };
}

(You may want to skip the optional modifier ?).

This gives us:

myFunction({ "abc": { test: "possible1", other1: "foo", other2: "bar" } }) // OK
myFunction({ "def": { test: "possible2", other1: ["a", "b"], other2: "bcd" } }) // Error

You can see that in this TypeScript Playground.

The reason this does not work if Example extends Definition is that the index signature on Definition means keyof Example evaluates to string | number, rather than the actual keys of Example. If extends Definition is removed, we get keyof Example as "abc" | "def" | "ghi" | "jkl".

If you want myFunction to accept one of the possible1 props from Example, you can instead use:

type OnlyPossible1Props<T> = {
    [J in keyof T]: T[J] extends { test: "possible1" } ? T[J] : never
}[keyof T];

This gives you a union type that you can see that in this TypeScript Playground:

myFunction({ test: "possible1", other1: "foo", other2: "bar" }) // OK
myFunction({ test: "possible2", other1: ["a", "b"], other2: "bcd" }) // Error

Upvotes: 1

Related Questions