beta

4.3

Отдельные типы аксеcсоров

Автоматическое преобразование типов в JavaScript сокращает время, но мешает TypeScript повысить его типобезопасность. Нередко можно встретить код, в котором предполагается, что геттер принадлежит к типу number, в то время как сеттер ожидает значение принадлежащее к любому типу, а соответствие производится за счет преобразования.

js
class T { private _value = 0; /** * Предполагается, что геттер value возвращает тип number. */ get value() { return this._value; } /** * Предполагается, что сеттер может обрабатывать * сценарии работы с любым типом. * * [0] преобразовываем value к типу number. Это необходимо, * посколькуможет оно может принадлежать к любому типу. * * [1] Если значение convertedValue не способно быть преобразовано, * то прекращаем выполнение кода сеттера... * * [2] ...иначе, присваиваем его приватному полю _value. */ set value(value){ let convertedValue = Number(value); // [0] if(!Number.isFinite(convertedValue)){ // [1] return; // [2] } this._value = convertedValue; // [3] } }

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

ts
// Ok class T0 { private _value = 0; get value(): number { return this._value; } /** * [*] Ok */ set value(value: number){ // [*] } } // Error class T1 { private _value = 0; get value(): number { return this._value; } /** * [*] Error -> 'get' and 'set' accessor must have the same type.ts(2380) */ set value(value: number | string | boolean){ // * } }

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

ts
// Начиная с текущей версии.. class T0 { private _value = 0; get value(): number { return this._value; } /** * [*] Ok */ set value(value: number | string | boolean){ // * } }

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

ts
class T { private _value = 0; /** * [*] Error -> The return type of a 'get' accessor must be assignable to its 'set' accessor typets(2380) */ get value(): number { // [*] return this._value; } /** * Сеттер не включает тип возвращаемый геттером. */ set value(value: string | boolean){ } }

Модификатор override и флаг --noImplicitOverride

Представьте случай переопределения подклассом некоторых методов своего суперкласса.

ts
class SuperClass { /** * [*] Определяет метод */ a(){} // [*] b(){}// [*] } class SubClass extends SuperClass { /** * [*] Переопределяет методы своего суперкласса. */ a(){} // [*] b(){} // [*] }

Но что, если над проектом работает большое количество команд находящихся в разных уголках земного шара и вдруг, разработчики SuperClass, решили изменить его api удалив оба метода? В таком случае, разработчики класса SubClass даже не узнают об этом, поскольку переопределение превратится в определение. Другими словами, компилятор даже глазом не моргнет, поскольку ему будет казаться, что класс SubClass определят методы a() и b().

ts
class SuperClass { /** * Удалили a() и b() и добавили c(). */ с(){} } class SubClass extends SuperClass { /** * Ошибки не возникает, так как компилятор считает * что данный класс определяет оба метода. */ a(){} b(){} }

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

ts
class SuperClass { /** * Удалили a() и b() и добавили c(). */ с(){} } class SubClass extends SuperClass { /** * [*] Error -> * This member cannot have an 'override' modifier * because it is not declared in the base class 'SuperClass'.ts(4113) * * Теперь компилятор понимает, что происходи переопределение * несуществующих методов. */ override a(){} // [*] override b(){} // [*] }

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

ts
/** * [0] метод определенный только в SubClass */ class SuperClass { a(){} } class SubClass extends SuperClass { b(){} // [0] } /** * [1] Но спустя некоторое время класс SuperClass * определяет метод b(), который уже существует в * классе-потомке [2]. Другими словами, произошло * нежелаемое переопределение способное привести * к непредсказуемому поведению программы. */ class SuperClass { a(){} b(){} // [1] } class SubClass extends SuperClass { b(){} // [2] }

При активации флага --noImplicitOverride, в подобных случаях будет возникать ошибка.

ts
class SuperClass { a(){} b(){} } class SubClass extends SuperClass { /** * --noImplicitOverride = true * * [*] Error -> This member must have an 'override' * modifier because it overrides a member in the base * class 'SuperClass'.ts(4114) */ b(){} // [*] }

Улучшение работы шаблонного строкового типа

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

ts
type SeaFish = `shark` | `barracuda`; type RiverFish = `pike` | `pike perch`; type FishSoup = `${SeaFish | RiverFish} fish`; // type FishSoup = "shark fish" | "barracuda fish" | "pike fish" | "pike perch fish"

Кроме того, при установлении совместимости компилятор в состоянии определить совместимость обычных типов, как например number или boolean с их литеральными представителями.

ts
declare let v0: `${number}-${number}-${number}`; declare let v1: `1-2-3`; v0 = v1; // Ok

Тем не менее, компилятору не под силу вычисления на основе типа значения.

ts
/** * [*] Error -> Type 'string' is not assignable to type '`Hello ${string}`'.ts(2322) * Хотя компилятор знает тип значения param, он не может расспознать совместимость * типа возвращаемого значения с типом указанным в сигнатере функции. */ function f(param: string): `Hello ${string}` { return `Hello ${param}`; // [*] }

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

ts
/** * >=v4.3 * * [*] Ok! */ function f(param: string): `Hello ${string}` { return `Hello ${param}`; // [*] }

Изменение поведения для объекта Promise в условных выражениях

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

ts
/** * Фабрика возвращающая объект обещания. */ function factory(){ return Promise.resolve(false); } /** * Действие, логика которого зависит от существования * объекта Promise. */ async function action() { /** * Если объект обещания существует, то выполняем * некоторые действия. */ if(factory()){ // ... } }

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

ts
/** * [0] Асинхронный валидатор */ async function validate(): Promise<boolean> { // [0] return false; } /** * [1] Асинхронное действие результат выполнения которого * зависит от асинхронного валидатора.[2] Но несмотря на то, что * результатом выполнения функции validate() является значение flase, * выполнение программы все равно зайдет в блок if поскольку * валидатор, по невнимательности, был вызван без ключевого слова await. * */ async function action(): Promise<void> { /** * --strictNullChecks = true * * До текущей версии - Ok, поскольку компилятор проверяет существоание объекта Promise. * Начиная с текущей версии - Error. * * This condition will always return true since this 'Promise<boolean>' appears to always be defined.ts(2801) */ if (validate()) { // [2] } }

Поэтому, начиная с текущей версии, при активном флаге --strictNullChecks обещания в условных выражениях необходимо конкретизировать. Проверки на существование объекта Promise теперь необходимо выполнять явно.

ts
async function validate(): Promise<boolean> { // [0] return false; } async function action(): Promise<void> { /** * --strictNullChecks = true */ if (validate()) { // Error } if (validate() !== null) { // Ok } validate() ? true : false; // Error validate() !== null ? true : false; // Ok validate() && true; // Error validate() !== null && true; // Ok }

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

ts
async function validate(): Promise<boolean> { // [0] return false; } async function action(): Promise<void> { /** * --strictNullChecks = true */ if (await validate()) { // Ok } await validate() ? true : false; // Ok await validate() && true; // Ok await validate() || true; // Ok }

Единственное о чем ещё стоит упомянуть, что на данный момент, по неизвестной причине, в условном выражении ИЛИ (||), при отсутствии ключевого слова await ошибка не возникает.

ts
validate() || true; // Ok validate() !== null || true; // Ok

Индексные сигнатуры класса

До этого момента индексные сигнатуры являлись исключительно членами экземпляра класса.

ts
class T { /** * Инжексная сигнатура принадлежащая * экземпляру класса. */ [key: string]: string; } let instance = new T(); instance['value'] = '🍨'; // динамическое объявление

Начиная с текущей версии индексные сигнатуры также можно определять на уровне самого класса (static).

ts
class T { /** * Индексная сигнатура принадлежащая * классу. */ static [key: string]: string; } T['value'] = '🍨'; // динамическое объявление

Правила для статических индексных сигнатур аналогичны правилам индексных сигнатур экземпляра класса.

Расширение возможностей ECMAScript приватного модификатора и флаг --useDefineForClassFields

До текущего момента TypeScript позволял применять нативный ECMAScript модификатор доступа private (#) только к полям экземпляра.

ts
/** * >=v4.3 * * [*] Error! */ class T { /** члены класса */ static #CLASS_FIELD = ""; // [*] static get #classProp(){ // [*] return T.#CLASS_FIELD; } static set #classProp(value: string){ // [*] } static #classMethod(){ // [*] } /** члены экземпляра класса */ #instanceField = ""; get #instanceProp(){ // [*] return this.#instanceField; } set #instanceProp(value: string){ // [*] } #instanceMethod(){ // [*] } }

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

ts
/** * >=v4.3 * * Ok! Но с одной оговоркой. [*] Для * разрешения нативных приватных полей класса * следует активировать флаг --useDefineForClassFields, * иначе возникнет ошибка. */ class T { /** члены класса */ static #CLASS_FIELD = ""; // [*] static get #classProp(){ return T.#CLASS_FIELD; } static set #classProp(value: string){ } static #classMethod(){ } /** члены экземпляра класса */ #instanceField = ""; get #instanceProp(){ return this.#instanceField; } set #instanceProp(value: string){ } #instanceMethod(){ } }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Изменения в lib.d.ts

Как и всегда, во имя улучшений, основная библиотека lib.d.ts претерпела множество изменений, большая часть которых пришлась на удаление api Mozila не реализованное ни в одном браузере.

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Enum больше нельзя сравнивать с произвольными числами

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

ts
/** * До текущей версии. */ enum E { A = 0, B = 1 } function f(p: E){ /** * [0][1] Ok! * [2] - Несуществующий индекс. */ if(p === 1){ // [0] } if(p === 2){ // [1] } }

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

ts
/** * >=v4.3 */ enum E { A = 0, B = 1 } function f(p: E){ /** * [0] Ok! * [1] Error -> * This condition will always return 'false' since the types 'E' and '2' have no overlap.ts(2367) */ if(p === 1){ // [0] } if(p === 2){ // [1] } }