Почему TypeScript не может выводить тип из отфильтрованных массивов? - PullRequest
1 голос
/ 27 мая 2020

Ниже приведен пример кода. 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[];

... но это кажется немного взломанным. (Или, может быть, я просто доверяю компилятору больше, чем самому себе!)

Есть ли лучший способ справиться с этим?

1 Ответ

2 голосов
/ 27 мая 2020

Здесь происходит кое-что.


Первая (незначительная) проблема заключается в том, что с вашим интерфейсом 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'.
}

Компилятор может сузить тип объекта только при проверке свойства, если тип объекта - размеченное объединение и вы проверяете его дискриминантное свойство. Но интерфейс Student не является объединением, дискриминированным или каким-либо другим образом; его свойство isValid относится к типу объединения, а Student - нет.

К счастью, вы можете получить почти эквивалентную версию размеченного объединения Student, подняв объединение на верхний уровень:

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

Теперь компилятор сможет использовать анализ потока управления , чтобы понять приведенную выше проверку:

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 подразумевает, что student - это ValidStudent, но за пределами isValidStudentSad(), компилятор знает только, что он возвращает boolean без каких-либо последствий для типа переданного -в параметре.

Один из способов справиться с этим недостатком логического вывода - аннотировать такие boolean-возвращающие функции, как функция защиты определяемого пользователем типа . Компилятор не может вывести это, но вы можете это подтвердить:

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() проверки типов! Это связано с тем, что типизациям стандартной библиотеки TypeScript для Array.filter() была была присвоена новая сигнатура вызова , которая сужает массивы, когда обратный вызов является определяемым пользователем типом защиты. Ура!


Итак, это лучшее, что компилятор может для вас сделать. Отсутствие автоматического c вывода функций защиты типа означает, что в конце дня вы все еще говорите компилятору, что обратный вызов выполняет сужение и не намного безопаснее, чем утверждение типа, которое вы ' повторное использование в вопросе. Но это немного безопаснее, и, возможно, когда-нибудь предикат типа будет выведен автоматически.

В любом случае, надеюсь, это поможет. Удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...