为什么类型谓词的否定会使对象永远不会是类型?

时间:2019-12-16 11:05:48

标签: typescript

我想创建一种方法来指定特定对象的属性(可为空)是否为null。我想这样做,以便可以筛选出属性确实为null的所有对象,以便可以安全地使用该对象并访问其属性。

在此示例中,Container是对象,nullableContent是属性。我想通过在Container的通用参数中使用条件类型来指定该属性不为null。如果HasThing为true,则nullableContent不为null。如果为false,则为null。

即使在创建类型谓词hasContent时,这似乎也可以工作。在hasContent(container) === true的情况下,可以算出container的类型为Container<true>。但是,如果hasContent(container) === false的TypeScript认为container的类型永远不会,那么我将无法再访问其任何属性。

为什么会这样?为什么TS无法确定container现在只是类型Container<false>

interface Container<HasThing extends boolean = boolean> {
  id: string;
  nullableContent: HasThing extends true ? string : null;
}

const hasContent = (container: Container): container is Container<true> =>
  !!container.nullableContent;

function doThing(container: Container) {
  if (hasContent(container)) {
    return container;
  } else {
    // Below line gives error "Property 'id' does not exist on type 'never'.ts(2339)"
    throw new Error(`Container with ID ${container.id} is empty`);
  }
}

编辑:我创建了this example on the TypeScript playground

2 个答案:

答案 0 :(得分:1)

在工会上,缩小效果最好。在false分支上具有并集之后,编译器可以从该联合中取出在true分支上处理过的组成部分。

如果没有要缩小的并集,则编译器将使原始类型与保护类型相交,该保护类型将在Container<true>分支上对true起作用,但是在false分支上它将尝试做类似Exclude<Container, Container<true>>的操作,结果在这里never(仍然试图绕开为什么……)

最简单的解决方案是将接口转换为联合:

type Container = {
  id: string;
} & ({ nullableContent: string } | { nullableContent: null })

const hasContent = (container: Container): container is  Exclude<Container, {  nullableContent: null }> =>
  !!container.nullableContent;

function doThing(container: Container) {
  if (hasContent(container)) {
    return container;
  } else {
    // Below line gives error "Property 'id' does not exist on type 'never'.ts(2339)"
    throw new Error(`Container with ID ${container.id} is empty`);
  }
}

Playground Link

或者如果您想保留type参数:

type Container<HasThing extends boolean = boolean> = {
  id: string;
} & (HasThing extends true ? { nullableContent: string } : { nullableContent: null })

const hasContent = (container: Container): container is Container<true> =>
  !!container.nullableContent;

function doThing(container: Container) {
  if (hasContent(container)) {
    return container;
  } else {
    // Below line gives error "Property 'id' does not exist on type 'never'.ts(2339)"
    throw new Error(`Container with ID ${container.id} is empty`);
  }
}

Playground Link

答案 1 :(得分:0)

看起来TS不能缩小类型。天哪,我不知道为什么,但是我可以提出一种解决方案,该解决方案至少会更改/影响您的代码。将类型定义更改为条件联合,例如:

type Container<T extends boolean = boolean> = T extends true ? {
  id: string;
  nullableContent: string;
} : {
  id: string;
  nullableContent: null;
}

现在您的其余代码都可以使用适当的类型范围缩小和推断。