microsoft / microsoft/TypeScript

Union of user-defined type guards spawns incorrect type

Open
#56,731 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

🔎 Search Terms
  • Type predicate
  • Type guard
  • Type guard union
  • TS 2677
  • A type predicate's type must be assignable to its parameter's type
🕗 Version & Regression Information
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about
  • Tested on (next, 5.3.2, 4.9.5, 3.9.7)
⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.3.2#code/PTAEFVQWwVwZwC6gKYA8HIHYBNQBUAaFAGzmVAHdyBzZJPAZQCYB2ANjZQCcuB7L0ACIAgqAQBPAA7lJXZNgCWAYwCGGAORwxU8rESgARuRVw4C6phUHi5BL1AKEWySq4qodZF03bpAOkEAWAAoCWkISy5xPB0AcRhXbAAeQggUdCxsLTxQAF58AD480AAKV2oALnwASjyi8octcABuEJCQUAAZOh9sZFViOVA4Xg9QagSubABaMws1GDlcADMYTCUEBV5MX2Q4EL6lYldbHVB4xIBGYpLUKuuAH1AmUCeAZlrcotRG0EvW4KHY5DMLkC5TF75W73V7PWEfOqgH4KLRMAFAk67c6TbBvG53OHvWEAFk+31+xIB7TAMWkuBMoAA2oJwbgKI4ABa7OAVQRER6E0BvfkAXVADLQ0g28hCoPwewQrOu+SV6QwOCakWicRxSQUmGWXnwRH1hoE4CKAH4mSycZROdzeURUuAxVVBAA5XhIVmCAEdWnycVaZms+0ILmgnl8v6wl7vflxsUS1BSjDYWVnPAK8CYLaYS6Q7FXWGsl5odVZCKubXSVl6g1G1Kmo0W0DW0N29kRx0xl1uoRen04v3U-A6ekh22JcORnTRogqTDiIjx0DEkVEEagAAGZdLOLeO+gaiUHL2WImM7mlgQi2QmfC2cQufzTDewmXxX3T1ZeIrmSajWgb1kuK4OI25pWqANpht2c7SAu4rLkQrqgO6Q7FlMo7BB0ABCMBIBGKKgNs5AkaCk6Dt6WHYDGFAcsoXIeEuWh7jia5-se2C8HsmDqEgUCnghNB2jeCxyH4UmPuQz4IK+2zvt+HEHok-4ZBq1ZRCBuotgIzaQRA0GdjO8G9s6qEDp6NG+lSuFgN0CA+PqCB8NgMBKMYoCrOsmzbLOwzmLe96kcsoBrPmQZXlM+yAv0wLkEo2z6GRADyyxVD+tFvOi8WYklmD6CoVSCjlbT2aAADqjE2KAHK8AAbl4+rUGI57eWsGz5qAAzEKA4i8DAPVLsMyAPhVO4FSlmDIOlVTQqVZJIr8a7EjuY4MUxo3IFAWgqFiDVbMcmyYK1jCsBwBBjgYhGUOQiAKMQfWqDskR8BQ3LBukaYysEChhSUaXLGU1S1AA3qAIQwTBHQ7iox4kX0yz6o4yDEOIoVYjuTDHuecghAAviQZCgGDUPQ7D8O-EjKMYOjmNyjuR51V440E+VHSVYlI1yK5CjIE1bUkYg-BBoza0QcMHlcqoZAHHlQxTUgBhVGBAL-aUQMlAYoOk+TMNgDuBi-NjJLrcE7PBEAA

💻 Code
// U must extend T, else we get TS2766 error "A type predicate's type must be assignable to its parameter's type."
type UnaryTypeGuard<T, U extends T = T> = (arg: T) => arg is U;

// Let's decalre some guard-signatured function types
declare type Guard1 = (x: 1 | 2 | 3) => x is 1;
declare type Guard2 = (x: 1 | 2 | 3) => x is 2;
declare type Guard3 = (x: 2 | 3 | 4) => x is 4;

// Typed as ["Guard with types:", 1 | 2 | 3, 1] as expected
type TestGuard1 = Guard1 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// Typed as ["Guard with types:", 1 | 2 | 3, 1 | 2] as expected
type TestUnion12 = Guard1 | Guard2 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";
// Typed as ["Guard with types:", any, 2 | 4], so `Guard2 | Guard3` matches type guard signature
type TestUnion23Any = Guard2 | Guard3 extends UnaryTypeGuard<any, infer U> ?  ["Guard with types:", any, U] : "Not Guard";
// But this one is typed as "Not Guard", which means `Guard2 | Guard3` doesn't match type guard signature...
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";

// Let's introduce a function with signature of unioned guards
declare const oneOf: Guard2 | Guard3;
declare const a:  2 | 3;

// While hovering the function call you can see
// `const oneOf: (x: 2 | 3) => x is 2 | 4`
// which seems a type violating TS2766,
// but we still can narrow types as expected
if (oneOf(a)) { 
    // `a` is definitely of type `2` here
} else {
    // `a` is definitely of type `3` here
}

// We can retrieve this stored type `4` in such case
declare const b: any;
if (oneOf(b)) {
    // `b is `2 | 4`
}
🙁 Actual behavior

A union type of user-defined guards' types produces an incorrect guard signature which leads to inconsistent behaviour. The resulting type doesn't extends the type guard sinature in general, but a value of such type acts as a type guard.

// Typed as "Not Guard"
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";

// typed as `const oneOf: (x: 2 | 3) => x is 2 | 4` which violates TS2677
decalre const oneOf: Guard2 | Guard3;
🙂 Expected behavior

A union type of user-defined guards' types should produce a correct type guard signature.

A possible way to achieve this is to forcibly intersect unioned predicates' types with intersected arguments' types to compute a resulting predicate's type. Then:

// ["Guard with types:", 2 | 3, 2]
type TestUnion23 = Guard2 | Guard3 extends UnaryTypeGuard<infer T, infer U> ? ["Guard with types:", T, U] : "Not Guard";

// typed as `const oneOf: (x: 2 | 3) => x is 2` which doesn't violates TS2677
decalre const oneOf: Guard2 | Guard3;
Additional information about the issue

This report comes from my original task of typing a generic function which combines several user-defined type guards with OR strategy.

The simplest solution (see below) spawns the reported behaviour.

function someGuard<TGuards extends UnaryTypeGuard<any>[]>(...guards: TGuards): TGuards[number] {
    return (x => guards.some(g => g(x))) as TGuards[number];
}

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 example and the Guard1, Guard2, Guard3, UnaryTypeGuard, and someGuard declarations. Reproduce the union behavior across the listed TypeScript versions, then trace how unioned user-defined predicates are inferred and validated. Done means the resulting predicate remains assignable to its parameter type and the conditional-type checks produce the expected results.

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
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.