Разработка

Предварительная версия TypeScript 3.6: что нового

Команда TypeScript сообщила 16 августа о выпуске предварительной версии (release candidate) TypeScript 3.6. В течение нескольких недель команда планирует стабилизировать предварительную версию. После этого TypeScript 3.6 выйдет официально.

Релиз кандидат доступен на NuGet. Также его можно установить через npm.

npm install -g [email protected]

Есть сборки для Visual Studio, Visual Studio Code и Sublime Text.

Давайте посмотрим, какие возможности предлагает TypeScript 3.6.

Строгие генераторы

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

function* foo() {
    if (Math.random() < 0.5) yield 100;
    return "Finished!"
}

let iter = foo();
let curr = iter.next();
if (curr.done) {
    // В TypeScript 3.5 и более ранних версиях здесь ожидалось 'string | number'.
    curr.value
}

Также генераторы ожидали, что типом yield по умолчанию является any.

function* bar() {
    let x: { hello(): void } = yield;
    x.hello();
}

let iter = bar();
iter.next();
iter.next(123); // oops! runtime error!

TypeScript 3.6 ожидает, что в первом примере curr.value имеет тип string. Это возможно благодаря изменениям в определении типов в Iterator и IteratorResult. Также появился новый тип для представления генераторов: Generator.

Iterator в TypeScript 3.6 позволяет пользователям указывать тип принимаемых и возвращаемых данных, а также тип данных, которых принимает next.

interface Iterator {
    // Принимает 0 или 1 аргумент - не принимает 'undefined'
    next(...args: [] | [TNext]): IteratorResult;
    return?(value?: TReturn): IteratorResult;
    throw?(e?: any): IteratorResult;
}

Новый тип Generator всегда итерабельный, также у него всегда есть методы return и throw.

interface Generator
        extends Iterator {
    next(...args: [] | [TNext]): IteratorResult;
    return(value: TReturn): IteratorResult;
    throw(e: any): IteratorResult;
    [Symbol.iterator](): Generator;
}

TypeScript 3.6 конвертирует IteratorResult в различаемое объединение. Это позволяет определять разницу между получаемыми и возвращаемыми данными.

type IteratorResult = IteratorYieldResult | IteratorReturnResult;

interface IteratorYieldResult {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult {
    done: true;
    value: TReturn;
}

В TypeScript 3.6 используется yield в теле функции-генератора. Это позволяет корректно представлять данные, которые передаются в генератор в результате вызова next().

function* foo() {
    let x: string = yield;
    console.log(x.toUpperCase());
}

let x = foo();
x.next(); // первый вызов 'next' всегда игнорируется
x.next(42); // error! ожидается 'string', получено 'number'

Также в TypeScript 3.6 можно явно указывать тип данных, полученных из yield expression. В примере ниже next() можно выбрать только с типом данных boolean. В зависимости от значения done, value может быть строкой или числом.

/**
 * - получает на вход числа
 * - возвращает строки
 * - можно передать в boolean
 */

function* counter(): Generator {
    let i = 0;
    while (true) {
        if (yield i++) {
            break;
        }
    }
    return "done!";
}

var iter = counter();
var curr = iter.next()
while (!curr.done) {
    console.log(curr.value);
    curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase());

// вывод:
//
// 0
// 1
// 2
// 3
// 4
// 5
// DONE!

Детали можно найти в пулреквесте.

Более точный оператор Array Spread

До появления стандарта ES-2015 в циклах for/of или спред-операторах массивов были довольно неудобные emit’ы. Поэтому в TypeScript используется более простой дефолтный emit, который поддерживает только массивы. Также он поддерживает итерацию других типов данных с флагом --downlevelIteration. При использовании этого флага код получается более точным, но менее лаконичным.

Параметр --downlevelIteration в новой версии исключён, так как большинство пользователей пользуются итерацией по массивам. Тем не менее emit, который поддерживает только массивы, работает по-другому в некоторых пограничных случаях.

Обратите внимание на пример ниже.

[...Array(5)]

Он эквивалентен массиву из следующего примера.

[undefined, undefined, undefined, undefined, undefined]

Однако TypeScript трансформирует его в такой код.

Array(5).slice();

В то же время Array(5) создаёт массив с длинной 5, но без определённых элементов.

1 in [undefined, undefined, undefined] // true
1 in Array(3) // false

При использовании метода slice() TypeScript создаёт массив, в котором значения не определены, но у которого есть индексы.

Этот нюанс может показаться несущественным, но он доставляет неудобства специалистам. Поэтому в TypeScript 3.6 представлен новый инструмент __spreadArrays. Подробную информацию о нём можно найти в соответствующем пулреквесте.

Более удобные промисы

Промисы — самый распространённый способ работы с асинхронными данными. Однако использование API, ориентированных на промисы, доставляет разработчикам неудобства. В TypeScript 3.6 улучшена работа с неправильно обрабатываемыми промисами.

Например, специалисты иногда забывают использовать .then() или await перед передачей промисов в другую функцию. Новая версия TypeScript сообщает об этой ошибке и предлагает пользователю использовать await.

interface User {
    name: string;
    age: number;
    location: string;
}

declare function getUserData(): Promise;
declare function displayUser(user: User): void;

async function f() {
    displayUser(getUserData());
//              ~~~~~~~~~~~~~
// Argument of type 'Promise' is not assignable to parameter of type 'User'.
//   ...
// Did you forget to use 'await'?
}

Также разработчики иногда пытаются получить доступ к методу до использования await или .then(). Пример ниже показывает, как TypeScript 3.6 реагирует на эту ошибку.

async function getCuteAnimals() {
    fetch("https://reddit.com/r/aww.json")
        .json()
    //   ~~~~
    // Property 'json' does not exist on type 'Promise'.
    //
    // Did you forget to use 'await'?
}

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

Также появилась опция быстрого исправления описанной выше ошибки (см. иллюстрацию).

автоисправление

Подробности смотрите в соответствующем issue.

Улучшена поддержка юникода в идентификаторах

В релиз кандидате TypeScript 3.6 улучшена поддержка юникода в идентификаторах. Пример ниже.

const 𝓱𝓮𝓵𝓵𝓸 = "world"; // ранее не поддерживалось, теперь можно использовать с '--target es2015'

Поддержка import.meta в System.js

В TypeScript 3.6 включена поддержка трансформации import.meta в context.meta. Нагляднее в коде.

// Этот модуль:

console.log(import.meta.url)

// трансформируется в:

System.register([], function (exports, context) {
  return {
    setters: [],
    execute: function () {
      console.log(context.meta.url);
    }
  };
});

Методы доступа get и set можно использовать в окружающем контексте

В более ранних версиях методы доступа get и set нельзя было использовать в окружающем контексте (например, в declare class). Теперь разработчики могут пользоваться геттерами и сеттерами.

declare class Foo {
    // Возможно начиная с версии 3.6+.
    get x(): number;
    set x(val: number): void;
}

Окружающие классы и функции можно объединять

В ранних версиях TypeScript попытка объединить классы и функции приводила к ошибке в любом случае. В предварительной версии TypeScript 3.6 классы и функции с модификатором declare можно объединять.

Это позволяет использовать такой код.

export declare function Point2D(x: number, y: number): Point2D;
export declare class Point2D {
    x: number;
    y: number;
    constructor(x: number, y: number);
}

Раньше пришлось бы писать так.

export interface Point2D {
    x: number;
    y: number;
}
export declare var Point2D: {
    (x: number, y: number): Point2D;
    new (x: number, y: number): Point2D;
}

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

API теперь поддерживают --build и --incremental

Начиная с версии 3.0 в TypeScript включена поддержка --build, а в версии 3.4 добавлена поддержка --incremental. Эти флаги позволяют гибко структурировать проекты и ускоряют разработку. К сожалению, --build и --incremental не работали со сторонними инструментами, например, Gulp или Webpack. В TypeScript 3.6 данная возможность появилась.

Чтобы создавать сборки с --incremental, разработчики могут использовать API createIncrementalProgram и createIncrementalCompilerHost. Новая функция readBuilderProgram позволяет использовать файлы из .tsbuildinfo, сгенерированные указанными API.

Подробности смотрите в пулреквесте.

Решена проблема точки с запятой

Редакторы и среды разработки типа Visual Studio Code и Visual Studio автоматически предлагают исправления и дополнения. Это реализовано с помощью TypeScript. Старые версии TypeScript автоматически добавляли точку с запятой в конце каждого выражения. Это неудобно для части пользователей, так как в некоторых стайлгайдах использование точки с запятой не предустматривается.

Новая версия TypeScript интеллектуально решает проблему точек с запятыми. Если в вашем проекте они используются, редактор их добавляет в код. В противном случае точки с запятыми не добавляются.

Детали в пулреквесте.

Умный синтаксис автоимпорта

Старые версии TypeSript по умолчанию используют для автоимпорта синтаксис ECMAScript. Это неприемлемо для части проектов TypeScript с определёнными настройками компилятора, а также для проектов Node с использованием нативного JavaScript, где используется require.

TypeScript 3.6 интеллектуально решает проблему автоимпортов. Для этого он изучает существующие импорты, которые уже используются в проекте.

Подробнее в пулреквесте.

Методы со строковым именем constructor становятся конструктором

Методы с названием constructor считаются конструктором независимо от того, как они объявляются: с помощью идентификатора или строкового имени.

class C {
    "constructor"() {
        console.log("Я конструктор.");
    }
}

Важное исключение — вычисляемые значения остаются методами.

class D {
    ["constructor"]() {
        console.log("Я метод, а не конструктор!");
    }
}

Обновления DOM

Ниже несколько важных изменений.

  • Тип глобального объекта window изменился с Window на Window & typeof globalThis.
  • Вместо GlobalFetch используется WindowOrWorkerGlobalScope.
  • Вместо контекста experimental-webgl используется webgl или webgl2.

Комментарии JSDoc не объединяются

В проектах JavaScript TypeScript обращается к комментариям JSDoc, чтобы определить объявленный типы.

/**
 * @param {string} arg
 */
/**
 * oh, hi, were you trying to type something?
 */
function whoWritesFunctionsLikeThis(arg) {
    // 'arg' has type 'any'
}

Ключевые слова не могут содержать избегающие последовательности

В релиз кандидате вводится запрет на использование избегающих последовательностей в ключевых словах.

while (true) {
    \u0063ontinue;
//  ~~~~~~~~~~~~~
//  error! Keywords cannot contain escape characters.
}

Что дальше

Команда TypeScript ждёт обратную связь по релиз кандидату TypeScript 3.6. Полученные данные помогут выпустить официальную версию TypeScript.

Адаптированный перевод статьи Announcing TypeScript 3.6 RC. Мнение администрации «Хекслета» может не совпадать с мнением автора оригинальной публикации.

Дмитрий Дементий 23 августа 2019
Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →