判断deque.isEmpty()
以缩小deque
的表观类型的唯一方法是,如果它是具有表单this is ⋯
的返回类型的user-defined type guard method.您可以为isEmpty()
等于true
的Deque<T>
类型指定一个名称:
interface EmptyDeque<T> extends Deque<T> {
head?: undefined;
first(): undefined;
}
然后制作isEmpty()
型防护方法:
class Deque<T> {
/* ✂ snip ✂ */
isEmpty(): this is EmptyDeque<T> {
return this.head == null
}
}
但是,当你直接勾选deque.isEmpty()
的时候,这就是你想要的:
const deque = new Deque<number>()
if (deque.isEmpty()) {
deque.first(); // undefined
deque.head; // undefined
}
当您选中!deque.isEmpty()
时,它将不会按预期运行:
if (!deque.isEmpty()) {
deque.first() // number | undefined
deque.head?.value // number | undefined
}
这是因为TypeScrip没有内置的机制来说明Deque<T>
,即not和EmptyDeque<T>
有任何特殊的特征.您可以这样说,当isEmpty()
返回false
时,它会将Deque<T>
缩小到NonEmptyDeque<T>
,定义如下
interface NonEmptyDeque<T> extends Deque<T> {
head: DequeNode<T>;
first(): T;
}
但是没有语法来描述类型保护的false
面.有一个对它的长期特性请求是microsoft/TypeScript#15048,如果它被实现了,也许你可以写下
class Deque<T> {
// ⚠ THIS IS NOT VALID TS, DON'T TRY THIS ⚠
isEmpty(): this is EmptyDeque<T> else this is NonEmptyDeque {
return this.head == null
}
}
一切都会如你所愿.但你不能这么做,所以我们被困住了.
理想情况下,您会说Deque<T>
要么是EmptyDeque<T>
,要么是NonEmptyDeque<T>
,这意味着从概念上讲,它是类似union type的
type Deque<T> = EmptyDeque<T> | NonEmptyDeque<T>;
如果是这样的话,当你把Deque<T>
缩小到用!deque.isEmpty()
禁止EmptyDeque<T>
的时候,它就必然会变成NonEmptyDeque<T>
.
但是类型脚本中的classes不能是联合;或者至少类declarations不会产生联合类型的实例.您可以这样描述构造函数的类型
const Deque: new<T>() => Deque<T> = class ⋯;
但任何这样的赋值都会使编译器感到困惑,因为右侧的类构造函数表达式不会被视为构造联合.你需要用像type assertion这样的
const Deque = class ⋯ as new<T>() => Deque<T>;
并非常谨慎地处理编译器验证中的松懈.
所以所有这些都给了我们
interface BaseDeque<T> {
isEmpty(): this is EmptyDeque<T>
tail?: DequeNode<T>;
size: number;
}
interface EmptyDeque<T> extends BaseDeque<T> {
head?: undefined;
first(): undefined;
}
interface NonEmptyDeque<T> extends BaseDeque<T> {
head: DequeNode<T>;
first(): T;
}
type Deque<T> = EmptyDeque<T> | NonEmptyDeque<T>;
class _Deque<T> {
head?: DequeNode<T>;
tail?: DequeNode<T>;
size = 0;
first() {
return this.head?.value;
}
isEmpty(): this is EmptyDeque<T> {
return this.head == null
}
}
const Deque = _Deque as new <T>() => Deque<T>;
我们现在可以测试一下:
const deque = new Deque<number>();
const x = deque.first(); // const x: number | undefined
const y = deque.head?.value; // const y: number | undefined
if (!deque.isEmpty()) {
const x = deque.first(); // const x: number
const y = deque.head.value; // const y: number
} else {
const x = deque.first(); // const x: undefined
const y = deque.head; // undefined
}
这看起来很好,工作起来就像你想要的那样.但这值得吗?
这真的取决于您的用例,这样的事情是否值得.就我个人而言,如果我需要在类型级别区分这两种情况,并且仍然使用类,我只需要编写一个BaseDeque<T>
类,然后有NonEmptyDeque<T>
和EmptyDeque<T>
个子类,并在其他地方使用联合类型Deque<T>
.这样就不需要类型断言了.但是,如果我们像这样进行重构,我们可能会完全避免使用类,因为您也可以使用普通对象.具体的利弊已经超出了这里的范围,所以我就不再离题了.
Playground link to code个