company_banner

В TypeScript 4.0 наконец появилось то, чего я очень ждал

Автор оригинала: Nathaniel Kessler
  • Перевод
6 августа сего года Microsoft объявила о выходе релиз-кандидата TypeScript 4.0. Там появилась поддержка кортежей с маркированными элементами (Labeled Tuple Elements). А это — именно то, появления чего я ждал в TypeScript.


Наверху — аргументы, которым назначены содержательные метки. Внизу — аргументы с бесполезными именами

Почему я этого ждал? Объясню это на примере разработки функции.

Обобщённый интерфейс, поддерживающий гибкую работу с аргументами


Вот — упрощённый пример. Он основан на использовании интерфейса IQuery. Интерфейс предназначен для описания характеристик функций, выполняющих запросы на получение чего-либо. Он всегда возвращает промис и принимает дженерик, описывающий то, что возвращает промис (TReturn). Этот интерфейс достаточно гибок, им можно пользоваться при создании функций, не принимающих никаких аргументов, или принимающих неизвестное количество аргументов (UParams extends any[] = []).

interface IQuery<TReturn, UParams extends any[] = []> {
  (...args: UParams): Promise<TReturn>
}

Исследуемая функция: findSongAlbum()


Мы, используя этот интерфейс, напишем функцию, которая ищет музыкальные альбомы по названию композиции (title) и по исполнителю (artist). Она возвращает промис, который выдаёт единственный объект типа Album:

type Album = {
  title: string
}

Без использования TypeScript код подобной функции мог бы выглядеть так:

const findSongAlbum = (title, artist) => {
  // код загрузки данных...
  
  const albumName = '1989';
  return Promise.resolve({
     title: albumName
  });
}

Если же написать такую функцию на TypeScript и воспользоваться при этом интерфейсом IQuery, то в качестве первого дженерик-параметра ей нужно передать тип Album. Это позволяет обеспечить то, что форма того, что возвращает промис, всегда будет соответствовать типу Album.

const findSongAlbum: IQuery<Album> = (title, artist) => {
  // код загрузки данных...
  
  const albumName = '1989';
  return Promise.resolve({
     title: albumName 
  });
}

Код, который писали до TypeScript 4.0


При разработке нашей функции ещё нужно объявить параметры и указать то, какие типы они имеют. В данном случае title и artist — это строки. Объявим новый тип, Params, и передадим его в качестве второго типа для IQuery.

В том примере, который написан без использования новых возможностей TypeScript 4.0, Params будет представлен списком типов. Каждый элемент этого списка определяет тип аргумента. Делается это в том же порядке, в котором аргументы располагаются при объявлении функции. Это — пример использования кортежей.

type Params: [string, string]
const findSongAlbum: IQuery<Album, Params> = (title, artist) => {
  // код загрузки данных...
  
  const albumName = '1989';
  return Promise.resolve({
     title: albumName
  });
}

Анализируя тип Params, можно узнать о том, что его первый элемент, string, назначает тип string первому аргументу, то есть — title. Второй аргумент, тоже выглядящий как string, естественно, следуя тому же ходу рассуждений, назначает тип string второму аргументу — artist. Это обеспечит типобезопасную работу со списком аргументов.

Попробуем поработать с этой функцией.


В подсказках к findSongAlbum() выводятся бессмысленные метки аргументов

К сожалению, подобный подход к использованию кортежей не даёт нам полезных и способствующих написанию надёжного кода имён (меток) аргументов при работе с функцией. Вместо этого нам сообщают о том, что аргументами функции являются args_0: string, args_1: string. Из этого сообщения я могу узнать, например, лишь о том, что первый аргумент должен иметь тип string. Метка arg_0 не даёт мне сведений о том, что первым параметром функции должно быть название (title) музыкальной композиции, которую я ищу.

Код, в котором используются возможности TypeScript 4.0


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

Теперь каждый элемент типа Params будет снабжён меткой, которая будет выводиться в IDE при использовании функции findSongAlbum:

type Params: [title: string, artist: string]
const findSongAlbum: IQuery<Album, Params> = (title, artist) => {
  // код загрузки данных...
  
  const albumName = '1989';
  return Promise.resolve({
     title: albumName
  });
}

А вот как выглядит работа с этой функцией.


В подсказках к findSongAlbum() выводятся метки аргументов, позволяющие понять смысл этих аргументов, что весьма полезно

Как видите, теперь, вместо подсказки вида arg_0: string нам дают подсказку title: string. А это значит, что мы теперь знаем не только о том, что функции надо передать строку, но и о том, что именно должна содержать эта строка.

Чего вам особенно не хватает в TypeScript?

RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

Комментарии 41

    –4
    Все это замечательно, но будет ли дальнейшее развитие и пооддержка TypeScript майкрософтом,
    при уже рабочем Blazor? Будут ли живы обе технологии или что то одно они похоронят?
    Для чего будет нужен TypeScript, если теперь можно будет на C# под браузер писать?
      +5

      TypeScript и Blazor/C# — слишком разные технологии, поэтому их сложно назвать конкурентами. Оба инструмента будут и дальше развиваться в своих нишах.

        0
        Разные то они разные, но в итоге решают одну и ту же задачу. Разве нет?
          +3

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

            0
            Да, но при этом одни языки «взлетают», а другие забрасывают.
            Как пример — Ruby в 2007 и Ruby в 2020. Очем я и спрашивал.
              +5

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

                0

                Забросят его, конечно, вряд ли, но до "джаваскрипту капец", который предрекает DarthWazer еще ооочень далеко

                  +1

                  Я застал предыдущую попытку завоевать веб с помощью C# — она называлась Silverlight. Как-то не взлетело.


                  Субъективно вижу только два кейса, на которых Blazor бы блистал:


                  1. Перевод в веб легаси-кода на C#, который страшно или слишком дорого переписывать
                  2. Написание веб-приложений новичками, которые знают только C# и хотят использовать его как серебряную пулю для решения абсолютно любых задач

                  Обе этих ниши довольно узкие, поэтому существование Blazor как таковое представляется скорее как попытка MS впрыгнуть в уезжающий поезд хайпа вокруг WASM. А хайп — штука довольно эфемерная.

                  0
                  Спасибо за конкретный ответ, это я и хотел получить на свой первый коментарий.
                  Я только сейчас начал интересоваться TypeScript, вот это меня и беспокоит.
              0
              Blazor подходит только для тех кто хорошо знает C# большая часть фронтов которая не знает C# ради одной только типизации его учить не будет ( они скорее на Flow уйдут ). Typescript это по факту тот же js но с типами.

              У многих языков есть возможность компилится в js тот же Dart, Scala, Kotlin и т.д и все это было задолго до Blazor. Как мы можем видеть до сих пор ничего из этого не взлетело. Так что я думаю что Blazor если и будет использоваться то только в стане C#, массовым он точно не станет.
            +2
            Blazor/C# — платформа, упрощающая разработку фронтенда, разработчикам бэкэнда. Все это напоминает разработку мобильного приложения: писать нативно или гибридный вариант. Часть ниши закроет, но скорее небольшую. Typescript актуален пока актуален js
            0
            Это обеспечит типобезопасную работу со списком аргументов.

            Насчёт типобезопасности, мы до сих пор можем написать так:


            const album = findSongAlbum('taylor swift', 'bad blood')

            Конкретно в этом примере такое вряд ли произойдёт, но возникают ситуации когда хочется себя дополнительно обезопасить и определить типы SongTitle, ArtistName. В TS это можно сделать с помощью костылей вроде type branding, но в язык nominal typing добавлять пока не спешат.


            А так хочется сказать: "В TypeScript X.Y наконец появилось то, чего я очень ждал" =)

              0
              И? Эти аргументы ничего не сломают, просто не будет найден альбом.

              А вообще да, номинальных типов, а также типов-подмножеств (только отрицательные/положительные, числа до 100, строки длиной (до) х символов) в определенных задачах не хватает, очень. Но спасибо и за то что есть. В крайнем случае, если уж очень нужно можно делать типы-обертки.
                0

                Программа работает неверно — это "ничего не сломают"?)

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

                    Никакая типизация не гарантирует, что программа будет работать верно.

                +4
                Чего вам особенно не хватает в TypeScript?

                Иногда очень сильно нехватает второго дженерик параметра у Promise, обозначающего тип ошибки.
                  +2

                  на самом деле ошибку невозможно типизировать. представьте http запрос с json ответом. вы можете получить ошибку с сетью, а можете получить ошибку того что пришел не json и распарсить результаты не вышло. в этих случаях вы получите разные ошибки. по хорошему нужно чтобы все ошибки в catch блоках были типа unknown как это (опционально) предложили делать в конструкции try-catch, что будет семантически правильно. но в целом достаточно .catch(err: Error) писать


                  поэтому eslint например требует что если используется явно Promise.reject(err) то err должно быть что-то Error-подобное.

                    +2

                    А это не самообман? Я имею ввиду, что он отнюдь неспроста там any (хотя имхо могли бы и unknown сделать). Никто не гарантирует, что Promise упадёт именно с таким типом, который вы желаете увидеть.

                      0

                      да, я совсем не спорю, что в Promise.reject можно засунуть все что угодно. и поэтому привел пример, что в eslint есть правило, которое разрешает использовать Promise.reject только с объектами типа Error и производных (что для меня кажется очень правильным)


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


                      я в домашних проектах всегда включаю strict: true, так что буду рад такой опции

                      0
                      Так ничего не мешает сделать стандартную сигнатуру Promise<T, E=any>, чтобы программист переопределял её на свой страх и риск. В 99% случаев ничего не поменяется, зато в 1% я буду гарантированно знать, какого типа у меня будет ошибка.

                      Не хочу плодить веткок, ответ для n-srg
                      Для примеров с запросами: у нас же все-равно есть какой-нибудь класс-прослойка между приложением и библиотекой для запросов. Можно обрабатывать ошибки там и приводить к нужному виду. Даже если будет несколько типов ошибок через |, ничего нам не помешает обработать их через type-guards.
                        +2

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

                          +1
                          зато в 1% я буду гарантированно знать, какого типа у меня будет ошибка.

                          Повторюсь, по моему мнению, вы занимаетесь самообманом. Видимо вы устали писать код вида:


                          .catch(error => {
                            if (isBlaBlaError(error)) 
                              handleBlablaError(error);
                            else
                              handleUnknownError(error);
                          });

                          Предполагая, что handleUnknownError чрезвычайно маловероятен. И желаете писать код вида:


                          .catch(handleBlaBlaError)

                          Всё так? Если да, то не проще ли сделать так:


                          .catch(genErrorHandler(blaBlaErrorPair))

                          Где blaBlaErrorPair это комбо { guard: isBlaBlaError, handler: blaBlaErrorHandler }? И волки сыты, о овцы целы. На мой взгляд желая Promise<Result, Error> вы желаете странного :)

                          +1

                          В TS4 хотя бы дали возможность типизировать как unknown

                          0
                          В catch может прилететь, все, что только возможно, любое исключение попадет в этот блок. Посмотрите в сторону монады either, она поможет разделить исключения и ошибки.
                          +1

                          Чего не хватает, чего не хватает… очевидного не хватает — например, нормальной поддержки ООП для статических свойств/методов (abstract static не работает и protected static тоже; typeof MyClass нельзя; typeof this.constructor = Function, а не тип своего класса, и т.д.). Там много issues подобного рода висит открытыми годами.

                            +1
                            например, нормальной поддержки ООП для статических свойств/методов (abstract static не работает и protected static тоже

                            А где abstract static работает? Во многих языках он не возможен — Java, C#, в php раньше можно было, потом запретили.


                            protected static работает в TS, например:


                            class Logger {
                                ['constructor']: typeof Logger
                                protected static PREFIX = '[info]'
                                public log(message: string): void {
                                    console.log(this.constructor.PREFIX + ' ' + message)
                                }
                            }
                            
                            class Warner extends Logger {
                              protected static PREFIX = '[warn]'
                            }
                            
                            const a = new Logger(); a.log('a')
                            const b = new Warner(); b.log('b')

                            Про "typeof MyClass нельзя" я не понял.

                              0

                              Я так понимаю, речь идет о конструкции вроде


                              function createInstance(classConstructor: typeof MyClass): MyClass {
                                return new classConstructor('some arg');
                              }

                              Сейчас можно только classConstructor: { new(someArg: string) => MyClass }

                                +1

                                Работает же и так:


                                class MyClass {
                                  constructor(arg: string) {
                                    console.log(arg)
                                  }
                                }
                                
                                function createInstance(classConstructor: typeof MyClass): MyClass {
                                  return new classConstructor('some arg')
                                }
                                
                                createInstance(MyClass)
                                  0

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

                            –1
                            А что мешало IDE совместить имена параметров из функции и типы из описания типов? Они ж в одном порядке идут. Вернее шли до 4.0, сейчас, вероятно, можно перемешивать.
                              0
                              Я не знаю не js ни ts. Скажите, а это такое странное описание функций там? Почему так сделали?
                              type Params: [title: string, artist: string]
                              const findSongAlbum: IQuery<Album, Params> = (title, artist) => {
                              


                              Чем оно удобнее того же питона/с/java/go?
                              IQuery<Album> findSongAlbum(string title, string artist) {
                              ...
                              
                                0
                                Вы все напутали.

                                В приведенном вами примере ваш код утверждает, что функция findSongAlbum вернет тип IQuery

                                НО, в примере из статьи, декларируется что переменная findSongAlbum имеет тип IQuery. Этот тип, судя по всему является вызываемым (callable), проще говоря это тип-функция. Затем мы присваиваем этой переменной анонимную функцию, и поскольку ее тип уже известен, нет нужды проставлять типы параметров.
                                  0
                                  Когда определяешь сам метод то да, ваш вариант удобнее. И он точно также выглядит в ts. Но допустим функция должна принимать другую функцию, определенного типа. Тогда Параметр надо как-то объявить.
                                    0
                                    Точно на TS пишете? ) Если функция принимает другую, определенного типа, то в чем проблема обьявить параметр во втором примере кода, если бы он был TypeScript'ом?

                                    function findSongAlbum(finder: (songs: Song[]) => Song) { //...
                                    
                                      0
                                      Если у вас 10 таких методов, которые получают однотипную функцию? В каждом будете объявление делать? Я не говорю что нельзя так делать, есть дженерики в конце концов. Но есть разница — каждый раз описывать сигнатуру функции, захламляя код или объявить в декларативном стиле.
                                      +2
                                      А кажись понял, это как на том же с++ определить переменную типа функция и присвоить ей лямбду.
                                      +1
                                      Это лямбда (title, artist) => {}, назначенная в переменную findSongAlbum, т.к. js язык с first-class functions.
                                      Более традиционный синтаксис тоже есть:
                                      function findSongAlbum(title: string,  artist: string): IQuery<Album> {
                                      
                                      }
                                      
                                      0
                                      не хватает pattern matching'a и вспомогательного типа Option

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

                                    Самое читаемое