Skip to content

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

Closed

Description

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)

Activity

  1. RyanCavanaugh commented on Aug 17, 2018

    @RyanCavanaugh
    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");
  2. ahejlsberg commented on Aug 17, 2018

    @ahejlsberg
    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.

  3. added
    QuestionAn issue which isn't directly actionable in code
    and removed
    BugA bug in TypeScript
    on Aug 17, 2018
  4. InExtremaRes commented on Aug 18, 2018

    @InExtremaRes
    Author

    Thank you very much for the answers.

    Anders Hejlsberg (@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.

  5. aleclarson commented on Sep 13, 2018

    @aleclarson

    I'm also curious why & string is necessary.

  6. locked as resolved and limited conversation to collaborators on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions