Перейти к содержанию

Логический вывод типа в TypeScript

TypeScript может логически вывести (а затем проверить) тип переменной на основе нескольких простых правил. Потому как эти правила просты, вы можете научить свой мозг распознавать безопасный / небезопасный код (это случилось со мной и моими товарищами по команде довольно быстро).

Поток типов - это то, как я представляю себе в уме распространение информации о типах.

Определение переменной

Типы переменной определяются по её определению.

1
2
3
let foo = 123; // foo `число`
let bar = 'Hello'; // bar `строка`
foo = bar; // Ошибка: невозможно `строке` присвоить `число`

Это пример типов, распространяющихся справа налево.

Типы значений возвращаемых функцией

Тип возвращаемого значения определяется инструкцией возврата, например, предполагается, что следующая функция возвращает число.

1
2
3
function add(a: number, b: number) {
    return a + b;
}

Это пример типов, распространяющихся снизу вверх.

Присвоение

Тип параметров функции / возвращаемых значений также может быть определен посредством присваивания, например, здесь мы говорим, что foo является сумматором, и это делает a и b типом число.

1
2
type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => a + b;

Этот факт может быть продемонстрирован с помощью приведенного ниже кода, который вызывает ошибку, как можно было и ожидать:

1
2
3
4
5
type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => {
    a = 'hello'; // Ошибка: невозможно `строке` присвоить `число`
    return a + b;
};

Это пример типов, распространяющихся слева направо.

Логический вывод типов срабатывает с этим же стилем присвоения, если вы создаете функцию с параметром в виде колбэка. В конце концов, argument -> parameter - это просто еще одна форма присвоения переменных.

1
2
3
4
5
6
7
8
type Adder = (a: number, b: number) => number;
function iTakeAnAdder(adder: Adder) {
    return adder(1, 2);
}
iTakeAnAdder((a, b) => {
    // a = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`
    return a + b;
});

Структурирование

Эти простые правила также работают при использовании структурирования (создание литерала объекта). Например, в следующем случае тип foo определяется как {a:number, b:number}

1
2
3
4
5
let foo = {
    a: 123,
    b: 456,
};
// foo.a = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`

Аналогично для массивов:

1
2
const bar = [1, 2, 3];
// bar[0] = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`

Ну и конечно же любое вложение:

1
2
3
4
let foo = {
    bar: [1, 3, 4],
};
// foo.bar[0] = 'hello'; // Будет ошибка: невозможно `строке` присвоить `число`

Деструктуризация

И, конечно же, они также работают с деструктуризацией, оба:

1
2
3
4
5
6
let foo = {
    a: 123,
    b: 456,
};
let { a } = foo;
// a = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`

и массивы:

1
2
3
const bar = [1, 2];
let [a, b] = bar;
// a = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`

И если параметр функции может быть логически выведен, то могут и его деструктурированные свойства. Например, здесь мы деструктурируем параметр на a/b.

1
2
3
4
5
6
7
8
9
type Adder = (numbers: { a: number; b: number }) => number;
function iTakeAnAdder(adder: Adder) {
    return adder({ a: 1, b: 2 });
}
iTakeAnAdder(({ a, b }) => {
    // Типы `a` и` b` логически выводятся
    // a = "hello"; // Будет ошибка: невозможно `строке` присвоить `число`
    return a + b;
});

Защита типа

Мы уже видели, как Защита типа помогает изменять и уточнять типы (особенно в случае объединений). Защиты типов - это просто еще одна форма логического вывода типа для переменной в блоке.

Предупреждения

Будьте осторожны с параметрами

Типы не распространяются в параметры функции, если они не могут быть логически выведены из присвоения. Например, в следующем случае компилятор не знает тип foo, поэтому он не может определить тип a или b.

1
2
3
const foo = (a, b) => {
    /* сделать что-нибудь */
};

Однако, если был введен foo, тип параметров функции может быть логически выведен (a,b оба выведены как имеющие тип number в примере ниже).

1
2
3
4
type TwoNumberFunction = (a: number, b: number) => void;
const foo: TwoNumberFunction = (a, b) => {
    /* сделать что-нибудь */
};

Будьте осторожны с возвращаемыми значениями

Хотя TypeScript может обычно логически выводить тип возвращаемого значения функции, он может не соответствовать ожидаемому. Например, здесь функция foo имеет тип возврата any.

1
2
3
4
5
6
7
function foo(a: number, b: number) {
    return a + addOne(b);
}
// Какая-то внешняя функция в библиотеке, которую кто-то написал в JavaScript
function addOne(c) {
    return c + 1;
}

Это связано с тем, что на возвращаемый тип влияет плохое определение типа для addOne (c равно any, поэтому возвращаемое от addIn равно any, поэтому отсюда и foo равно any).

Я считаю, что проще всего всегда быть явным в описании возвращаемых значений функций. Ведь эти описания являются теоремой, а тело функции является доказательством.

Есть и другие случаи, которые нетрудно себе представить, но хорошая новость заключается в том, что есть флаг компилятора, который может помочь отловить такие ошибки.

noImplicitAny

Флаг noImplicitAny указывает компилятору выдавать ошибку, если он не может определить тип переменной (и, следовательно, может иметь ее только как неявный any тип). Далее вы сможете

  • либо сказать, что да, я хочу, чтобы это было типом any и явно добавить описание типа : any
  • либо помочь компилятору, добавив еще несколько правильных описаний.

Комментарии