I would like to have autocompletion for a function that takes a key of an object as a parameter and determines the type of the second parameter based on the key. If I only use a single interface as the source of the key, everything works fine:
interface A {
X: {params: {a: string; b: number}};
Y: {params: {c: number; d: number}};
}
const foo = <T extends keyof A>(k: T, params: A[T]["params"]) => {
console.log(k, params);
};
foo("X", {a: "a", b: 1}); // works
foo("Y", {c: 1, d: "a"}); // correct error: Type 'string' is not assignable to type 'number'.
However, as soon as I add a second interface as a possible source, TypeScript tells me that I can no longer use the generic type as an index:
interface A {
X: {params: {a: string; b: number}};
Y: {params: {c: number; d: number}};
}
interface B {
Z: {params: {e: string; f: string}};
}
const foo = <T extends keyof A | keyof B>(k: T, params: A[T]["params"] | B[T]["params"]) => {
console.log(k, params);
};
// errors:
// * Type 'T' cannot be used to index type 'A'.
// * Type '"params"' cannot be used to index type 'B[T]'.
How do I have to type params
of the foo
function to make it work?
UPDATE
For the example above, it would be sufficient to combine A & B
into one interface (thanks @jcalz).
But for my use case this is not enough. I have a new example here, which hopefully shows more clearly what I want.
interface A {
X: {params: {a: string; b: number}};
Y: {params: {c: number; d: number}};
}
interface Props {
[key: string]: {params: any};
}
type AdditionalProps = Props & {[K in keyof A]: never};
interface B extends AdditionalProps {
Z: {params: {e: string; f: string}};
}
class C<AP extends AdditionalProps> {
// error: Type 'T' cannot be used to index type 'A'.
foo = <T extends keyof A | keyof AP>(k: T, params: A[T]["params"] | AP[T]["params"]) => {
console.log(k, params);
};
}
const c = new C<B>();
// works - without autocompletion for parameter `k`, but for `params` if `k` is a key of interface `A`
c.foo("X", { a: "", b: 1 })
// No autocomplete for `k`. If `"Z"` is entered for `k`, `params` has type `never`
c.foo("Z", )
SOLUTION
Here is the slightly modified solution from @Alexander Nenashev with working auto-completion.
interface A {
X: {params: {a: string; b: number}};
Y: {params: {c: number; d: number}};
}
type Props<T> = {
[key in keyof T]: {params: any};
};
type AdditionalProps<T extends object = object> = Props<T> & {[K in keyof A]: never};
interface B extends AdditionalProps {
Z: {params: {e: string; f: string}};
}
type ByKey<K, A, B> = K extends keyof A ? A[K] : K extends keyof B ? B[K] : never;
class C<AP extends AdditionalProps<AP>> {
foo = <T extends keyof A | keyof AP>(k: T, params: ByKey<T, A, B>["params"]) => {
console.log(k, params);
};
}
const c = new C<B>();
c.foo("X", {a: "", b: 1});
c.foo("Y", {c: 1, d: 2});
c.foo("Z", {e: "1", f: "2"});
c.foo("D", {e: "1", f: "2"}); // correct error: Argument of type '"D"' is not assignable to parameter of type '"X" | "Y" | "Z"'
Given that you could use or couldn't the same keys you could utilize some conditional typing:
interface A {
X: {params: {a: string; b: number}};
Y: {params: {c: number; d: number}};
}
interface B {
X: {params: {a: number; b: number}};
Z: {params: {e: string; f: string}};
}
type ChooseByKey<K extends any> = K extends keyof A ? K extends keyof B ? (A|B)[K] : A[K] : K extends keyof B ? B[K]: never
const foo = <K extends keyof A | keyof B>(k: K, params: ChooseByKey<K>['params']) => {
console.log(k, params);
};
foo('X', {a: '1', b: 1});
foo('X', {a: 1, b: 1});
foo('Y', {c: 1, d: 1});
foo('Y', {c: 1, d: 's'}); // error