Skip to content

(Indirect) inference of array element type in callback function type does not work #40743

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

Closed
danielrentz opened this issue Sep 24, 2020 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@danielrentz
Copy link

TypeScript Version: 4.0.2

Search Terms: array element type inference callback

Code

Found a problem with type inference when tried to implement functions and array methods taking callbacks, where the array element type will be inferred from the actual array type.

What I have:

  • a helper type ElemT that extracts the element type from an array type.
  • a function type FuncT to be used in array iteration functions.
  • a global function taking an array and a callback.
  • a new array class with a method taking a callback.

The callback wants to pass the array with its actual type as parameter. Therefore, FuncT needs the array type as generic parameter, and must infer the element type for its first parameter.

// helper type to infer element type from array type
type ElemT<AT extends any[]> = AT extends (infer T)[] ? T : never;

// callback function taking element type and array type
type FuncT<AT extends any[]> = (v: ElemT<AT>, a: AT) => void;

// global function
function example<AT extends any[]>(arr: AT, cb: FuncT<AT>) {
    cb(arr[0], arr); // <==== works
}

// class method
class MyArr<T> extends Array<T> {
    public example(cb: FuncT<this>) {
        cb(this[0], this); // <==== fails
    }
}

However, I fond a workaround: When I bake the ElemT into the FuncT it works as expected. But that's a little inconvenient because this has to be done for multiple callback types in my real code.

// callback function taking element type and array type
type FuncT<AT extends any[]> = AT extends (infer T)[] ? (v: T, a: AT) => void : never;

// global function
function example<AT extends any[]>(arr: AT, cb: FuncT<AT>) {
    cb(arr[0], arr); // <==== works
}

// class method
class MyArr<T> extends Array<T> {
    public example(cb: FuncT<this>) {
        cb(this[0], this); // <==== works now
    }
}

Expected behavior:
In MyArr#example, ElemType<this> resolves to T.

Actual behavior:
In MyArr#example, this[0] (with type T) cannot be assigned to ElemT<this>.

Playground Link:
https://www.typescriptlang.org/play?#code/PTAEAsFMBsAdIE6gC4E94oPagJYDsAzRUGSAW0j2RXUlAIUzNAEMEEXUb4AoNDAKLRyAFQA8AQREkAHskoATAM6s8qANoBdAHygAvKCmz5eZaAAU+IkhEBKLaAD8oaQC5QeSADdEAbh48IKAAxizQ0ABGLMEA1vQArnjByDiYeCgsMfgA5iTCFFTcdCymrOycRXy0oABiicHiRpByiiolGjr6Fl7uQqKSItoANKzuUrb6ul6YOAr+gWDZ0JhR0AlJKWk8BPWb6c0sZLDCA8atqh3a5mwIYyIjwRHudUmNgxMA3jygPyER1+x1AAGTQjG62XygIJiPSwgwAd0wCBiSh4AF8AkFgtAWEoVBRkOBMAoeNjcSoALKoCTsMSDM6mFQ0jioOm6L6-UCweIRaA4YKyQ7HSDmR7PeriQk4JTaT7fTm-R7mKVKYGglDgaUQqFgGFw+gsHDQVGcjEYoA

@andrewbranch
Copy link
Member

It’s incredibly difficult to understand/explain why, but this is basically working as intended. In fact, we believe that your workaround example maybe shouldn’t work if we wanted to be totally safe. The TL;DR is I think your code is safe in practice, but only because of constraints that the compiler can’t/doesn’t track. The more abstract patterns the compiler sees here are not safe in general. The proper solution is to replace ElemT<AT> with AT[number]—you don’t need a conditional type to get the element type of an array type.

The longer, more complicated answer, is that the this type is actually a type parameter (because it gets instantiated with different types in different subclasses), which means that the conditional type ElemT<this> gets deferred—it doesn’t want to give you T right away, because a subclass will have a different this and therefore a different element type. Now, you know that if you subclass MyArr<T>, the element type of that subclass will necessarily be assignable to T (the element type of MyArr<T>), because T is covariant in MyArr<T>. But I’m not sure the compiler knows that about all possible instantiations of this. Even if it did know that, it would also have to know that all possible instantiations of this would always pass inference in AT extends (infer T)[] and take the true branch of that conditional. This is another fact that appears clear to you as a human, since you are not able to remove the array-ness of a subclass of Array, but this is another fact that may not be encoded into the compiler (it seems like it would have to be a special case, and/or would be a lot of work to determine). The final gotcha is that ElemT<AT> is a distributive conditional type, which means it can have non-linear assignability if AT is a union type. Unresolved distributive conditional types have no assignability relationships. (Actually, I believe the current state is that any unresolved conditional type has no assignability relationships—see #30639 and #27932.) Yet again, it appears clear that a subclass cannot have a this type that’s a union, but I don’t think the checker rules that out to create a special exception here—it just sees a distributive conditional type and says “nope.”

@andrewbranch andrewbranch added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 25, 2020
@danielrentz
Copy link
Author

Thank you very much for this comprehensive explanation. Indeed, I forgot about the T[number] syntax, I will give that a try in my code.

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants