为什么 TypeScript 不能从过滤后的数组中推断类型?

uni*_*dle 7 type-inference typescript

下面是一些示例代码。TypeScript 推断validStudentsas的类型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);
Run Code Online (Sandbox Code Playgroud)

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

const validStudents = students.filter(student => student.isValid) as ValidStudent[];
Run Code Online (Sandbox Code Playgroud)

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

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

jca*_*alz 7

这里正在发生一些事情。


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

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

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

幸运的是,您可以Student通过将联合推到顶级来获得几乎等效的受歧视联合版本:

interface BaseStudent {
    name: string;
}
interface ValidStudent extends BaseStudent {
    isValid: true;
}
interface InvalidStudent extends BaseStudent {
    isValid: false;
}
type Student = ValidStudent | InvalidStudent;
Run Code Online (Sandbox Code Playgroud)

现在编译器将能够使用控制流分析来理解上述检查:

if (s.isValid) {
    foo([s]); // okay
}
Run Code Online (Sandbox Code Playgroud)

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


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

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

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

在内部 isValidStudentSad(),编译器知道这student.isValid意味着它student是 a ValidStudent,但在外部 isValidStudentSad(),编译器只知道它返回 aboolean而对传入参数的类型没有影响。

处理这种缺乏推理的一种方法是将此类boolean返回函数注释为用户定义的类型保护函数。编译器无法推断它,但您可以断言它:

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

的返回类型isValidStudentHappy是一个类型谓词,student is ValidStudent。现在编译器将理解这isValidStudentHappy(s)s.

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

另请注意,您可以将箭头函数注释为用户定义的类型保护......相当于isValidStudentHappy

const isValidStudentArrow = 
  (student: Student): student is Student => student.isValid;
Run Code Online (Sandbox Code Playgroud)

我们快到了。如果您将回调注释filter()为用户定义的类型保护,则会发生奇妙的事情:

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

foo(validStudents); // okay!
Run Code Online (Sandbox Code Playgroud)

调用foo()类型检查!这是因为 TypeScript 标准库类型Array.filter()赋予了一个新的调用签名,当回调是用户定义的类型保护时,它会缩小数组的范围。万岁!


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

无论如何,希望有帮助。祝你好运!

Playground 链接到代码