Union of user-defined type guards spawns incorrect type
#56,731 创建于 2023年12月11日
仓库指标
- Star
- (48,455 star)
- PR 合并指标
- (平均合并 6天 17小时) (30 天内合并 9 个 PR)
描述
🔎 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
💻 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];
}