为什么TypeScript编译器无法正确处理嵌套的对象过滤?

时间:2019-03-25 12:37:43

标签: typescript

也许是通用TypeScript问题。 考虑这样一个简单的过滤代码:

interface User {
  id: number;
  name?: string;
}

const users: User[] = [
  { id: 1, name: 'aaa' },
  { id: 2  },
  { id: 3, name: 'bbb' },
];

users
  .filter(user => Boolean(user.name))
  // Object is possibly 'undefined'... why?
  .map(user => console.log(user.name.length));

我在undefined块中过滤了filter,但是编译器坚持说Object is possibly 'undefined'

有人可以解释为什么TypeScript会表现出这种现象吗? 任何好的解决方法。

  

注意:我想使用strictNullChecks选项。

谢谢。

4 个答案:

答案 0 :(得分:4)

TypeScript不够聪明,无法推断出您的过滤器暗示user.name现在不能为假。

显然,Array#filter有一个特定的重载,用于处理类型防护:

// From lib.es5.d.ts
interface Array<T> {
  // ...
  filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];
  // ...
}

这意味着,如果您传递类型保护函数进行过滤,则结果数组将是较窄的类型!

interface ConfirmedNameUser extends User {
  name: string; // no ?
}

// snip

  .filter((user): user is ConfirmedNameUser => Boolean(user.name)
  // no longer errors, type of user is ConfirmedNameUser and not User
  .map(user => console.log(user.name.length));

Playground Example


或者,使用较少的解决方案是使用undefined非null断言运算符告诉它信任您不是!

.map(user => console.log(user.name!.length));

请注意,只有在100%确定TypeScript过于狂热并且“您知道得更好”时,才应谨慎使用!操作。 !仅使警告静音,它不执行任何类型的运行时检查(与建议的?.??运算符相对)。

在不应该使用!的地方可能会导致运行时TypeErrors。

答案 1 :(得分:2)

另一种解决方案是使用type guard作为过滤器回调(将arg is T作为函数的返回类型),以便过滤器之后的调用具有正确的输入类型:

users
  .filter((user): user is Required<User> => !!user.name)
  .map(user => console.log(user.name.length));

在这种情况下,您可以使用内置的映射类型Required<T>,因为只有名称是可选的。但是您也可以根据需要使用类似{ name: string }的类型。

答案 2 :(得分:2)

这里的另一种方法是通知编译器您的过滤器充当type guard

interface NamedUser extends User {
  name: string;
}

users
  .filter((user: User): user is NamedUser => Boolean(user.name)) //annotated callback
  .map(user => console.log(user.name.length)); // okay now

我介绍了一种名为NamedUser的类型,它与User相同,但是其中肯定存在name属性。然后,我将回调方法注释为users.filter()作为类型保护。这将导致编译器选择以下filter() defined in the standard library重载:

interface Array<T> {
  filter<S extends T>(
    callbackfn: (value: T, index: number, array: T[]) => value is S, 
    thisArg?: any
  ): S[];
}

因此,users.filter()的返回值现在为NamedUser[],而不仅仅是User[]。因此,随后的map()调用会按预期工作。

有一个suggestion使编译器将布尔值回调(例如x => !!xx => Boolean(x))识别为类型保护。如果该建议或类似建议曾经得到实施,则您的原始代码可能会在没有任何警告的情况下进行编译。但是现在您必须手动对其进行注释。

希望有所帮助;祝你好运!

答案 3 :(得分:1)

如果您有一些使用命名用户的代码,建议您为他们创建一个类型:

type NamedUser = Required<User>

然后,您可以使用这种代码:

const namedUsers = users.filter(user => Boolean(user.name)) as NamedUser[]
namedUsers.forEach(user => console.log(user.name.length));

或者使用Aaron和jcalz建议的类型保护:

function isNamedUser(user: User): user is NamedUser {
    return Boolean(user.name)
}

users
  .filter(isNamedUser)
  .forEach(user => console.log(user.name.length));