Анонс новых возможностей Typescript 1.4

Original author: Ryan Cavanaugh
  • Translation
Выпустив версию Typescript 1.3, мы сфокусировались на усовершенствовании системы типов и добавлении функционала ECMAScript 6 в TypeScript. Давайте рассмотрим некоторые новые возможности, которыми вы сможете пользоваться в новой версии.

Все описанные в статье вещи уже реализованы в мастер-ветке нашего репозитория на Github — вы можете выкачать ее и попробовать их уже сейчас.



Новые возможности позволяют более аккуратно и легко работать с переменными, которые имеют различный тип во время исполнения. Они сокращают количество мест, где нужно явно указывать тип, проверять его или использовать тип any. Авторы типизирующих файлов (.d.ts) могут также использовать эти возможности для описания внешних библиотек. Те, кто следят за развитием компилятора, могли заметить, что мы сами тоже ими пользуемся.

Типы-объединения


Общие сведения


Типы-объединения — это мощный способ выразить значение, которое может иметь один из нескольких типов. Допустим, у вас может быть API для выполнения программы, который принимает аргументы командной строки в виде string или string[]. Теперь можно написать так:

interface RunOptions {
  program: string;
  commandline: string[]|string;
}

Присвоение значений такого типа работает интуитивно — любое значение, которое можно было бы присвоить любому из членов перечисления, сработает:

var opts: RunOptions = /* ... */;
opts.commandline = '-hello world'; // OK
opts.commandline = ['-hello', 'world']; // OK
opts.commandline = [42]; // Ошибка: тип number не совместим с string или string[]

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

if(opts.commandline.length === 0) { // У string и string[] есть свойство length
  console.log("Пусто!");
}

С помощью ограничителей типов, работать с переменной типа-объединения легко и просто:

function formatCommandline(c: string[]|string) {
  if(typeof c === 'string') {
    return c.trim();
  } else {
    return c.join(' ');
  }
}

Ограничители типов


Распространенная практика в Javascript — использовать операторы typeof или instanceof для определения типа значения во время исполнения. Typescript теперь понимает эти конструкции и использует их при выводе типа, если они используются в блоке условия:

var x: any = /* ... */;
if(typeof x === 'string') {
   console.log(x.subtr(1)); // Ошибка: в типе 'string' нет метода 'subtr'
}
// в этой области видимости переменная 'x' все еще типа 'any'
x.unknown(); // OK

Вот так можно пользоваться instanceof с классами и объединенными типами:

class Dog { woof() { } }
class Cat { meow() { } }
var pet: Dog|Cat = /* ... */;
if(pet instanceof Dog) {
   pet.woof(); // OK
} else {
   pet.woof(); // Error
}

Более строгие обобщенные типы


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

function equal<T>(lhs: T, rhs: T): boolean {
   return lhs === rhs;
}

// Раньше: никаких ошибок
// Новое поведение: Ошибка - нет общего типа между 'number' и 'string'
var e = equal(42, 'hello');

Улучшенный вывод типов


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

var x = [1, 'world']; // x: Array<string|number>
x[0] = 'hello'; // OK
x[0] = false; // Ошибка - тип 'boolean' не является ни 'number', ни 'string'

Псевдонимы для типов


Можно объявить псевдоним для типа с помощью ключевого слова type:

type PrimitiveArray = Array<string|number|boolean>;
type MyNumber = number;
type NgScope = ng.IScope;
type Callback = () => void;

Псевдоним типа является его полным синонимом, они полностью взаимозаменяемы при использовании.

В следующей статье я расскажу о возможностях ECMAScript 6, которые мы добавляем в Typescript. Чтобы узнать больше и попробовать самому, выкачайте ветку master из репозитория Typescript на Github, попробуйте и поделитесь с нами.

Примечание переводчика



В Typescript 1.3 появилась возможность использовать кортежи из массивов. Однако автоматический вывод типов при этом не происходит:

// ожидается: [number, string]
// фактически будет: Array<string|number>
var x = [1, 'world'];

Почему так сделали? Автоматический вывод типов-кортежей ломает множество сценариев использования, например:

var x = [dog, cat, animal]; // тип для 'x' будет 'Animal[]'
x[0] = new Frog();

Если бы тип переменной x был выведен как кортеж [Dog, Cat, Animal], то присвоение во второй строке вызвало бы ошибку. Авторы посчитали более правильным требовать явного указания кортежей, и это звучит довольно логично.

Кроме того, кортежи совместимы с типами-перечислениями в одностороннем порядке:
var x : [number, string] = [1, "test"];
var y : Array<number|string> = x; // Все в порядке
x = y; // ошибка: типы совместимы только в одну сторону

Скорее бы поддержка этих возможностей попала в Resharper!

UPD: Не пора ли создать отдельный хаб под Typescript?

Only registered users can participate in poll. Log in, please.

Используете ли вы Typescript?

  • 18.6%Да, вместе с другими продуктами Microsoft (Visual Studio, .NET)101
  • 9.8%Да, в Linux / Mac OS / другой системе53
  • 35.0%Хотел бы попробовать, но пока не доходят руки190
  • 9.8%Использую другие препроцессоры (CoffeeScript, LiveScript)53
  • 18.2%Препроцессоры зло, только нативный Javascript!99
  • 8.7%Не пишу ни на JS, ни на чем подобном47
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 39

    0
    Мне кажется «Типы-объединения» крайне неоднозначная штукенция. Может быть, «на кошечках» это и хорошо, но на деле придётся явно проверять тип. На пару проверок меньше, конечно, но сути не меняет и куда-то пропадает плюс от строгой типизации
      +4
      Эта фишка не заменяет проверку типов, а дополняет ее. Раньше для объявления метода, у которого могут быть разные типы аргументов, приходилось писать кучу заглушек:

      function test(x: number, y: number): boolean;
      function test(x: string, y: number): boolean;
      function test(x: number, y: string): boolean;
      function test(x: string, y: string): boolean;
      function test(x: any, y: any): boolean {
          if (typeof x === "number") {
              // ...
          } else {
              // ...
          }
      
          // ...
      }
      

      Теперь же можно свести всё в одно объявление:

      function test(x: number|string, y: number|string): boolean {
          // ...
      }
      

      Так что возможность действительно полезная и отлично передает «дух JS».
        0
        Но при этом есть гарантия, что переменной можно присвоить только указанные вами типы, с any там что угодно может оказаться. Да проверки все равно нужны, но не ради сокращения проверок думаю это сделано.
        +2
        Осталось сделать, чтобы типы по умолчанию были non-nullable и будет совсем сказка :-)
        А в местах, где нужен nullable тип — просто объединять тип с Null.

        В идеале, ещё бы и ограничители типов через сравнение, а не только через typeof:
        function print( node : HTMLElement | null ) : HTMLElement {
            if( node === null ) node = document.createElement('div')
            node.innerHTML = 'qwerty'
            return node
        }
        
          0
          На счет того, что подобное не компилируется
          var e = equal(42, 'hello');
          

          Вот в scala подобное компилируется без проблем, т.к. общий тип Any. Это порой дико раздражает, однако формально тут придраться не к чему. А разве в typescript нет общего для всех типа? Я в коде также наблюдаю «any». Нет ли здесь какой-то логической ошибки?
            0
            Насколько я понимаю, any — это просто способ сказать компилятору, чтобы он не проверял тип и ты берешь за это ответственность на себя. Т.е. он перестает «ругаться» если ты будешь делать с переменной этого типа что-нибудь странное.
              0
              Получается, что в typescript нет общего предка для всех типов?
                0
                Формально он есть в JavaScript: это тип Object. Разница с any, насколько я понимаю в том, что объявляя переменную типа Object вы ограничиваете доступные методы методами базового типа Object. В случае с any вы просто говорите компилятору чтобы он игнорировал все действия с этим типом, предполагая их как допустимые.
                  0
                  Вот как раз формально то и получается, что общего типа нет, т.к. нет к нему приведения. Либо есть какие-то ограничения на приведение типов к базовому, но этот принцип не совсем понятен.
                    –3
                    В js вообще нет типов. Есть объекты и их прототипы.
                      0
                      Кто-то мог бы сказать, что вы по своему правы, если бы знал, что вы подразумеваете под понятием «тип». Но безотносительно этого, речь здесь не про js.
              0
              В объявлении функции сказано, что оба параметра должны быть типа T. Приведение типов к общему предку тут не только неинтуитивно, но недопустимо — оно ломает проверку типизации, например:

              function push2<T>(data: T[], a: T, b: T) {
                  data.push(a);
                  data.push(b);
              }
              
              push2([1], true, "test"); // работает!
              [1].push(true); // ошибка типизации
              
                0
                На счет этого понятно
                [1].push(true); // ошибка типизации
                

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

                Но в статье говорится, что вот это
                push2([1], true, "test"); // работает!
                

                тоже не должно работать. Вот пытаюсь понять кто прав и как же на самом деле происходит вывод типов.
                  0
                  Разумеется не должно. В нижнем случае получается, что типы не проверяются вообще никак. Если бы это было желаемым результатом, зачем вообще использовать обобщение? Проще было бы написать обычную функцию и типы аргументов не указывать, неявно подразумевая any.
                    0
                    На счет желаемого результата согласен. В большинстве случаев это выглядит удобно. Однако вопрос в том, почему так происходит. Либо в typescript нет общего базового типа, либо вывод типов останавливается где-то посередине по непонятным правилам. Какой вариант правильный?
                      –1
                      В TypeScript реализована структурная типизация. В ней понятие «общий базовый тип» не имеет смысла. Там используются понятия «совместимости типов» и «пересечения типов».
                      Так вот, типы string, number и bolean не являются совместимыми, но даже если бы и были, то нотация вида:
                      function push2 < T > ( data: T[], a: T, b: T ) { }
                      

                      Накладывает ограничение на параметры: a и b должны иметь идентичный тип, а data — массив того же типа.
                      Чтобы они могли иметь разные реальные типы, то для них должны быть введены дополнительные имена:
                      function push2 < DataType , AType extends DataType , BType extends DataType > ( data: DataType[], a: AType, b: BType ) { }
                      

                        0
                        Пересечение совместимых типов или общий базовый тип — это лишь вопрос терминологии в контексте вывода типов. Но в целом я вроде бы понял ситуацию. Спасибо за пояснения.
                          0
                          >> В TypeScript реализована структурная типизация. В ней понятие «общий базовый тип» не имеет смысла.

                          Ну разумеется, имеет. Базовый тип для двух любых типов в таком случае — это тип, содержащий пересечение множеств их членов. Очевидно, что общим базовым типом тогда является тип, не содержащий членов вообще. Он, конечно, не сильно полезен, но он есть.
                            0
                            Пусть у нас есть два типа:

                            interface A {
                                valueOf : () => string
                            }
                            
                            interface B {
                                valueOf : () => number
                            }
                            


                            А теперь попробуем, синтезировать «общий базовый тип». Это можно сделать разными способами. Например, через пересечение:

                            interface AandB {
                            }
                            


                            А можно и через объединение:

                            interface AorB {
                                valueOf : () => any
                            }
                            


                            При номинативной типизации у нас всегда есть иерархия типов, так что поиск «ближайшего общего предка» — тривиальная и однозначная задача. При структурной — у нас нет никакой предопределённой иерархии, так что термины иерархической модели типов несут в себе мало смысла.

                            Приведу более сложный пример с хитрым выведением типов:
                            function parseTime( time : number | string | Date ) : number {
                            
                                // typeof time === Number or String or Date
                                if( typeof time === 'number' ) return time
                            
                                // typeof time === not( Number ) and ( Number or String or Date ) )
                                // typeof time === String or Date
                                if( typeof time === 'string' ) time = new Date( time )
                            
                                // typeof time === Date or ( not( String ) and ( String or Date ) ) )
                                // typeof time === Date
                                return time.getTime()
                            }
                            
                    0
                    Тут проблама с ковариантностью/контравариантностью.
                    Т.к. TypeScript имеет структурную типизацию, то считается, что он выводит ковариантность при операциях.
                    Но вот, в случаях функциях, нужно знать вариантность на уровне сигнатуры.
                    function push<T>(data: T[], a: T): void {
                        data.push(a); // здесь в data заносятся данные, т.е. подойдут типы >=T, т.е. T - контравариантен
                    }
                    

                    Значит, при вызове push([1], "string") — общий тип не может быть найден.
                    Но вот, при вызове push(animals, dog) — тип T должен быть выведен как Animal.

                    Т.е. надо бы либо указывать явно (как минимум в случае declare инструкций) вариантность типа, либо выводить автоматически по реализации (только чтение — ковариантен, только запись — контравариантен).

                    Пожалуй, зафайлю это соображение к ним на GitHub.
                  0
                  Опрос какой-то странный. Если CoffeeScript/LiveScript еще можно считать препроцессорами, то ClojureScript/PureScript/GHCJS/ScalaJS?
                    0
                    Поддерживаю.
                    Но ближайшая альтернатива TrueScript — это Dart.
                    Судя по последним тенденциям, я бы стал доверять продукту от MS, а не от Google.
                      0
                      Эммм, а что с Dart не так?
                        0
                        С самими Dart все ok, он примерно в том же статусе, что и TrueScript. Но то, как обе компании поддерживают свои продукты и сообщество — уже серьезный довод в пользу TrueScript.
                        Сейчас MS гораздо больше заслуживает статус «корпорации добра».
                          0
                          Как минимум тяжелый runtime, который он тащит за собой в ваш код?
                            0
                            Не такой уж и тяжелый, в самом-то деле — в целом не критично. Dart все-таки больше предназначен для написания сколь-нибудь сложных приложений, а не статических страниц с парой скриптов на них.
                            0
                            1. Выводить типы умеет только в самых тривиальных случаях.
                            2. Исполняется в отдельной виртуальной машине. Рантайм из dart2js — это практически порт dart-vm на js.
                            3. Сложности интеграции с существующим JS кодом.
                            4. Кривые SourceMaps и прочие проблемы дебага транслированного в js кода.
                            0
                            Еще теперь есть flow. А coffee — препроцессор, но вообще в другую степь. Стоит добавить в опрос остальные языки, расширяющие синтаксис для написания типизированного кода.
                              +5
                              Что это за ТруСкрипт такой? :-)
                            0
                            Когда будут variadic templates как в C++? Скажем хочется описать интерфейс функции у которой есть несколько дополнительных свойств:

                            interface CustomizedFunction<...TArgs, TResult> {
                              (...args: ...TArgs): TResult;
                              customProperty: number;
                            }
                            


                            Сейчас, насколько позволяют судить мои небольшие познания в TS, придётся забыть про типизацию аргументов и написать ...args: any[].

                            Также хотелось бы видеть более продвинутые шаблоны, опять же как в C++, чтобы можно было определить сначала интерфейс Something а затем Something<T, U> или скажем сделать частичную специализацию вроде Something<T, number>.
                              0
                              Последнее там точно не к месту, поскольку требует различения всех этих типов на этапе выполнения — а в TS для всего используется type erasure (не только для дженериков, а вообще везде), чтобы поведение было максимально близко к JS — вся проверка типов делается статически на этапе трансляции.
                                +1
                                Чисто для спортивного интереса, или у вас действительно есть задача на TS, вызывающая потребность в такого рода конструкциях?
                                  0
                                  Действительно есть. Мне кажется, что такие конструкции довольно часто применяются. Возьмём тот же паттерн observer который можно описать в виде объекта Event:

                                  interface IEvent<...TArgs> {
                                    type TListener = (...args: ...TArgs) => void;
                                    addListener(listener: TListener): void;
                                    removeListener(listener: TListener): void;
                                  }
                                  


                                  После чего использовать этот интерфейс для описания объектов с событиями:

                                  interface IObservableArray<T> {
                                    type TIndex = number;
                                    added: IEvent<T, TIndex>;
                                    removed: IEvent<T, TIndex>;
                                    changed: IEvent<>;
                                  }
                                  


                                  Или я не прав и можно обойтись без конструкций вида ...TArgs?
                                    0
                                    interface Event {
                                    }
                                    
                                    interface ItemEvent < KeyType , ValueType > extends Event {
                                        key : KeyType
                                        value : ValueType
                                    }
                                    
                                    interface EventManager < EventType > {
                                        listen( handler : ( event : EventType ) => void ) : void
                                        forget( handler : ( event : EventType ) => void ) : void
                                    }
                                    
                                    interface ObservableArray < ItemType > {
                                        added : EventManager < ItemEvent < number , ItemType > >
                                        removed : EventManager < ItemEvent < number , ItemType > >
                                        changed : EventManager < Event >
                                    }
                                    
                                      0
                                      Тем самым вы заставляете бросать любое событие в виде объекта с полями, что, возможно, даже лучше. Плюс этого подхода в возможности позже добавить новые поля к уже определённым событиям, а минусы в том, что надо каждый раз доставать данные из события через точку, что поля события получили жёстко зафиксированные имена (key и value) и что каждый раз когда возникает событие надо создавать и удалять временный объект с этими полями:

                                      array.added(event => { if (event.key == "...") event.value... });
                                      


                                      Дело вкуса вообщем.
                                    0
                                    Я, как член команды DefinitelyTyped (типизации TypeScript для разных JS библиотек), могу сказать, что часто.
                                    Часто приходится описывать множеством перегрузок (конечным), то, что проще описать вариативным шаблоном.

                                    Есть библиотеки, котороые из одной (или больше) функции превращают в другую, попутно меняя входные/выходные данные по определенным правилам (оборачивают во что-то, добавляют/удаляют параметры).
                                      0
                                      Кстати говоря, далеко ходить за примером не надо: попробуйте описать функцию вроде Q.async которая берёт синхронную функцию (которая может вернуть значение или бросить исключение) и оборачивает её в «асинхронную» функцию которая возвращает промис. Я вижу этот описание как то так:

                                      interface Q {
                                        async<...TArgs, TRes>(fn: F<...TArgs, TRes>): F<...TArgs, Promise<TRes>>;
                                      }
                                      
                                      interface F<...TArgs, TRes> {
                                        (...args: ...TArgs): TRes;
                                      }
                                      


                                      Поскольку написать так сейчас нельзя, в definitely typed пришлось превратить всё в any, тем самым потеряв всю информацию о типах аргументов функций:

                                          /**
                                           * This is an experimental tool for converting a generator function into a deferred function. This has the potential of reducing nested callbacks in engines that support yield.
                                           */
                                          export function async<T>(generatorFunction: any): (...args: any[]) => Promise<T>;
                                      
                                        0
                                        Сейчас можно выкручиваться как-то так:

                                        interface Q {
                                            async < TRes > ( gen : () => TRes ) : { () : Promise<TRes> };
                                            async < TRes , T1 > ( gen : ( a1 : T1 ) => TRes ) : { ( a1 : T1 ) : Promise<TRes> };
                                            async < TRes , T1 , T2 > ( gen : ( a1 : T1 , a2 : T2 ) => TRes ) : { ( a1 : T1 , a2 : T2 ) : Promise<TRes> };
                                            async < TRes , T1 , T2 , T3 > ( gen : ( a1 : T1 , a2 : T2 , a3 : T3 ) => TRes ) : { ( a1 : T1 , a2 : T2 , a3 : T3 ) : Promise<TRes> };
                                        }
                                        
                                        
                                          0
                                          Обратите внимание на то, как удобно было бы определить интерфейс Function/F/IFunction и применить его здесь несколько раз: он бы не только уменьшил количество букв, но и позволил бы избавиться от ненужных имён переменных (a1, a2, ...) которые нужны здесь лишь потому что в TS по другому нельзя. Однако для этого TS должен поддерживать «перегрузку» шаблонов:

                                          interface F<R> { ... }
                                          interface F<T1, R> { ... }
                                          interface F<T1, T2, R> { ... }
                                          ...
                                          


                                          А лучше, конечно, написать один раз F<...T, R> :)

                                  Only users with full accounts can post comments. Log in, please.