【问题标题】:Why can't TypeScript infer type from filtered arrays?为什么 TypeScript 不能从过滤后的数组中推断类型?
【发布时间】:2020-09-13 20:24:59
【问题描述】:

下面是一些示例代码。 TypeScript 将validStudents 的类型推断为Students[]。任何阅读代码的人都应该清楚,因为所有无效记录都被过滤掉了,所以validStudents 可以安全地被认为具有ValidStudents[] 的类型。

interface Student {
    name: string;
    isValid: boolean;
}
type ValidStudent = Student & { isValid: true };

const students: Student[] = [
    {
        name: 'Jane Doe',
        isValid: true,
    },
    {
        name: "Robert'); DROP TABLE Students;--",
        isValid: false,
    }
];

const validStudents = students.filter(student => student.isValid);

function foo(students: ValidStudent[]) {
    console.log(students);
}

// the next line throws compile-time errors:
// Argument of type 'Student[]' is not assignable to parameter of type 'ValidStudent[]'.
//   Type 'Student' is not assignable to type 'ValidStudent'.
//     Type 'Student' is not assignable to type '{ isValid: true; }'.
//       Types of property 'isValid' are incompatible.
//         Type 'boolean' is not assignable to type 'true'.ts(2345)
foo(validStudents);

可以通过添加类型断言来使这段代码工作:

const validStudents = students.filter(student => student.isValid) as ValidStudent[];

...但感觉有点hacky。 (或者也许我只是比我自己更信任编译器!)

有没有更好的方法来处理这个问题?

【问题讨论】:

  • 它看起来有点难看,但我认为你正在做的是正确的做法。
  • 为什么 TS 能够推断出这个?它只知道您在数组上调用.filter,您也可以指定student => student.name.startsWith("A") 作为过滤条件。或者其他任何不会改变数组组成的东西,因此它不能真正缩小类型。此外,如果您过滤一个空数组,您的有效性过滤器 still 会生成一个无效学生数组。以及一组有效学生。

标签: typescript type-inference


【解决方案1】:

这里发生了一些事情。


第一个(次要)问题是,对于您的Student 接口,编译器不会将检查isValid 属性视为type guard

const s = students[Math.random() < 0.5 ? 0 : 1];
if (s.isValid) {
    foo([s]); // error!
    //   ~
    // Type 'Student' is not assignable to type 'ValidStudent'.
}

如果对象的类型是discriminated union 并且您正在检查它的判别属性,编译器只能在检查属性时缩小对象的类型。但是Student接口不是联合,歧视或其他;它的isValid 属性是联合类型,但Student 本身不是。

幸运的是,您可以通过将联合推到顶层来获得几乎等效的 Student 的可区分联合版本:

interface BaseStudent {
    name: string;
}
interface ValidStudent extends BaseStudent {
    isValid: true;
}
interface InvalidStudent extends BaseStudent {
    isValid: false;
}
type Student = ValidStudent | InvalidStudent;

现在编译器将能够使用control flow analysis 来理解上述检查:

if (s.isValid) {
    foo([s]); // okay
}

此更改并不重要,因为修复它不会突然使编译器能够推断您的 filter() 缩小。但如果可以做到这一点,则需要使用诸如可区分联合之类的东西,而不是具有联合值属性的接口。


主要问题是 TypeScript 不会将控制流分析的结果在函数实现内部传播到调用函数的范围。

function isValidStudentSad(student: Student) {
    return student.isValid;
}

if (isValidStudentSad(s)) {
    foo([s]); // error!
    //   ~
    // Type 'Student' is not assignable to type 'ValidStudent'.
}

内部isValidStudentSad(),编译器知道student.isValid暗示studentValidStudent,但是外部isValidStudentSad(),编译器只知道它返回一个boolean,对传入参数的类型没有影响。

处理这种缺乏推理的一种方法是将boolean-returning 函数注释为user-defined type guard function。编译器无法推断它,但你可以断言它:

function isValidStudentHappy(student: Student): student is ValidStudent {
    return student.isValid;
}
if (isValidStudentHappy(s)) {
    foo([s]); // okay
}

isValidStudentHappy 的返回类型是一个类型谓词student is ValidStudent。现在编译器会明白isValidStudentHappy(s)s 的类型有影响。

请注意,microsoft/TypeScript#16069 建议编译器应该能够为student.isValid 的返回值推断类型谓词返回类型。但是它已经开放了很长时间,我没有看到任何明显的迹象表明它正在工作,所以现在我们不能指望它会被实施。

还请注意,您可以将箭头函数注释为用户定义的类型保护...等价于isValidStudentHappy 是:

const isValidStudentArrow = 
  (student: Student): student is Student => student.isValid;

我们快到了。如果你将你的回调注解为filter() 作为用户定义的类型保护,那么一件奇妙的事情就会发生:

const validStudents = 
  students.filter((student: Student): student is ValidStudent => student.isValid);

foo(validStudents); // okay!

foo() 的调用类型检查!这是因为 Array.filter() 的 TypeScript 标准库类型是 given a new call signature,当回调是用户定义的类型保护时,它将缩小数组。万岁!


所以这是编译器可以为您做的最好的事情。缺乏类型保护函数的自动推断意味着在一天结束时您仍然告诉编译器回调会缩小范围,并且并不比您正在使用的类型断言更安全在问题中。但它更安全一些,也许有一天会自动推断类型谓词。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-17
    • 1970-01-01
    • 1970-01-01
    • 2023-02-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多