type DequeNode<T> = {
  value: T;
  prev?: DequeNode<T>;
  next?: DequeNode<T>;
};

class Deque<T> {
  head?: DequeNode<T>;
  tail?: DequeNode<T>;
  size = 0;

  first() {
    return this.head?.value;
  }

  isEmpty() {
    return this.head == null
  }
}

const deque = new Deque<number>()
if (!deque.isEmpty()) {
  deque.first() // number | undefined
  deque.head?.value // number | undefined
}

if (deque.head) {
  deque.first() // number | undefined
  deque.head.value // number
}

Playground

在上面的代码中,如果deque.head不为空,它可以正确地推断类型为deque.head.value.但用deque.first()就不行了

如何才能使isEmpty()判断有效,并使first()只返回类型number

推荐答案

判断deque.isEmpty()以缩小deque的表观类型的唯一方法是,如果它是具有表单this is ⋯的返回类型的user-defined type guard method.您可以为isEmpty()等于trueDeque<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>,即notEmptyDeque<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

Typescript相关问答推荐

在角形加载组件之前无法获取最新令牌

如何获取数组所有可能的索引作为数字联合类型?

如何在不使用变量的情况下静态判断运算式的类型?

您可以创建一个类型来表示打字员吗

Angular 17 -如何在for循环内创建一些加载?

即使子网是公共的,AWS CDK EventBridge ECS任务也无法分配公共IP地址

如何在TypeScript对象迭代中根据键推断值类型?

node—redis:如何在redis timeSeries上查询argmin?

typescribe警告,explate—deps,useEffect依赖项列表

在分配给类型的只读变量中维护const的类型

迭代通过具有泛型值的映射类型记录

剧作家元素发现,但继续显示为隐藏

@TANSTACK/REACT-QUERY中发生Mutations 后的数据刷新问题

将对象的属性转换为正确类型和位置的函数参数

为什么我的函数arg得到类型为Never?

Select 类型的子项

如何在Vue动态类的上下文中解释错误TS2345?

我如何键入它,以便具有字符串或数字构造函数的数组可以作为字符串或数字键入s或n

Select 器数组到映射器的Typescript 泛型

const nav: typechecking = useNavigation() 和 const nav = useNavigation() 之间有什么区别?