TypeScript 用户自定义类型保护的联合体生成了不正确的类型,

j9per5c4  于 6个月前  发布在  TypeScript
关注(0)|答案(1)|浏览(52)

🔎 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)

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];
}
wwtsj6pe

wwtsj6pe1#

我认为这就是类型交集和分布的工作原理。也许你需要再加一层 extends 或一些通用类型,以排除类型 predicate 的类型不能分配给其参数类型的情况。
在这段代码中,类型分布不起作用。

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

Guard2 | Guard3 的结果类型是一个接受 x: 2 | 3 的类型保护器,但 Guard3 需要 4 作为参数类型。

相关问题