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

Allow narrowing generic type to single element of union type #17713

Open
dlants opened this issue Aug 9, 2017 · 6 comments
Open

Allow narrowing generic type to single element of union type #17713

dlants opened this issue Aug 9, 2017 · 6 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@dlants
Copy link

dlants commented Aug 9, 2017

It is a fairly common pattern to create a set of generic components, and then to use the generic component type within the system. Currently, typescript has an issue handling cases where some values of a generic component depend on others.

TypeScript Version: 2.4.0 / nightly (2.5.0-dev.201xxxxx)
2.4
Code

let index = {
    a: {
        arg: 'a',
        fn: (a: string) => {a.toUpperCase()}
    },
    b: {
        arg: 1,
        fn: (b: number) => {b + 1}
    }
}

let runFn= <K extends keyof (typeof index)>(key: K) => {
    let i = index[key]
    let arg = i.arg
    let fn = i.fn
    fn(arg) // Error: compiler doesn't understand that fn and arg will match
}

Expected behavior:
No error, since we know that arg and fn are pulled off of the same map entry.

Actual behavior:
Error: cannot invoke expression whose type lacks a call signature

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Aug 22, 2017
@RyanCavanaugh
Copy link
Member

Very difficult to see how this could be enforced. You could equally have written this

let runFn= <K extends keyof (typeof index)>(key1: K, key2: K) => {
    let i = index[key1]
    let arg = i.arg
    let fn = index[key2].fn
    fn(arg) // Error: compiler doesn't understand that fn and arg will match
}
runFn<'a' | 'b'>('a', 'b');

which is indistinguishable from a type perspective (all expressions used here have the same type as in your example). You could written a different type than typeof index if you needed to express this pattern

@dlants
Copy link
Author

dlants commented Aug 22, 2017

The suggestion is to perhaps introduce a new syntax to mean a single key of that takes a string union type and enforces that the type is bound to a single element of that union, rather than any subset...

@dlants
Copy link
Author

dlants commented Aug 22, 2017

Using in, for example - since that already has a similar use when defining interfaces,

let runFn= <K in keyof (typeof index)>(key1: K, key2: K) => {
    let i = index[key1]
    let arg = i.arg
    let fn = index[key2].fn
    fn(arg) // OK here
}
runFn<'a' | 'b'>('a', 'b'); // Error here - K in keyof matches only 'a' or 'b', but not 'a' | 'b'

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript and removed Design Limitation Constraints of the existing architecture prevent this from being fixed labels Aug 22, 2017
@mattmccutchen
Copy link
Contributor

+1 to the idea of <K in T> constraining K to be a singleton type; this might help with #25879 too. However, even with that, I don't see an easy way for the compiler to determine that arg is assignable to the parameter type of fn. Either (1) it would need to recognize that the constraint of K is a known union of literal types and do case analysis on K, which is unlike any existing logic in the compiler that I'm familiar with and would need care to avoid stack overflows and such, or (2) index would need to have a type that includes an existential (#14466), something like {[i: string]: exists X. {arg: X, fn: (x: X) => X}}, which would probably need to be annotated since I don't see how the compiler could infer it in general.

@mhegazy
Copy link
Contributor

mhegazy commented Jul 25, 2018

This request hinges on #7294. the main problem here is that regardless of how K is defined, the compiler needs to repeat the checking of the call expression once for every possible value of the union to ensure it is valid. The way the compiler is setup today, checking a call expression has side-effects of applying contextual types to type sensitive expressions, and thus a call can only be checked once.

@mattmccutchen
Copy link
Contributor

If we had the declarations:

interface ArgTypes {
    a: string;
    b: number;
}
let index: {[K in ArgTypes]: {arg: ArgTypes[K], fn: (arg: ArgTypes[K]) => ArgTypes[K]}} = {
    a: {
        arg: 'a',
        fn: (a: string) => {a.toUpperCase()}
    },
    b: {
        arg: 1,
        fn: (b: number) => {b + 1}
    }
}
let runFn= <K in keyof (typeof index)>(key1: K, key2: K) => {
    let i = index[key1]
    let arg = i.arg
    let fn = index[key2].fn
    fn(arg) // OK here
}

then we don't need the case analysis on K, we just need the ability to declare it as a singleton and substitute into mapped types. There's a case like that in this Stack Overflow question. I've edited #25879 to refer to the easier suggestion of just singleton type parameters and substitution into mapped types (although the use case currently written on that issue is something else). This issue can continue to refer to the harder suggestion that requires case analysis on K and #7294.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants