David Gomes
David Gomes

Reputation: 5825

Type that depends on generic argument as value argument to function

I have a generic type Group that looks like this:

// K -> Key
// I -> Input
type Group<K, I> = {
    key: K;
    func: (i: I) => void;
};

There is a fixed number of Group values which I declared in an object like this:

const GROUPS = {
    "a": {
        func: (i: {x: number}) => { console.log(i); },
        key: "a",
    },
    "b": {
        func: (i: { y: number }) => { console.log(i) },
        key: "b"
    }
} as const;

I then have 2 utility types to refer to all the possible group keys and all the possible group inputs:

type GroupKey = keyof typeof GROUPS;
type GroupInput<K extends GroupKey> = Parameters<typeof GROUPS[K]["func"]>[0];

// GroupValue test:
type TestType = GroupInput<"b">; // { y: number}, this works

Finally, I have a function that receives both a group key and a group input:

function test<K extends GroupKey>(key: K, input: GroupInput<K>) {
    if (key === "b") {
        (input.y); // Why doesn't TypeScript understand that `input.y` must be `number` here?
    }
}

This function is generic over the type of the key that is passed in and unfortunately, TypeScript cannot "understand" that if key is "b", then input is of type { y: number }. Why is this the case, what is TypeScript missing to be able to do this? I'd especially like to find a GitHub issue on this (so that I can subscribe to it), but I wasn't able to find one as this type of thing is particularly hard to search for.

Full Playground URL

Upvotes: 3

Views: 185

Answers (2)

artur grzesiak
artur grzesiak

Reputation: 20348

Please consider this snippet:

const key = 'a' as GroupKey
const input = { y: 1 } // optionally cast as GroupInput<'b'> or as GroupInput<GroupKey>

test(key, input) // compiles, but not intended

The input might be independent from the key. There are no guarantees input.y must be a number when test is called with value 'b' as the first argument. type TestType = GroupInput<"b"> uses literal type ('b') which allows Typescript to restrict 'a' | 'b' to just 'b'. The same applies for test('b', ...), but passing key of type 'a' | 'b' allows to pass input of type GroupInput<'a' | 'b'>.

One option would be to check if 'y' in input, but this still does not address the main issue of not allowing wrong arguments to test. In general case input as GroupInput<'b'> is unsafe and should be avoided at any cost.

A possible fix:

type Params = { [K in GroupKey]: [key: K, input: GroupInput<K>] } // key: and input: are used for auto-completion instead of generic arg_0, arg_1

function test2(...args: Params[GroupKey]) {
    // const [key, input] = args // will not work
    if (args[0] === "b") {
        const input = args[1];
        input.y; // number
    }
}

test2('b', { y: 1 }) // ok
test2('b', { x: 1 }) // error
test2(key, input) // error

Playground

Upvotes: 2

Robert Kossendey
Robert Kossendey

Reputation: 6998

Because the input parameter is independent from your key parameter. input is not required to have y as a property although your key might be equal to 'b'. You would have to Typecast your input:

 function test<K extends GroupKey>(key: K, input: GroupInput<K>) {
    if (key === "b") {
        const typedInput = input as GroupInput<'b'>;
        (typedInput.y)
    }
}

Upvotes: 0

Related Questions