beta

4.7

Модификаторы вариантности параметров типа in и out

Идентификация данных осуществляется при помощи типов и предназначена для предотвращения попадания в операции неподходящих для них значений. Каждая операция имеет свое низкоуровневое описание содержащее, в том числе и тип, к которому должно принадлежать пригодное для неё значение. Сверка типа значения с этим типом называется процессом выявления совместимости. Совместимость осуществляется по правилам вариантности, которые бывают четырех видов. Но прежде, чем рассмотреть каждый из них, ненадолго отвлечемся на TypeScript. Поскольку TypeScript реализует структурную типизацию, тип проще всего представить в виде обычного листка бумаги, который может содержать имена (идентификаторы) ассоциированные с какими-либо другими типами. Идентификатор + ассоциированный с ним тип = признак типа. Именно на основе этих признаков и осуществляется процесс выявления совместимости речь о которой пойдет сразу после того освещения ещё одной очень простой темы - иерархии наследования. Представляя иерархию наследования в голове сразу вырисовывается картина из мира номинативной типизации, что неосознанно выбивает из колеи структурной. Поэтому сразу стоит сосредоточиться на интересующей нас детали - логическом обозначении иерархических отношений. Дело в том, что иерархия направлена сверху вниз, а значит более базовый тип расположен выше, чем его подтип. Таким образом, в логических выражениях представляющих иерархию базовые типы обозначаются, как большие (>) по отношению к своим подтипам. И наоборот. То есть, SuperType > SubType и SubType < SuperType. Но упомяну ещё раз, в структурной типизации нет понятия иерархия наследования, поскольку при сравнении берутся в расчет признаки типов, а не ссылки (ref). Но чтобы не забивать особо голову просто возьмем за правило, что тип, который обладает всеми признаками другого типа и кроме этого содержит дополнительные, будет считаться подтипом, а значит, в логических выражениях будет обозначаться меньшим (<).

ts
interface A { f0: boolean; } interface B { f0: boolean; f1: number; } interface С { f0: boolean; f1: number; } // A > B или B < A // A > C или C < A // B = C или C = B

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

И так, вариантов всего четыре и каждый из них имеет собственное название. Но чтобы было более понятно рассмотрим сценарий, когда переменной принадлежащей к типу A может быть присвоено значение с типом B.

Ковариантность предполагает, что проверка на совместимость завершится успехом в случаи, когда B < A или B = A (в номинативной типизации B подтип A ). Контрвариантность предполагает, что B > A или B = A (в номинативной бы это звучало, как базовый тип можно совместим с подтипом или самим собой, но не наоборот). Инвариантность, это когда совместимы исключительно при условии B = A. Бивариантность подразумевает B < A, B > A или B = A, то есть - все предыдущие варианты в одном.

А теперь к сути дела. В TypeScript все типы проверяются на совместимость по ковариантным правилам, за исключением параметров функций, которые контрвариантны. Поскольку различные правила на сложных рекурсивных типах требуют дорогостоящие вычисления, TypeScript реализует механизм явного аннотирования параметров типа при с помощью необязательных модификаторов in и out.

in указывает, что параметр типа ковариантен, а out контрвариантен. Но стоит сделать акцент на том, что с помощью этих модификаторов невозможно изменить правила по которым TypeScript производит вычисления совместимости, а можно лишь их конкретизировать.

ts
type Setter<T> = (param: T) => void; type Getter<T> = () => T; /** * Стандартный код. * При сравнении двух сеттеров параметры в сигнатуре будут проверятся по контрвариантным правилам, а для геттеров возвращаемые типы по ковариантным. */
ts
type Setter<in T> = (param: T) => void; type Getter<out T> = () => T; /** * [Код с модификаторами] * Правила будут идентичны предыдущему примеру. Разница лишь в явной конкретизации. */
ts
type Setter<out T> = (param: T) => void; // [0] type Getter<in T> = () => T; // [1] /** * [Код с модификаторами] * К тому же, нельзя изменить поведение, то есть - нельзя поменять модификаторы местами! */ /** * [0] * Type 'Setter<sub-T>' is not assignable to type 'Setter<super-T>' as implied by variance annotation. * Types of parameters 'param' and 'param' are incompatible. * Type 'super-T' is not assignable to type 'sub-T'.ts(2636) */ /** * [1] * Type 'Getter<super-T>' is not assignable to type 'Getter<sub-T>' as implied by variance annotation. * Type 'super-T' is not assignable to type 'sub-T'.ts(2636) */

Проще всего воспринимать эти модификаторы, как указание на то, что тип будет использоваться во входных параметрах (<in T>) или выходных (<out T>) или и то и другое одновременно <in out T>.

ts
/** * Указание на то, что тип используется во входных и в выходных параметрах. */ type Func<in out T> = (param: T) => T;

Анализ потока управления для вычисляемых свойств

Предыдущие версии TypeScript прекрасно справлялись с анализом вычисляемых членов объектов идентификаторы которых определялись непосредственно в квадратных скобках {[id]: type}.

ts
let o = { [`hundred`]: Math.round(Math.random()) ? 100 : "100" }; if (typeof o[`hundred`] === "string") { o[`hundred`].toUpperCase(); // Ok }else{ o[`hundred`].toFixed(); // Ok }

Но это не работало при внешнем определении идентификатора.

ts
const key = `hundred`; let o = { [key]: Math.round(Math.random()) ? 100 : "100" }; if (typeof o[key] === "string") { o[key].toUpperCase(); // Error -> Property 'toUpperCase' does not exist on type 'string | number'. }else{ o[key].toFixed(); // Error -> Property 'toFixed' does not exist on type 'string | number'. }

Начиная с текущей версии алгоритм анализа был доработан.

ts
const key = `hundred`; let o = { [key]: Math.round(Math.random()) ? 100 : "100" }; if (typeof o[key] === "string") { o[key].toUpperCase(); // Ok }else{ o[key].toFixed(); // Ok }

Кроме этого, новые правила сделали возможным выявление забытых инициализаций для вычисляемых членов.

ts
/** * [*] * До v4.7 (Ok) на этапе компиляции, но (Error) во время выполнения * Начиная с v4.7 (Error) -> Property '["field"]' has no initializer and is not definitely assigned in the constructor.ts(2564) */ class T { ["field"]: string; // [*] constructor() { // Забыли инициализировать индексное поле name } }

Улучшение вывода функций

В текущей версии были внесены значительные доработки вывода функциональных типов внутри объектов и массивов.

ts
type Param<T> = { a(p: number): T; b(p: T): void; }; declare function f<T>(param: Param<T>): void; f({ a: () => 100, b: x => x.toFixed() }); // Ok f({ a: n => n, b: (x: number) => x.toFixed() }); // Ok f({ a: n => n, b: x => x.toFixed() }); // Было Error, стало Ok f({ a: function() { return 100 }, b: x => x.toFixed() }); // Было Error, стало Ok f({ a: function() { return 100 }, b: x => x.toFixed() }); // Было Error, стало Ok

Конкретизация ссылки на функцию

Существуют ситуации при которых обычные конструкции могут быть более общими, чем это требуется.

ts
interface Box<T> { value: T; } // функция, которая порождает коробки с любым содержимым function createBox<T>(value: T): Box<T> { return {value}; }

До текущей версии конкретизация типа могла бы быть осуществлена с помощью излишнего кода в виде обертки..

ts
interface Food{} // более конкретная функция, которая порождает исключительно коробки с Food function createFoodBox(value: Food) { return createBox(value); }

..или же при помощи псевдонима типа.

ts
type FoodBox = (value: Food) => Box<Food>; // ссылка на более общую функцию createBox, но конкретизированная псевдонимом типа const createFoodBox: FoodBox = createBox;

Первый вариант немного излишен, а второй слишком громоздкий. Поэтому для разрешения подобных сценариев был добавлен механизм конкретизации непосредственно самой ссылки на порождающий объект.

ts
// сохранение ссылки и конкретизация типа одновременно const createFoodBox = createBox<Food>;

Этот же механизм распространяется на функции-конструкторы..

ts
class Box<T> { constructor(readonly value: T){} } const NumberBox = Box<number>; let a = new NumberBox(5); // Ok let b = new NumberBox(`5`); // Error -> Argument of type 'string' is not assignable to parameter of type 'number'.ts(2345)

.. в том числе и Array, Map и Set.

ts
const NumberMap = Map<string, number>; let numberMap = new NumberMap();

Ограничение infer с помощью extends

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

ts
type FirstNumberItem<T> = T extends [infer S, ...unknown[]] ? S extends number ? S : never : never; // type A = number type A = FirstNumberItem<[number, boolean, string]>; // type B = 100 type B = FirstNumberItem<[100, boolean, string]>; // type C = 100 | 500 type C = FirstNumberItem<[100 | 500, boolean]>; // type D = never type D = FirstNumberItem<[boolean, number, number]>;

Несмотря на то, что данный код решает возложенную на него задачу, вложенные условные выражения затрудняют его читаемость. Именно этот факт и стал причиной создания нового механизма позволяющий ограничивать переменные типа infer конкретным типом при помощи ключевого слова extends.

ts
type FirstNumberItem<T> = T extends [infer S extends number, ...unknown[]] ? S : never; /** * ..здесь располагается код аналогичный предыдущему. */

typeof для #приватных членов

Начиная с текущей версии стало возможным выполнять запрос типа на приватных полях предусмотренных в ECMAScript спецификации.

ts
class Class { #field = 100; method(): typeof this.#field { return this.#field; } }

Настройка разрешения поиска модулей с помощью moduleSuffixes

Теперь в tsconfig.json можно указать правила для разрешения модулей на основе расширений.

json
{ "compilerOptions": { "moduleSuffixes": [".ios", ".native", ""] } }

При конфигурации выше, код ниже..

ts
import * as x from "./x";

..будет разрешен таким образом, что сначала осуществится поиск ./x.ios, затем ./x.native и наконец ./x.ts. Стоит обратить внимает, что указание пустой строки в качестве элемента параметра moduleSuffixes требуется для указания поиска файлов с расширением .ts и это значение является значение по умолчанию.

Разрешение импорта и экспорта только типа с помощью resolution-mode

Компилятор TypeScript умеет успешно разрешать сценарии при импортировании таких модулей, как CommonJS и ECMAScript. Для разрешения импорта только типа теперь существует отдельный механизм, который осуществляется путем указания после пути до модуля ключевого слова assert за которым в фигурных скобках определяется конфигурация позволяющая конкретизировать вид модулей.

ts
// Для require() import type { TypeFromRequere } from "module-name" assert { "resolution-mode": "require" }; // Для import import type { TypeFromImport } from "module-name" assert { "resolution-mode": "import" }; export interface MergedType extends TypeFromRequere, TypeFromImport {}

Помимо этого, утверждения можно применить и к динамическому импорту..

ts
import("module-name", { assert: { "resolution-mode": "require" } }).TypeFromRequire; import("module-name", { assert: { "resolution-mode": "import" } }).TypeFromImport;

..а также, к коментарной директиве.

ts
/// <reference types="module-name" resolution-mode="require" /> /// <reference types="module-name" resolution-mode="import" />

Поддержка ECMAScript модулей в Node.js

Начиная с текущей версии TypeScript реализует поддержку ECMAScript модулей в Node.js, который указывается с помощью новых значений node12 и nodenext свойства компилятора module.

json
{ "compilerOptions": { "module": "nodenext" } }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Параметры типа больше не совместимы с {} в strictNullChecks

Изначально параметры типа рассматривались компилятором, как неявно принадлежащие к типу {}, который затем был заменен на unknown. В деактивированном режиме strictNullChecks эти два типа совместимы, но при активации совместимость пропадает. Это известный факт, но до текущей версии для обратной совместимости компилятор TypeScript пренебрегал им.

ts
function f<T>(x: T) { const a: {} = x; // Ok }

Начиная с текущей версии данный прореха в безопасности была закрыта.

ts
function f<T>(x: T) { const a: {} = x; // Error -> Type 'T' is not assignable to type '{}'.ts(2322) }
ts
`````ts

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Метод readFile класса LanguageServiceHost теперь обязательный

Если вы создаете экземпляр класса LanguageService, то необходимые ему LanguageServiceHosts теперь обязаны реализовывать метод readFile.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Свойство length у кортежа теперь readonly

Начиная с текущей версии свойство length у кортежей стало только для чтения readonly.

ts
function f(tuple: [boolean, number, string]) { tuple.length = 0; // Error -> Type '0' is not assignable to type '3'. }