提问人:dude 提问时间:10/24/2023 最后编辑:dude 更新时间:10/25/2023 访问量:44
在编译时强制类型未缩小范围
Enforce at compile time that a type is NOT narrowed
问:
受本文的启发,我现在正在使用一种返回错误而不是抛出错误的模式(如在 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()
答:
最简单的方法,接近你想要的,看起来像
function isErr<T>(
x: T,
): x is Extract<T, Err<any>> {
return typeof x === 'object' && x != null && ERR in x;
}
从本质上讲,我们不是试图要求输入是多个泛型类型的并集,而是让它只是一个泛型类型,然后通过使用 Extract
实用程序类型来过滤并集来计算 的 “ 部分”。这立即为您提供了与良好调用相同的行为:T
Err
T
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 时才会约束。所以这看起来像Err
Err<any> extends T
T super Err<any>
T super U
T extends (U extends T ? unknown : U)
U extends T ? unknown : U
T
U 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
评论