microsoft / microsoft/TypeScript

array filter/find has inconsistent results and incorrect constraint specifications

Open
#56,572 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Domain: check: Type Inference Possible Improvement
Dominant language
Go
Stars
111k
Forks
14.4k
Avg merge
1d 19h
Merged PRs (30d)
117

Description

🔎 Search Terms

type predicate error 2677
union of arrays type predicate
array filter result
infer return type from constraint

One error noted in this bug report (2345 on the rhs of a1 in the code) is a known problem with design-limitation status, I believe. That is incidental and not the main focus of this bug report.

🕗 Version & Regression Information
  • 5.2.2 / 5.3.2
  • I was unable to compare the result on prior versions because there were other worse problems in prior versions
⏯ Playground Link

playground

💻 Code
interface Fizz {
    id: number;
    fizz: string;
}

interface Buzz {
    id: number;
    buzz: string;
}

declare function isFizz(x: unknown): x is Fizz;
const x1 = ([] as Fizz[]|Buzz[]).find(isFizz);
//    ?^  x1: Fizz | undefined
const x2 = ([] as Buzz[]).find(isFizz);
//    ?^  x2: Buzz | undefined

const y1 = ([] as Fizz[]|Buzz[]).filter(isFizz);
//   ?^  y1: Fizz[]
const y2 = ([] as Buzz[]).filter(isFizz);
//   ?^  y2: Buzz[]


// Different results obtained for this non library version
declare function filter2<T,S extends T>(predicate:(t:T)=>t is S, arr:T[]): S[] ;
declare function filter2<T>(predicate:(t:T)=>unknown, arr:T[]): T[] ;

const z1 = filter2(isFizz, ([] as Fizz[]|Buzz[]));
//   ?^  z1: Fizz[]
const z2 = filter2(isFizz, ([] as Buzz[]));
//   ?^  z2: Fizz[]

// Related: If the constraint "S extends T" is ommitted, an error occurs.
declare function filter3<T,S>(predicate:(t:T)=>t is S, arr:T[]): (T&S)[];
//                                                  ~ Type 'S' is not assignable to type 'T'. 2677


// Assuming the object type T is open (i.e., could have other keys, which is the TS default assumption)
// then the filter result should be (T&S)[], so try defining that result explicitly (without an overload, to avoid confusion).
// This should work:
declare function typePredicateFilter4<D, T extends D,S extends D>(predicate:(t:D)=>t is S, arr:T[]): (T&S)[];

const a1 = typePredicateFilter4(isFizz, ([] as Fizz[]|Buzz[])); // error
//   ?^  z1: Fizz[] (invalidated by error)
const a2 = typePredicateFilter4(isFizz, ([] as Buzz[]));
//   ?^  z2: (Buzz & Fizz)[]
🙁 Actual behavior
  1. The types of y2 (Buzz[]) and z2 (Fizz) differ.

  2. As shown in function3, omitting the constraint S extends T results in error 2677.

  3. The result a2 is (Fizz & Buzz)[] as desired (good), but the result of a1 invalidated because of the error occurring on Fizz[]|Buzz[]. So typePredicateFilter4 is not currently usable in the general case.

🙂 Expected behavior
  1. The types of y2 and z2 match.

  2. Want to be able to omit the that constraint S extends T which triggers the error.
    2.1. One reason we want to omit it is because S extends T triggers the inference logic we don't need if (T&S) is explicitly provided as the return type.
    2.2. The other reason is that S extends T unnecessarily constrains the domain of the predicate type function. See typePredicateFilter4 for the correct constraints - D is the domain of the predicate function, and both T and S independently extends D.

  3. typePredicateFilter4 should not produce an error when Fizz[]|Buzz[] is the array argument type. This is an already known issue. 1 and 2 can be solved without solving this problem #3.

Additional information about the issue

Some special type inference logic to calculate the return type is triggered by the presence of the type predicate function arg is S as an argument, and that logic uses the constraint S extends T to infer the return type. If the requirement of having a constraint is dropped, and return type was specified explicitly (as in function filter3 and typePredicateFilter4, then that logic would not be required.

Would such a change be good? If (T & S) is what is actually expected to pass the type predicate function, then I think yes.

Moreover, the entire result signature output of typePredicateFilter4is "linear" with respect to it type inputs. This allows faster resolution.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked TypeScript Playground reproduction and compare the filter/find examples, especially y2 versus z2 and the typePredicateFilter4 calls. Trace type-predicate inference and constraint checking for the shown declarations. Done means the reported result types match and the Fizz[]|Buzz[] call no longer errors, without regressing the stated behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.