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

Argument of type 'T[T[K] extends T[] ? K : never]' is not assignable to parameter of type 'T[]'. #30728

Closed
blixt opened this issue Apr 3, 2019 · 2 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@blixt
Copy link

blixt commented Apr 3, 2019

TypeScript Version: 3.4.0-dev.20190403

Code

function toHTML<T, K extends keyof T>(items: T[], key: T[K] extends T[] ? K : never) {
    if (!items.length) return '';
    // i[key] here is an error :(
    const innerHTML = items.map(i => `<li>${i}${toHTML(i[key], key)}</li>`).join('\n');
    return `<ul>\n${innerHTML}\n</ul>`;
}

class Item {
    constructor(public name: string, public nodes: Item[]) { }
    toString() { return this.name; }
}

const tree = [
    new Item("Root", [
        new Item("Node 1", [new Item("Subnode 1", [])]),
        new Item("Node 2", [])
    ])
];

// This works correctly! Anything but 'nodes' (eg. 'name') is an error.
document.write(toHTML(tree, 'nodes'));

Idea: Take a list of type T and a key K into type T that returns a T[] and recursively walk the entire tree.

Expected behavior:

If I specify that a keyof T must extend T[] (or it becomes never), I can use it as such.

Actual behavior:

It partially works, but indexing into the T type with K shows the error:

test.ts:4:56 - error TS2345: Argument of type 'T[T[K] extends T[] ? K : never]' is not assignable to parameter of type 'T[]'.

4     const innerHTML = items.map(i => `<li>${i}${toHTML(i[key], key)}</li>`).join('\n');
                                                         ~~~~~~

Playground Link: http://www.typescriptlang.org/play/#src=function%20mapTree%3CT%2C%20K%20extends%20keyof%20T%2C%20R%3E(nodes%3A%20T%5B%5D%2C%20childrenKey%3A%20T%5BK%5D%20extends%20(T%5B%5D%20%7C%20undefined)%20%3F%20K%20%3A%20never%2C%20fn%3A%20(item%3A%20T)%20%3D%3E%20R)%3A%20R%5B%5D%20%7B%0D%0A%20%20%20%20const%20result%3A%20R%5B%5D%20%3D%20%5B%5D%0D%0A%20%20%20%20for%20(const%20node%20of%20nodes)%20%7B%0D%0A%20%20%20%20%20%20%20%20fn(node)%0D%0A%20%20%20%20%20%20%20%20const%20children%20%3D%20node%5BchildrenKey%5D%0D%0A%20%20%20%20%20%20%20%20if%20(children)%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20%2F%2F%20This%20does%20not%20work%20even%20if%20we%20validate%20that%20T%5BK%5D%20is%20always%20T%5B%5D.%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20result.push(...mapTree(children%2C%20childrenKey%2C%20fn))%0D%0A%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%7D%0D%0A%20%20%20%20return%20result%0D%0A%7D%0D%0A%0D%0Ainterface%20MenuItem%20%7B%0D%0A%20%20%20%20id%3A%20number%0D%0A%20%20%20%20label%3A%20string%0D%0A%20%20%20%20submenu%3F%3A%20MenuItem%5B%5D%0D%0A%7D%0D%0A%0D%0Aconst%20menu%3A%20MenuItem%5B%5D%20%3D%20%5B%5D%0D%0A%2F%2F%20This%20works%3A%20changing%20submenu%20to%20something%20else%20is%20an%20error.%0D%0Aconst%20allLabels%20%3D%20mapTree(menu%2C%20%22submenu%22%2C%20item%20%3D%3E%20item.label)%0D%0A

@jcalz
Copy link
Contributor

jcalz commented Apr 3, 2019

I think there's only so much we can expect the compiler to understand about manipulating conditional types that depend on generic type parameters. Maybe the compiler could be made to specifically check that T[T[K] extends U ? K : never] is assignable to U. But it would cost something in terms of type checker complexity and compile time, and any benefit would only be seen by some fraction of users who specifically do this kind of thing. It could be worth it, but I wouldn't hold my breath.

Meanwhile, there are two general ways for you to deal with this. One: a judicious use of a type assertion to tell the compiler that it is not as clever as you:

type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
function toHTML<T, K extends KeysMatching<T, T[]>>(items: T[], key: K) {
    if (!items.length) return '';
    const innerHTML = items.map(i => `<li>${i}${toHTML(
      i[key] as any as T[], // I'm smarter than you, compiler! 🤓
    key)}</li>`).join('\n');
    return `<ul>\n${innerHTML}\n</ul>`;
}

Two: walk the compiler through the type safety of the situation by giving it some generic types that it does properly check:

type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]
// constrain T to be a subtype Record<K, T[]>, so T[K] will be recognized as T[] by the compiler
function toHTML<T extends Record<K, T[]>, K extends KeysMatching<T, T[]>>(items: T[], key: K) {
    if (!items.length) return '';
    const innerHTML = items.map(
        i => `<li>${i}${toHTML(i[key], key)}</li>`
    ).join('\n');
    return `<ul>\n${innerHTML}\n</ul>`;
}

Note that in both of the above cases, I changed the K type parameter to be of type KeysMatching<T, T[]> instead of just keyof T, since that's the constraint you actually want to impose. This is probably more useful to callers of toHTML() who would like some auto-completion in their IDEs (i.e., you want to see just "nodes" as a suggested value for key, and not "nodes" | "name" and then give you an error when you enter "name".)

Hope this helps you. Good luck!

@blixt
Copy link
Author

blixt commented Apr 3, 2019

Hey this is very nice! I had started doing something like this but it didn't work out for me (types can get a bit finicky, like when filtering out nevers etc). Thank you! The original issue could still be fixed, but you're right it would put more work on the type checker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants