Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameter of type 'K extends FunctionPropertyNames<T>' is always a union type and not restricted to the string literal passed to function #25215

Closed
InExtremaRes opened this issue Jun 26, 2018 · 4 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@InExtremaRes
Copy link

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)

@mhegazy mhegazy added the Bug A bug in TypeScript label Jun 26, 2018
@RyanCavanaugh
Copy link
Member

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");

@ahejlsberg
Copy link
Member

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.

@ahejlsberg ahejlsberg added Question An issue which isn't directly actionable in code and removed Bug A bug in TypeScript labels Aug 17, 2018
@ahejlsberg ahejlsberg removed their assignment Aug 17, 2018
@ahejlsberg ahejlsberg removed this from the TypeScript 3.1 milestone Aug 17, 2018
@InExtremaRes
Copy link
Author

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.

@aleclarson
Copy link

I'm also curious why & string is necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants