TypeScript Version: 2.8.3 and 2.9.1 (I think. The actual version in playground)
Code
The next code works as expected.
interface I {
property: number;
method1(): void;
method2(a: string): number;
}
declare const ob: I;
declare function foo<T, K extends keyof T>(ob: T, key: K): T[K];
const f1 = foo(ob, 'property'); // number
const f2 = foo(ob, 'method1'); // () => void
const f3 = foo(ob, 'method2'); // (a: string) => number
// f1(); OK, not a function
f2();
f3('a string');
But I want to restrict the key parameter in foo to just functions.
Since #21316, and as an example on that PR, we can use something like FunctionPropertyNames.
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
declare function bar<T, K extends FunctionPropertyNames<T>>(ob: T, key: K): T[K];
// const f4 = bar(ob, 'property'); OK, 'property' is not allowed
const f5 = bar(ob, 'method1'); // () => void | (a: string) => number
const f6 = bar(ob, 'method2'); // () => void | (a: string) => number
// ERROR! Both are of type '() => void | (a: string) => number'
f5();
f6('a string');
// Type assertion
const f7 = bar(ob, 'method1' as 'method1'); // () => void
const f8 = bar(ob, 'method2' as 'method2'); // (a: string) => number
// OK again
f7();
f8('a string');
Expected behavior:
f5 and f6 inferred to () => void and (a: string) => number, respectively, without need a type assertion (pretty much as the first snippet).
Actual behavior:
f5 and f6 are both inferred to () => void | (a: string) => number, which is very odd since I'm passing a literal string.
Playground link
Playground link
Related Issues:
Maybe #24080 (but that seems related to the keyof T ~ string | number | symbol issue and my example fails with TS 2.8)
Two possible workarounds and you can pick your favorite
interface I {
property1: string;
method1(): void;
method2(s: string): number;
}
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
function getProperty<T, K extends keyof T>(obj: T, propName: K & FunctionPropertyNames<T>): T[K] {
return obj[propName];
}
declare const i: I;
const m1 = getProperty(i, "method1");
const m2 = getProperty(i, "method2");
const p1 = getProperty(i, "property1");
or
interface I {
property1: string;
method1(): void;
method2(s: string): number;
}
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
function getProperty<T, K extends string & FunctionPropertyNames<T>>(obj: T, propName: K): T[K] {
return obj[propName];
}
declare const i: I;
const m1 = getProperty(i, "method1");
const m2 = getProperty(i, "method2");
const p1 = getProperty(i, "property1");
Another workaround is to simply change FunctionPropertyNames<T> to
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T] & string;
The intersection with string makes it clear to the compiler that the type is a subtype of string which in turn makes it clear that you want to preserve literal types during type inference.
Thank you very much for the answers.
@ahejlsberg Could you explain why the checker doesn't do that without the & string? I am not an expert on this matter but I can't figure out why.
I'm also curious why & string is necessary.
Most helpful comment
Another workaround is to simply change
FunctionPropertyNames<T>toThe intersection with
stringmakes it clear to the compiler that the type is a subtype ofstringwhich in turn makes it clear that you want to preserve literal types during type inference.