Pull to refresh

Comments 39

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

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».
Но при этом есть гарантия, что переменной можно присвоить только указанные вами типы, с any там что угодно может оказаться. Да проверки все равно нужны, но не ради сокращения проверок думаю это сделано.
Осталось сделать, чтобы типы по умолчанию были non-nullable и будет совсем сказка :-)
А в местах, где нужен nullable тип — просто объединять тип с Null.

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

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

function push2<T>(data: T[], a: T, b: T) {
    data.push(a);
    data.push(b);
}

push2([1], true, "test"); // работает!
[1].push(true); // ошибка типизации
На счет этого понятно
[1].push(true); // ошибка типизации

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

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

тоже не должно работать. Вот пытаюсь понять кто прав и как же на самом деле происходит вывод типов.
Разумеется не должно. В нижнем случае получается, что типы не проверяются вообще никак. Если бы это было желаемым результатом, зачем вообще использовать обобщение? Проще было бы написать обычную функцию и типы аргументов не указывать, неявно подразумевая any.
На счет желаемого результата согласен. В большинстве случаев это выглядит удобно. Однако вопрос в том, почему так происходит. Либо в typescript нет общего базового типа, либо вывод типов останавливается где-то посередине по непонятным правилам. Какой вариант правильный?
В 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 ) { }

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

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

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()
}
Тут проблама с ковариантностью/контравариантностью.
Т.к. 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.
Опрос какой-то странный. Если CoffeeScript/LiveScript еще можно считать препроцессорами, то ClojureScript/PureScript/GHCJS/ScalaJS?
UFO just landed and posted this here
Эммм, а что с Dart не так?
UFO just landed and posted this here
Как минимум тяжелый runtime, который он тащит за собой в ваш код?
Не такой уж и тяжелый, в самом-то деле — в целом не критично. Dart все-таки больше предназначен для написания сколь-нибудь сложных приложений, а не статических страниц с парой скриптов на них.
1. Выводить типы умеет только в самых тривиальных случаях.
2. Исполняется в отдельной виртуальной машине. Рантайм из dart2js — это практически порт dart-vm на js.
3. Сложности интеграции с существующим JS кодом.
4. Кривые SourceMaps и прочие проблемы дебага транслированного в js кода.
Еще теперь есть flow. А coffee — препроцессор, но вообще в другую степь. Стоит добавить в опрос остальные языки, расширяющие синтаксис для написания типизированного кода.
Что это за ТруСкрипт такой? :-)
Когда будут variadic templates как в C++? Скажем хочется описать интерфейс функции у которой есть несколько дополнительных свойств:

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


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

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

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


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

Есть библиотеки, котороые из одной (или больше) функции превращают в другую, попутно меняя входные/выходные данные по определенным правилам (оборачивают во что-то, добавляют/удаляют параметры).
Кстати говоря, далеко ходить за примером не надо: попробуйте описать функцию вроде 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>;
Сейчас можно выкручиваться как-то так:

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> };
}

Обратите внимание на то, как удобно было бы определить интерфейс Function/F/IFunction и применить его здесь несколько раз: он бы не только уменьшил количество букв, но и позволил бы избавиться от ненужных имён переменных (a1, a2, ...) которые нужны здесь лишь потому что в TS по другому нельзя. Однако для этого TS должен поддерживать «перегрузку» шаблонов:

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


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

Articles