В жизни каждого разработчика на TypeScript наступает момент, когда ему хочется рвать все связи с типом any. А ведь по началу any казался таким милым! Сделай переменной аннотацию типа any и используй любое свойство и метод этой переменной так, как привык работать в JavaScript. Никаких тебе ошибок, все чинно и спокойно, по-старому.

Документация TypeScript оправдывает использование any только на время переноса кодовой базы из JavaScript в TypeScript, но считает постыдным его использование в полноценном проекте. Казалось бы, все хорошо, только в описании типов библиотечных функций самого TypeScript аннотации any встречаются. Очень полезный `JSON.parse`, один из таких примеров.

// из lib.es5.d.ts
interface JSON {
    parse(text: string, reviver?: (this: any, key: string, value: any) => any): any;
    stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string;
    stringify(value: any, replacer?: (number | string)[] | null, space?: string | number): string;
}

Выйти из положения позволяет умение TypeScript объединять описания интерфейсов. Давайте разберемся как это можно исправить библиотечное описание интерфейса JSON.

Использование слияния интерфейсов и перегрузки методов
interface JSON {
    parse(
        text: string,
        reviver?: (this: unknown, key: string, value: unknown) => unknown,
    ): unknown;
}

Песочница

Слияние деклараций интерфейсов

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

В простейшем случае описания просто добавляются. Следующий пример показывает счастливый TypeScript.

interface User {
    name: string;
}

interface User {
    memberSince: number;
}

const user: User = {
    memberSince: 2022,
    name: 'author'
}

Песочница

В первом варианте описания интерфейс User содержит одно поле `name`, во втором варианте тоже одно поле, но с другим именем. TypeScript объединил оба описания в одно. Все ожидаемо.

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

interface User {
    name: string;
    memberSince: number;
}

interface User {
    memberSince: number;
}

const user: User = {
    memberSince: 2022,
    name: 'author'
}

Песочница

Если бы имена полей были одинаковыми, но разных вариантах поля имели разные типы, то TypeScript высказал бы свое недовольство кодом ошибки (2717). Как это видно в следующей песочнице.

interface User {
    name: string;
    memberSince: string;// пока еще все хорошо
}

interface User {
    memberSince: number;//ошибка
}

const user: User = {
    memberSince: 2022, // ошибка
    name: 'author'
}

Песочница

Как работает перегрузка функций в TypeScript

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

function getYear():number;
function getYear(ticks: number): number;
function getYear(iso:string):number;
//скрытая от вызывающей стороны реализация
function getYear(arg?: undefined| string|number):number{
    if(typeof arg === 'undefined'){
        return 2022;
    }
    return new Date(arg).getFullYear();
}

Песочница

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

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

interface User{
    login(token:string):void;
    login(name: string, password: string):void;
    login():void;
}

declare const user:User;

//вариант 1
user.login("--secret--");
//вариант 2
user.login("admin","pa$5w||d");
//вариант 3
user.login();

Песочница

Мы обратим дополнительное внимание на то, что имена методов повторяются. Более того, при слиянии интерфейсов у TypeScript нет требования уникальности имени метода.

Этим мы и воспользуемся для решения вопроса с улучшенной типизацией метода parse интерфейса JSON

Слияние с библиотечным интерфейсом

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

interface JSON {
    parse(
        text: string,
        reviver?: (this: unknown, key: string, value: unknown) => unknown,
    ): unknown;
}
const x = JSON.parse("0", function ():unknown {
    console.log(this);// TypeScript подсказывает, что this: unknown
    return 42;
});

console.log(x) // TypeScript подсказывает, что x:unknown

Теперь мы можем не опасаться что TypeScript перестанет нам помогать в борьбе за качественное ПО.

Комментарии (12)


  1. i360u
    28.02.2022 16:02
    -5

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


    1. Devoter
      28.02.2022 17:37
      +5

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

      P.S.: Минус не мой.


      1. i360u
        01.03.2022 10:14

        Ответил ниже, но вам тоже напишу. Я совсем не новичок в TS, и конечно знаю про unknown и все рекомендации. Немного раскрою про рантайм. Обычно на этом месте приводят пример с данными с сервера, но, как мне кажется, с сервером все проще: данные, скорее всего, будут нормализованы и описать их типы не представляет проблем. Приведу другой пример. Представьте, что вы делаете встраиваемый виджет, в логике которого вы планируете получить коллекцию DOM-элементов снаружи вашего решения (из хост-приложения). А там, внезапно, кастомные элементы, о типах которых, вы не знаете вообще ничего.


    1. iliazeus
      28.02.2022 20:07
      +3

      const arr = [];
      // через n строк; массив заполнен
      arr[0].foo.bar;
      // cannot get property bar of undefined

      В проектах, над которыми я работал, ребятам тоже было лень поставить аннотацию. А потом все вместе весело ловили вот такие баги, как выше, вместо ошибок компиляции.


      1. Devoter
        01.03.2022 09:29
        +2

        К сожалению, разработчики TS при всех успехах упорно не желают делать даже опциональную поддержку глубокого вывода типов, мол, все будет безумно медленно. Лично я хотел бы иметь строгий режим для компилятора с глубоким выводом, да и работает же как-то в других языках, просто у них там странная философия своя. Из-за этого типы в TS не совсем полноценны даже в compile-time, что прискорбно.


        1. iliazeus
          01.03.2022 10:21

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


      1. i360u
        01.03.2022 09:40
        +2

        Во первых, чем принципиально отличаются ошибки рантайма от ошибок компиляции? Тесты писать не надо?

        Во вторых, вы привели уж совсем детский кейс, где any использовать либо глупо, либо вопрос можно решить за счет conditional chaining, либо использовать unknown или проброс типов.

        В третьих, раз уж вы привели пример с коллекциями, посмотрите как в TS реализованы декларации для работы, например, с DOM API (стандартный dom.d.ts). Есть стандартный querySelectorAll, где описаны возвращаемые типы через маппинг имен тегов к конструкторам элементов. Вот только согласно W3C стандартам, селекторы могут не содержать имен тегов, а сами теги, внезапно, могут быть экземплярами CustomElements. Для всех неопознанных типов, возвращается тип Element, который, как вы понимаете, совсем не то, что мы можем ожидать. И вот представьте, что вы разработчик библиотек для работы с DOM API, и вам предстоит бороться с такой несогласованностью стандартов и реализаций. Дженерики и unknown работают не всегда и не всегда так просто, как это требуется для решения какой-то частной задачи.

        В четвертых, есть еще и типы рантайма.


        1. iliazeus
          01.03.2022 10:19
          +1

          Во первых, чем принципиально отличаются ошибки рантайма от ошибок компиляции?

          В данном случае - местом возникновения. Ошибка типов времени компиляции намного ближе к месту настоящей ошибки программиста.

          В третьих, раз уж вы привели пример с коллекциями, посмотрите как в TS
          реализованы декларации для работы, например, с DOM API (стандартный
          dom.d.ts).

          За работой команды TS слежу не слишком плотно, но точно видел активную работу над тем, чтобы можно было без костылей подключать свои тайпинги для DOM. Но, конечно, жалко, что при дизайне DOM API не задумывались о вопросах типизации, а думали только про JS и (на совсем ранних этапах) про Java, в которой тогда ещё не было дженериков.

          В целом, моя позиция такова: any - это крайний случай, когда уж совсем ничего другого нормально не написать. Он имеет слишком неприятное свойство "заражать" код (большинство операций с any дают any на выходе), и делает систему типов неконсистентной (он одновременно и top, и bottom, грубо говоря).


          1. i360u
            01.03.2022 14:43

            Но, конечно, жалко, что при дизайне DOM API не задумывались о вопросах типизации

            Вы как-то поставили "вагон впереди паровоза". Об этом разработчики TS должны были задуматься в первую очередь. С системой типов в JS проблемы то нет.


  1. Devoter
    28.02.2022 17:40

    Стоило добавить, что документация TS рекомендует использовать объединения типов и значения по умолчанию для функций и методов вместо перегрузки.