在编译时强制类型未缩小范围

Enforce at compile time that a type is NOT narrowed

提问人:dude 提问时间:10/24/2023 最后编辑:dude 更新时间:10/25/2023 访问量:44

问:

本文的启发,我现在正在使用一种返回错误而不是抛出错误的模式(如在 golang 或 fp-ts 中)。我用更多的键入扩展了文章中的代码。这样,就可以在编译时知道函数返回了哪种类型的错误。

const ERR = Symbol('ERR');
type Err<ErrType extends string> = {
  [ERR]: true;
  message: string;
  type: ErrType;
};

function isErr<ErrType extends string, Ok>(
  x: Ok | Err<ErrType>,
): x is Err<ErrType> {
  return typeof x === 'object' && x != null && ERR in x;
}

function Err<ErrType extends string>(
  message: string,
  type: ErrType,
): Err<ErrType> {
  return { [ERR]: true, message: message, type: type };
}

你可以像这样使用模式(链接到 playgound):

function errIfFalse(input: boolean) {
    if (input) {
        return Err("input is false", "falseInput")
    }
    return "sucess"
}

function doSomething() {
    const a = errIfFalse(true)
    if (isErr(a)) {
        console.log("error: " + a.message)
        return a.type
    } 
    console.log(a)
    return "no error"
}

const a = doSomething()

到目前为止,它有效,函数的返回类型被正确推断为字符串文字“falseInput”和“no error”之一。但是我对这种模式有一个问题:当代码库被重构时,碰巧一个先前返回错误的函数不再如此。然后,包装函数可能会错误地推断其返回类型。以下示例就是这种情况。这可能会导致代码库中其他部分出现令人困惑的错误。doSomething()

function doSomething2() {
    const a = true
    if (isErr(a)) { // should throw compile time error because a is already narrowed down 
        // this code is unreachable but the compiler does not know that
        console.log("error: " + a.message)
        return a.type
    } 
    console.log(a)
    return "no error"
}

我想以某种方式修改类型缩小函数,以便它只接受尚未缩小范围的输入。我怎样才能做到这一点?isErr()

节点 .js 打字稿 类型断言 类型缩小

评论

0赞 jcalz 10/24/2023
这种方法是否满足您的需求?如果是这样,我会写一个答案来解释;如果没有,我错过了什么?
0赞 dude 10/24/2023
@jcalz它看起来真的很好。我还不明白你的解决方案,解释一下就好了!:)

答:

1赞 jcalz 10/24/2023 #1

最简单的方法,接近你想要的,看起来像

function isErr<T>(
  x: T,
): x is Extract<T, Err<any>> {
  return typeof x === 'object' && x != null && ERR in x;
}

从本质上讲,我们不是试图要求输入是多个泛型类型的并集,而是让它只是一个泛型类型,然后通过使用 Extract 实用程序类型来过滤并集来计算 的 “ 部分”。这立即为您提供了与良好调用相同的行为:TErrT

function doSomething() {
  const a = errIfFalse(true)
  if (isErr(a)) {
    a // Error<"falseInput">
    console.log("error: " + a.message)
    return a.type
  }
  a // "sucess" 
  console.log(a)
  return "no error"
}

对于错误的调用,编译器最终会将输入范围缩小到不可能的 never 类型

function doSomething2() {
  const a = true
  if (isErr(a)) { // okay, but
    console.log("error: " + a.message) // error
    //              --------> ~~~~~~~
    //  Property 'message' does not exist on type 'never'.
    return a.type // error
    //   --> ~~~~
    // Property 'type' does not exist on type 'never'.
  }
  console.log(a)
  return "no error"
}

这不是你要求的,但很明显出了点问题。


如果确实需要禁止调用,除非输入可能是 ,那么可以使用通用约束。理想情况下,您可以给它一个下限约束,例如 或 ,但 TypeScript 仅直接支持上限约束microsoft/TypeScript#14520 上有一个长期存在的未解决问题,但在实施之前,除非实现,否则我们需要解决它。您通常可以通过编写 来模拟约束,其中条件类型仅在不为 true 时才会约束。所以这看起来像ErrErr<any> extends TT super Err<any>T super UT extends (U extends T ? unknown : U)U extends T ? unknown : UTU extends T

function isErr<T extends (Err<any> extends T ? unknown : Err<any>)>(
  x: T,
): x is Extract<T, Err<any>> {
  return typeof x === 'object' && x != null && ERR in x;
}

同样,良好的呼叫不受影响。但是现在这个糟糕的调用是这样做的:

function doSomething2() {
  const a = true
  if (isErr(a)) { // error
    //  --> ~
    // Argument of type 'boolean' is not assignable to parameter of type 'Err<any>'.
    console.log("error: " + a.message)
    return a.type
  }
  console.log(a)
  return "no error"
}

这会在你想要的地方给你一个错误。


这两种窗体都没有按照您想要的方式执行可访问性分析,但这主要只是 TypeScript 的限制或缺少的功能,记录在 microsoft/TypeScript#12825 中。将值缩小到这样的值发生得太晚了,以至于可达性分析无法注意到。希望这个限制没什么大不了的,因为无论哪种方式,你都会遇到错误。never

Playground 代码链接