🔎 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];
}
1条答案
按热度按时间wwtsj6pe1#
我认为这就是类型交集和分布的工作原理。也许你需要再加一层
extends
或一些通用类型,以排除类型 predicate 的类型不能分配给其参数类型的情况。在这段代码中,类型分布不起作用。
Guard2 | Guard3
的结果类型是一个接受x: 2 | 3
的类型保护器,但Guard3
需要4
作为参数类型。