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

Valid filter predicates rejected after 5.2.2 upgrade #53489 #55777

Open
craigphicks opened this issue Sep 18, 2023 · 2 comments
Open

Valid filter predicates rejected after 5.2.2 upgrade #53489 #55777

craigphicks opened this issue Sep 18, 2023 · 2 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@craigphicks
Copy link

craigphicks commented Sep 18, 2023

πŸ”Ž Search Terms

Add fallback logic for generating signatures for unions of array members #53489

πŸ•— Version & Regression Information

  • This changed between versions 5.1.6 and 5.2.2

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.2.2#code/CYUwxgNghgTiAEYD2A7AzgF3rGaUC55MYBLFAcwG0BdeAH3hQFcBbAIxBhoG4AoX0JFgIAZkxRgMJVPBYBPAApxgJMFAwgAFADcoEJiELN2nADTwyoAB5HWHGOZxQ5tk12oBKQmyRIIIKBQ+QWg4eDEJKRl5JRAVNQ0dPQNCYjJyc0sQG0Y7M2wYGGdUjFIKGi94Hz8AoP5kdCwYAEZ4AF4C3BQAOhESCA0YTRjlVXUQDz5eAHpp+ABWbubugDZ4ThgkGEJNGT6Bzg8ZuYAVAAsSNHWrAAc4NDRpFAsrlCQsNQhoNn9u4-h4ABRKBgM6yEBueBIETwDBnBDiJ6wuQ3BD-eAAcgA3vAADwAZWuGhQwCuaQoAD5NHc4mMNDtdPpDERSulMiTsq57I5CsUWWUqJ52hT4IyDC94PjzHDLgBBGDkAD8hECckq+J48HRAPg1NGCWZSSZJQF7OsXPyThc-PSFWF8HEAGs3gB3FDSi5oeVKlUoNUm23UbjwAC+9HgOO6UeDIYxWtmOrOUDJJHIKHUTHu5jYTCwbxQCGhsLOSDQCEeaYz9wKCGQLBu6hIPwQLpIcPWILB73hMG6mgATABmAAsAE4jv9Fv3uv31oUtjtoop9eMjgmAHJIKHaTgQJBQYCydSgkBXGVXT4QP4JgEAeR3MD3B-grSL-fMGL1tINDOSzPJ5DhsY3IWByOTAZavLWpoAFAXkMAeHabQimKCCXDaFBwW4HpygqyrYH6lQwaymEMBBCE0Bi5jkFAO7FqIfh7q2mEbFs15zDq8DeqwIAoFgRYYCiCDYrqqEWg4oHmrk2GdHy5EVN4vj+IEwZGikGEZJJnLSSBVoBuUniKTUKmhnG6FvFgyYVumzawluDZFCwICDFCMKCaimJqf+JGAWR8Fmtp5E8kU0GwX5biIUKyGin+EphTpnAYuxnE6icQlXEWDlQE5gxXBiqFxoEh75X+hVhGQdYNlIzbJSlqVCZi8XkWZrzvNgDypjZ-h2ciHkYs1tV1fVfUAS1jBtVZnVQLZGBbu5wkDdq967vuh6zm+H5fvE4y-saGlYSBWTgf5smhT5B2HEhIpOq67rFrhPoEf6upNfBkVUfANF0XCDFfEgzGAaxvbagC3FOXxrm9cJOJeeJAXHTJekJe4lTVMpQSiX++maUdcOndjClVEptQxmNFntdZ03dbN8BZTlnCQ-Nnlift4WHWBeNI8RAoXRRUXXSgzr-SgSUg8Np6Q3TzmcHlBUEcVcvCKBlWNjVYucWlI3nWziUSuTk2VjNc0Nf18GizeQ0AprwmjXrE0dYb1PG31A0DgA7Cs478Ez8ohQAYv0gx++IkhPNbuK+841tErxpLwHAB6oBAcgOoLt00CKHS8KDUHR9kxJx5oZAiAzgInJF8CKtnOpbXShiw0CJzw5zUGEJHcjWx49o3cLOFenhvpqvaZc0FqnFGCAD7wFMAjgKEaF8ZwIgggg7cAOptmca9bI6sBIOIwB+1sAcHDAEcnDHJJXD3boZxG1cAvsgxt1BJ9ByHUQoOHsonBSwa8CGeoqBMDx0HO0XUOA8DtS4lBDecJt4wF3psA+R8YBv04LieaRZIEoApB4XogdODDGXN+VcfAEwgjACAG4GArgjFIRoeW2AIBoC3EmC8Ww4CSHjs5TMzwmYACIAKjx1u4AR-AEyoPgGcVggRaacCYGgPQvAmYXw6O3dBaCP5hyEpgoS2DCh4D-v8E4SkpA3CICWF0aB-iqPAf8TQtcfyYz2gBZuyNgp8mEfzVOQs3R929PhVUXdoreK7v8BgjiaTbXpC49SQUtII10q3ZGV1fG3QCQPJ6ISRTyU8EAA

πŸ’» Code

Passing valid predicate types to Array.filter is an error in version 5.2.2.
The following code produces errors in both version 5.1.6 and 5.2.2,
but the errors are are different and specify different locations.
In 5.1.6 the error is on filter, in 5.2.2 it is on myPredicates.

declare const arrsn: string[] | number[];

declare function myPredicate(value: number, index: number, array: number[]): boolean;
declare function myPredicate(value: string, index: number, array: string[]): boolean;

const r1 = arrsn.filter(myPredicate);

// 5.1.6 error: (on filter)
// This expression is not callable.
//  Each member of the union type 
//  '{ <S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): S[]; 
//     (predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): string[]; } | { ...; }' 
//   has signatures, but none of those signatures are compatible with each other.(2349)

// 5.2.2 error: (on myPredicate)
// No overload matches this call.
//   Overload 1 of 2, '(predicate: (value: string | number, index: number, array: (string | number)[]) => value is string | number, thisArg?: any): (string | number)[]', gave the following error.
//     Argument of type '{ (value: number, index: number, array: number[]): boolean; (value: string, index: number, array: string[]): boolean; }' is not assignable to parameter of type '(value: string | number, index: number, array: (string | number)[]) => value is string | number'.
//       Types of parameters 'value' and 'value' are incompatible.
//         Type 'string | number' is not assignable to type 'number'.
//           Type 'string' is not assignable to type 'number'.
//   Overload 2 of 2, '(predicate: (value: string | number, index: number, array: (string | number)[]) => unknown, thisArg?: any): (string | number)[]', gave the following error.
//     Argument of type '{ (value: number, index: number, array: number[]): boolean; (value: string, index: number, array: string[]): boolean; }' is not assignable to parameter of type '(value: string | number, index: number, array: (string | number)[]) => unknown'.
//       Types of parameters 'value' and 'value' are incompatible.
//         Type 'string | number' is not assignable to type 'number'.
//           Type 'string' is not assignable to type 'number'.(2769)

πŸ™ Actual behavior

Error, as shown above.

Also, the return type (string|number)[] is not correct - it should be string[]|number[]. I understand that the overly widened return type was thought to be better than nothing, However, since the new behavior in 5.2.2. is still buggy, perhaps the final fix could could also return the correct type.

πŸ™‚ Expected behavior

Emit no error.

Additional information about the issue

Note that using a template function does work with current 5.2.2 version:

declare function myPredicateTpl<T>(value: T, index: number, array: T[]): boolean;
const r2 = arrsn.filter(myPredicateTpl); // no error in 5.2.2., return type is (string|number)[]

However, the behavior described by passing the overloaded myPredicate template function as not the same is not the same as the behavior described by passing myPredicateTpl . Only the former describes the scenario such that the same type is used for all index values and that the array has single valued element type.

We can actually generate a working version of this using the following distributed template function:

type ArrayFilterFunctionType<ArrayType extends readonly unknown[]> = 
  ArrayType extends (infer ET)[] ?
    (predicate:(value: ET, index: number, array: ArrayType) => unknown, thisArg?: any) => ET[] 
    : never ;

declare interface ArrayWithWorkaroundForFilter<AT extends unknown[]> {
    filter: ArrayFilterFunctionType<AT>; 
}

const r3 = (arrsn as ArrayWithWorkaroundForFilter<typeof arrsn>).filter(myPredicate);
// accepts myPredicate and also has correct return type "string[] | number[]"

It also actually returns the correct type string[] | number[].

Here we can see it describes a union of signature types:

// For human perusal
type T = ArrayFilterFunctionType<typeof arrsn>;
// Tooltip shows
// type T = 
// ((predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any) => string[]) 
// | ((predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any) => number[])

While currently TypeScript has the unfortunate limitation of only being able to return string[] | number[],
it may at some point be improved to overcome that limitation and return the accurate type. For that reason
it is better to encourage habitual use of a predicate description which actually describes the behavior of the predicate.

I guess putting a distributed template type like ArrayFilterFunctionType in a TypeScript lib file would be probably be too computationally expensive. However, if the same thing could be achieved with oneof -like notation and implementation, then it could become cheap enough.

Finally, note that adding a catch all function with return type never to the overaloads will get rid of the error:

declare function myPredicate(value: number, index: number, array: number[]): boolean;
declare function myPredicate(value: string, index: number, array: string[]): boolean;
declare function myPredicate(value: any, index: number, array: any[]): never;
const r1 = arrsn.filter(myPredicate); // no error in 5.2.2, return type (string|number)[]

However, it returns (string|number)[], not the correct return type string[] | number[].

@craigphicks
Copy link
Author

craigphicks commented Sep 18, 2023

@ericbf

The predicate doesn’t look to be valid for the type of the array?

If the array is (A[] | B[]), in the context of the array.filter predicate, each item is A | B, so the predicate that forces (item: A) breaks that expectation and is not compatible.

The way I see it, that value predicate should fail. There is no guarantee from a type system perspective that item is an A, so a predicate of (item: A...) isn’t compatible.

It is predicates in the plural. So it will be either element A with array A[], or element B with array B[]. No mixing.
That's what the overload means.

It's up to the user implementation to implement that correctly. Are we on the same wavelength now?

@ericbf
Copy link
Contributor

ericbf commented Sep 18, 2023

In isolation, each of the myPredicate types is invalid, but with the overloading, it should be valid (but it is not valid with the current implementation of "unioning" the array types to merge the definition). I see what you’re saying here.

Really the type of (A[] | B[]).filter should boil down to something like this for any members of the union.

filter(predicate: ((value: A, index: number, array: A[]) => value is A) | ((value: B, index: number, array: B[]) => value is B), thisArg?: any): A[] | B[]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

4 participants