Поводом для данной заметки стали несколько обстоятельств. Негативный опыт на одном проекте, и следующий спич в одном из докладов по ТС 2023 года:
"Так когда же использовать any? Никогда. Шучу, конечно. Если идет портирование или при разработке дженериков можно" - за точность уже отвечать не могу, но смысл примерно такой.
А так же заявления некоторых команд в духе: "У нас отличный проект. У нас нет any"

Так как относиться неискушенному разработчику к any?

Документация

Первым делом обратимся к современной документации на ТС. А имеем мы следующие:

TypeScript also has a special type, any, that you can use whenever you don’t want a particular value to cause typechecking errors.

И подобное действительно может ввести в заблуждение, что с any можно обращаться как с любым другим типом и использовать при разработке.
Потому что звучит это как: "У нас тут тип, что бы скрыть сообщения об ошибках типизации"

Пример 1.

export const anyAgainEx1 = () => {
  const A: any = 1
  const B: string = A

  const C = B.repeat(10)
}

Запустив тест мы получим подтверждение, что функция выкинет исключение с ошибкой B.repeat is not a function

Таким образом, использовать any как тип в ТС проекте нельзя, потому что основная его функция - это отключать типизацию в месте использования.

И в документации об этом написано прямым текстом. Но не в самом разделе посвященном any, а в, на мой взгляд весьма отдаленном разделе, Do's and Don'ts:

Don’t use any as a type unless you are in the process of migrating a JavaScript project to TypeScript. The compiler effectively treats any as “please turn off type checking for this thing”. It is similar to putting an @ts-ignore comment around every usage of the variable. This can be very helpful when you are first migrating a JavaScript project to TypeScript as you can set the type for stuff you haven’t migrated yet as any, but in a full TypeScript project you are disabling type checking for any parts of your program that use it.

И если мы не знаем какой тип должен быть на месте должны использовать unknown

Пример 2.

export const anyAgainEx2 = () => {
  const A: unknown = 1
  const B: string = typeof A === 'string' ? A : '1'

  return B.repeat(2)
}

Generic

Когда у нас есть ТС проект any в нем все равно использовать можно.

Пример 3.

type A<T> = { value: T }
type B<T> = T extends any ? A<T> : never
type C<T extends { value: any }> = T extends { value: infer InnerT } ? InnerT : never

type testType = string | number

type Result = {
value1: A<testType> // { value: string | number }
value2: B<testType> // { value: string } | { value: number }
value3: C<A<string> | B<number>> // string | number
}

Мы имеем два примера выразительного использования any

  • В первом случае таким образом ТС позволяет включать дистрибутивность объединения при передаче в дженерик

  • Во втором случае с помощью any мы определили форму ограничения для типа дженерика

Отключение типизации через any как рабочий вариант

Задача: Написать декоратор для функции, который подсчитывает количество вызовов

Пример 4.

export const anyAgainCounts: { [key: string]: number } = {}

const decoratorCount = function<T extends (...p: any) => any>(fn: T, desc: string): T {
  anyAgainCounts[desc] = 0

  return ((...params: any[]) => {
    anyAgainCounts[desc]++

    return fn(...params)
  }) as T
}

Ключевые моменты использования any:

  1. Задание формы для типа параметра декорируемого подсчитывающей функцией

  2. Отключение типизации, т.к. в данном случае нас вообще не интересует с какими параметрами работает декорирующая функция. О количестве параметров должна заботится декорируемая функция.

Без any пример выглядит так:

const decoratorCount2 = <F extends (...args: Parameters<F>) => ReturnType<F>>(fn: F, desc: string) => {
  anyAgainCounts[desc] = 0

  return ((...params: Parameters<F>) => {
    anyAgainCounts[desc]++

    return fn(...params)
  })
}

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

Заключение

Так что же означает фраза: "У нас на проекте нет any"?

Во-первых, это говорит о стадии проекта. Либо он изначально был на ТС, либо все операции портирования завершены.

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

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


  1. meonsou
    14.09.2024 19:26
    +1

    Мы имеем два примера выразительного использования any

    определили форму ограничения для типа дженерика

    А чего такого выразительного здесь в any? Сами же пишете что

    если мы не знаем какой тип должен быть на месте должны использовать unknown

    Почему здесь any стал лучше (или не лучше?) чем unknown? Ну и в примере с дистрибутивностью тоже можно на него заменить например.

    Написать декоратор для функции, который подсчитывает количество вызовов

    А вот так уже лучше не делать.

    Во первых, ограничив fn: T через (...p: any) => any, мы получаем возможность использовать его в функции как если бы он имел этот тип. То есть например можно вызывать переданную нам функцию fn с рандомными аргументами, потому что проверки отключены. Тут потенциал для стрельбы по ногам бесконечный.

    // 100% валидный код
    function test<T extends (...p: any) => any>(fn: T) {
      fn(1, 2, 3)
      fn("hello world")
      fn(null)
    }
    

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

    Эту функцию можно типизировать без any проще:

    function decoratorCount<A extends unknown[], R>(fn: (...args: A) => R, desc: string) {
      anyAgainCounts[desc] = 0
    
      return (...args: A) => {
        anyAgainCounts[desc]++
    
        return fn(...args)
      }
    }
    

    Здесь решены все перечисленные недостатки + корректно обрабатываются дженерики в fn.

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

    Приведу примеры немного более ясных и продуманных ситуаций.

    Всё дело обычно во вариантности. При написании ограничений часто нужен самый общий тип, и если этот тип параметризован то в ковариантных позициях ставится unknown (или максимально допустимый тип), а в контравариантных never. В этих случаях можно обходиться без any, но это может быть затруднено 1) слишком сложными ограничениями (много букв) 2) сложностью определения вариантности. Ну а если параметр инвариантный то выбора нету совсем.

    Пример:

    // Тип с инвариантным параметром T
    type A<in out T> = (arg: T) => T
    
    // Тип который принимает любой A и делает с ним что-то
    type B<T extends A<any>> = [T]
    

    Тут стоит иметь в виду что можно например применить B<(arg: 1) => 2>, при том что тип (arg: 1) => 2 на самом деле невозможно получить с помощью A и значение такого типа невозможно присвоить в A<_>, за исключением A<any>. Поэтому при расстановке any в ограничения тоже следует быть осторожным.

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

    Тайпгард имеет вид:

    type TypeGuard<in A, out R extends A> = (a: A) => a is R
    

    То есть мы не можем просто так обобщить его через TypeGuard<never, unknown>, так как R должен быть не шире A. Приходится использовать any.

    type IsTypeGuard<T> = T extends TypeGuard<any, any> ? true : false
    
    type t1 = IsTypeGuard<(arg: 1 | 2) => 1>
    //   ^? type t1 = false
    type t2 = IsTypeGuard<(arg: 1 | 2) => arg is 1>
    //   ^? type t2 = true
    

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


    1. Vitaly_js Автор
      14.09.2024 19:26

      "А чего такого выразительного здесь в any? Сами же пишете что"

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

      А второй момент, использование any для дистрибутивности объединений - это официальный подход описанный в документации.

      Основной "косяк" any при использовании в дженериках не работает. Поэтому, думаю, его так "легко" и используют в дженериках и документации.

      "Во первых, ограничив fn: T через (...p: any) => any, мы получаем возможность использовать его в функции как если бы он имел этот тип. ... "

      нет, не получим. Потому что "any" мы приводим к T. Поэтому, получается совершенно безопасная реализация не загроможденная всякими служебными типами, которые нам все равно не нужны. И ваши примеры будут выдавать ошибки.

      "Во вторых, мы потеряли важный документирующий аспект типов. Когда человек видит что аргументы одной функции определены через аргументы другой функции например, он понимает что тут есть какая-то логическая связь. Не говоря уже о компиляторе."-

      давайте посмотрим, что видит разработчик.

      Я использовал распространенный шаблон для описания типа функции с любыми параметрами и любым возвращаемым значением. И этот шаблон использован на месте параметра. Семантически все верно. Любая функция может быть парметром декоратора. Поэтому все что я хотел показать отражено в типе. А связь о которой вы говорите должна быть отражена в имени декоратора.

      "Эту функцию можно типизировать без any проще:"

      вот как раз, что бы такой ерундой не страдать `function decoratorCount<A extends unknown[], R>(fn: (...args: A) => R, desc: string)` я и использовал распространенный тип функции в смысле описанном выше как ограничение, и T на месте параметра. Мой пример легко читается и легко понимается. А вот этот пример, на мой взгляд, очень невыразительный. Плюс, оставляет место для дискуссии, почему не использовать пример из моей статьи вместо вашего (там где нет any)? Что бы все это убрать, я и использовал any

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

      Сам я выбирал примеры, которые с одной стороны распространены, а с другой используются в документации. Поэтому мои примеры легко использовать в проекте. Тот же декоратор можно использовать для каких-нибудь дебоунсов и тротлингов. Ваши примеры я раскусить не смог. Сам я такие штуки еще ни разу не видел, но с документацией ознакомился: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html#optional-variance-annotations-for-type-parameters, поэтому спасибо узнал что-то новое. Но, если есть возможность я бы хотел увидеть, что-то менее синтетическое.


      1. meonsou
        14.09.2024 19:26
        +1

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

        Не понял что за "тип на месте". Вы пишете что T extends { value: any } это пример "выразительного использования" any. Я просто поинтересовался чем именно он выразителен в сравнении, например, с unknown. Ни о каком приведении речи не шло.

        нет, не получим. Потому что "any" мы приводим к T. Поэтому, получается совершенно безопасная реализация не загроможденная всякими служебными типами, которые нам все равно не нужны. И ваши примеры будут выдавать ошибки.

        Ну вот же, вы бы проверили хоть.

        А связь о которой вы говорите должна быть отражена в имени декоратора.

        Ну и почему она не должна быть отражена в типе? Я тоже могу заявить что const x: number писать излишне, можно же всё отразить в имени const number_x: any.

        Мой пример легко читается и легко понимается.

        А ещё легко ломается, см. плейграунд выше. Мало того что any внутри функции, так ещё и as.

        Плюс, оставляет место для дискуссии, почему не использовать пример из моей статьи вместо вашего (там где нет any)?

        1. Потому что он проще и букв меньше

        2. Потому что он, например, сохраняет дженерики в переданной функции (я писал об этом)

        Ну и в целом странная аргументация. А места для дискуссии почему any вместо нормальных типов не остаётся значит?

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

        Я привёл примеры где any действительно может быть нужен и его проблематично/невозможно заменить.

        Но, если есть возможность я бы хотел увидеть, что-то менее синтетическое.

        Примеры не слишком распространённые как раз потому что ситуация где действительно необходим any не распространённая. Примеры либо синтетические, либо километровые, но в любом случае достаточно сложные. В простых случаях уровня T extends { value: any } я не вижу смысла использовать any вместо never/unknown, потому что 1) это и так ничего не стоит 2) any всё ещё может попасть по ногам. Лучше отучаться от его использования и знать где он действительно нужен (такое бывает редко).