beta

4.8

Улучшение пересечений, объединений и механизма сужения типов

Начиная с версии 4.8 TypeScript вносит значительные изменения при работе в строгом режиме (конкретно --strictNullChecks) затрагивающие пересечения и объединения, а также механизм их сужения.

Теперь такой тип, как unknown стоит воспринимать в образе объединения {} | null | undefined, что даже выглядит логичным поскольку он представляет null, undefined или любой другой тип. Это в свою очередь делает совместимым тип unknown с перечисленными типами.

ts
function f ( a: unknown, b: {} | null | undefined ) { a = b; // (раньше) Ok (теперь) Ok b = a; // (раньше) Error (теперь) Ok }

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

ts
class A { value = true; } let a: {} & A; // (раньше) let a: {} & A (теперь) let a: A let b: {} & string; // (раньше) let b: {} & string (теперь) let b: string

Тем не менее пересечение {} с типами null или undefined сужается до типа never.

Этот факт позволил улучшить логику такого типа, как NonNullable<T>, который, как все помнят, предназначен для исключения типов null и undefined из объединений.

ts
let v: NonNullable<string | number | null | undefined>; // let v: string | number
ts
type NonNullable<T> = T extends null | undefined ? never : T; type NonNullable<T> = T & {};

Поскольку пересечения {} & null и {} & undefined сужаются до типа never, а объединение never | ConcreteType сужаются до конкретного типа, то по факту новая логика NonNullable<T> просто отбрасывает из объединения все null и undefined.

Результатом всего это стало возможным уменьшение условного типа (пока только его) NonNullable<T>.

ts
function foo<T> ( a: NonNullable<T>, b: NonNullable<NonNullable<T>> ) { a = b; // (раньше) Ok (теперь) Ok b = a; // (раньше) Error (теперь) Ok }

В свою очередь эти изменения позволили улучшить анализ потока управления и сужения типов.

ts
function f ( p: unknown ) { if ( p ) { // (раньше) unknown (теперь) {} } else { // (раньше) unknown (теперь) unknown } }

Данное поведение распространяется и на генерики.

ts
function nullableAssert<T> ( value: T ): NonNullable<T> { if ( value === undefined || value === null ) { throw Error( `Value not must null.` ); } /** * (раньше) Error -> Type 'T' is not assignable to type 'NonNullable<T>'.(2322) * (теперь) Ok */ return value; }

Улучшение вывода для infer в литеральном строковом типе

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

ts
function f0 ( p: "true" extends `${ infer U extends boolean }` ? U : never ) { p; // (раньше) boolean (теперь) true } function f1 ( p: "100" extends `${ infer U extends number }` ? U : never ) { p; // (раньше) number (теперь) 100 } function f2 ( p: "100" extends `${ infer U extends bigint }` ? U : never ) { p; // (раньше) bigint (теперь) 100n }

Но есть один нюанс на который стоит обратить внимание. Дело в том, что новый механизм судит о принадлежности типа на основе двухстороннего анализа. То есть, он сначала преобразует значение к указанному типу, а затем преобразует его обратно к первостепенному типу. И если значение полученное в результате преобразования будет отличаться, то тип будет выведен не литеральный. То есть, логическое выражение String(Boolean("true"")) === "true" будет верным, а вот String(Number("1.0")) === "1.0" нет, поскольку результатом преобразования String(Number("1.0")) будет 1, а не 1.0.

ts
function f ( p: "1.0" extends `${ infer T extends number }` ? T : never ) { p; // (раньше) number (теперь) number }

Улучшение производительности для флагов --build, --watch и --incremental

Оптимизация процессов связанных со сборкой у разработчиков TypeScript всегда находится в особом статусе. Поэтому Начиная с текущей версии сборка будет происходить на 10-25% быстрее.

Возникновение ошибки при проверке на равенство с литералом объектного типа

В некоторых языках (как например Phyton) оператор == в операциях с объектными типами выполняет проверку над значениями объектов, как если бы эти объекты были примитивами.

py
// выражение проверяет person_array на отсутствие в нем элементов if person_array == []: print("Длина массива равна 0!")

В JavaScript объектные типы представлены ссылками на них, поэтому схожее выражение всегда будет ложным. Поэтому, чтобы облегчить новичкам переход из одного языка в TypeScript, его поведение было откорректировано.

ts
let array: number[] = []; // бессмысленное условие (раньше) Ok (теперь) Error -> This condition will always return 'false' since JavaScript compares objects by reference, not value.(2839) if (array === []) { } let object: object = {}; // бессмысленное условие (раньше) Ok (теперь) Error -> This condition will always return 'false' since JavaScript compares objects by reference, not value.(2839) if (object === {}) { }

Ужесточение правил вывода типов

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

ts
declare function mergeRandom<T> ( x: T, y: T ): T; /** * function mergeRandom<[number, boolean, string]>(x: [number, boolean, string], y: [number, boolean, string]): * [number, boolean, string] * * a: number, b: boolean, c: string */ let [a, b, c] = mergeRandom([100, true, `🙂`], [500, false, `🤩`]); let a0 = [100, true, `🙂`]; // let a0: (string | number | boolean)[] let a1 = [500, false, `🤩`]; // let a1: (string | number | boolean)[] /** * function mergeRandom<(string | number | boolean)[]>(x: (string | number | boolean)[], y: (string | number | * boolean)[]): (string | number | boolean)[] * * d: string | number | boolean, e: string | number | boolean, f: string | number | boolean */ let [d, e, f] = mergeRandom( a0, a1 );

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

ts
declare function f<T> ( p?: T ): T; /** * (раньше) Ok * [a: any, b: any, c: any] = function f<[any, any, any]>(p?: [any, any, any] | undefined): [any, any, any] * * (теперь) Error -> Type 'unknown' must have a '[Symbol.iterator]()' method that returns an iterator.(2488) * * function f<unknown>(p?: unknown): unknown */ let [a, b, c] = f();

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

ts
declare function f<T> ( p?: T ): T; let [a, b, c] = f<[number, boolean, string]>(); // a: number, b: boolean, c: string

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

До этого неограниченные параметры типа были совместимы с типом {}.

ts
function f<T> ( p: T ) { let a: {} = p; // (раньше) Ok }

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

ts
function toKeys ( object: {} ) { Object.keys( object ); } function f<T> ( p: T ) { let keys = toKeys( p ) // (раньше) Ok, но ошибка вовремя выполнения программы поскольку в функцию toKeys может // попасть undefined } f( undefined ); // ломаем программу передав undefined

В связи с этим поведение было изменено.

ts
function f<T> ( p: T ) { let a: {} = p; // (раньше) Ok (теперь) Error -> Type 'T' is not assignable to type '{}'.(2322) }

Для сохранения логики при новом поведении необходимо явно ограничить параметр типа..

ts
function f<T extends {}> ( p: T ) { let a: {} = p; // (раньше) Ok (теперь) Ok }

..не допустить принадлежность к null или undefined при помощи условия..

ts
function f<T> ( p: T ) { if( p === null || p === undefined ){ return; } let a: {} = p; // (раньше) Ok (теперь) Ok }

..или в случае уверенности в наличии значения прибегнуть к помощи ненулевого утверждения (x!).

ts
function toKeys ( object: {} ) { Object.keys( object ); } function f<T> ( p: T ) { let keys = toKeys( p! ) // (раньше) Ok (теперь) Ok }

[КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ] Типы больше не могут быть импортированы и экспортированы в JavaScript файлах

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

ts
// было import { SomeValue, SomeType } from "some-module"; /** - * @type {SomeType} + * @type {import("some-module").SomeType} */ export const value = SomeValue; // стало import { SomeValue } from "some-module"; /** - * @type {SomeType} + * @type {import("some-module").SomeType} */ export const value = SomeValue;
ts
// было /** * @typedef {string | number} SomeType */ export { SomeType as SomeTypeExported }; // стало /** * @typedef {string | number} SomeType */ /** + * @typedef {SomeType} SomeTypeExported + */