microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.76k stars 12.46k forks source link

checkJs can't correctly determine type of generic function parameter #31243

Open ghost opened 5 years ago

ghost commented 5 years ago

TypeScript Version: 3.4.5

jsdoc checkJs template function parameter

/**
 * @template T
 * @param {(left: T, right: T) => boolean} equals
 * @returns {(xs: T[]) => T[]}
 */
const uniq = equals => xs => {
    /** @type {T[]} */
    const seen = []
    const out = []
    const len = xs.length
    let j = 0
    for(let i = 0; i < len; i++) {
        const item = xs[i]      
        if(!seen.some(s => equals(s, item))) {          
            seen.push(item)
            out[j++] = item
       }
    }

    return out;
}

uniq((x, y) => x == y)([1, 3, 3, 7])

Expected behavior: Typechecks fine

Actual behavior: Type 'number' is not assignable to type 'T

Playground Link: https://www.typescriptlang.org/play/#src=%2F**%0D%0A%20*%20%40template%20T%0D%0A%20*%20%40param%20%7B(left%3A%20T%2C%20right%3A%20T)%20%3D%3E%20boolean%7D%20equals%0D%0A%20*%20%40returns%20%7B(xs%3A%20T%5B%5D)%20%3D%3E%20T%5B%5D%7D%0D%0A%20*%2F%0D%0Aconst%20uniq%20%3D%20equals%20%3D%3E%20xs%20%3D%3E%20%7B%0D%0A%09%2F**%20%40type%20%7BT%5B%5D%7D%20*%2F%0D%0A%09const%20seen%20%3D%20%5B%5D%0D%0A%09const%20out%20%3D%20%5B%5D%0D%0A%09const%20len%20%3D%20xs.length%0D%0A%09let%20j%20%3D%200%0D%0A%09for(let%20i%20%3D%200%3B%20i%20%3C%20len%3B%20i%2B%2B)%20%7B%0D%0A%09%09const%20item%20%3D%20xs%5Bi%5D%09%09%0D%0A%09%09if(!seen.some(s%20%3D%3E%20equals(s%2C%20item)))%20%7B%09%09%09%0D%0A%09%09%09seen.push(item)%0D%0A%09%09%09out%5Bj%2B%2B%5D%20%3D%20item%0D%0A%09%20%20%20%7D%0D%0A%09%7D%0D%0A%0D%0A%09return%20out%3B%0D%0A%7D%0D%0A%0D%0Auniq((x%2C%20y)%20%3D%3E%20x%20%3D%3D%20y)(%5B1%2C%203%2C%203%2C%207%5D)

Related Issues: This seems related, but I don't think it's it

https://github.com/Microsoft/TypeScript/issues/26883

weswigham commented 5 years ago

This isn't really specific to JS, even in TS in master,

declare function uniq<T>(p: (left: T, right: T) => boolean): (xs: T[]) => T[];

uniq((x, y) => x == y)([1, 3, 3, 7]);

shows uniq's signature as function uniq<unknown>(p: (left: unknown, right: unknown) => boolean): (xs: unknown[]) => unknown[].

@ahejlsberg was an invocation like this supposed to be covered in the recent higher order function assignability stuff? It certainly looks similar to many of those.

ghost commented 5 years ago

@weswigham am I right then in thinking there's actually no way to type this function in actual typescript either?

My attempt was something like this, but T wasn't available across all the parameters.

const uniq = (equals: <T>(left: T, right: T) => boolean) => (xs: T[]): T[] => { 
    const seen: T[] = []
    const out = []
    const len = xs.length
    let j = 0
    for(let i = 0; i < len; i++) {
        const item = xs[i]      
        if(!seen.some(s => equals(s, item))) {          
            seen.push(item)
            out[j++] = item
       }
    }

    return out;
}
weswigham commented 5 years ago
const uniq = <T>(equals: (left: T, right: T) => boolean) => (xs: T[]): T[] => { 
    const seen: T[] = []
    const out = []
    const len = xs.length
    let j = 0
    for(let i = 0; i < len; i++) {
        const item = xs[i]      
        if(!seen.some(s => equals(s, item))) {          
            seen.push(item)
            out[j++] = item
       }
    }

    return out;
}

you should just be able to attach T to the outermost function. But as I said, we still don't infer the right types on usage.

ghost commented 5 years ago

Thanks for the clarification, I wrongly assumed it was a checkJs issue. I'll just use @ts-ignore for now.

ahejlsberg commented 5 years ago

A simpler example that illustrates the issue:

declare function foo<T>(f: (x: T) => void): (x: T) => void;

let func: (x: { foo: 'hello' }) => void = foo(x => x.foo);  // Ok

foo(x => x.foo)({ foo: 'hello' });  // Error

In the assignment to func we infer from the type of func to the return type of foo which in turn gives us a contextual type for x. However, when the function is immediately invoked, we make no inferences from the arguments of the rightmost call, but it is potentially feasible. I'll mark this issue as a suggestion to do so.