Поводом для данной заметки стали несколько обстоятельств. Негативный опыт на одном проекте, и следующий спич в одном из докладов по ТС 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:
Задание формы для типа параметра декорируемого подсчитывающей функцией
Отключение типизации, т.к. в данном случае нас вообще не интересует с какими параметрами работает декорирующая функция. О количестве параметров должна заботится декорируемая функция.
Без 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
просто так, то точно имеет смысл ознакомиться с особенностями из этой заметки.
meonsou
А чего такого выразительного здесь в
any
? Сами же пишете чтоПочему здесь
any
стал лучше (или не лучше?) чемunknown
? Ну и в примере с дистрибутивностью тоже можно на него заменить например.А вот так уже лучше не делать.
Во первых, ограничив
fn: T
через(...p: any) => any
, мы получаем возможность использовать его в функции как если бы он имел этот тип. То есть например можно вызывать переданную нам функциюfn
с рандомными аргументами, потому что проверки отключены. Тут потенциал для стрельбы по ногам бесконечный.Во вторых, мы потеряли важный документирующий аспект типов. Когда человек видит что аргументы одной функции определены через аргументы другой функции например, он понимает что тут есть какая-то логическая связь. Не говоря уже о компиляторе.
Эту функцию можно типизировать без
any
проще:Здесь решены все перечисленные недостатки + корректно обрабатываются дженерики в
fn
.Приведу примеры немного более ясных и продуманных ситуаций.
Всё дело обычно во вариантности. При написании ограничений часто нужен самый общий тип, и если этот тип параметризован то в ковариантных позициях ставится
unknown
(или максимально допустимый тип), а в контравариантныхnever
. В этих случаях можно обходиться безany
, но это может быть затруднено 1) слишком сложными ограничениями (много букв) 2) сложностью определения вариантности. Ну а если параметр инвариантный то выбора нету совсем.Пример:
Тут стоит иметь в виду что можно например применить
B<(arg: 1) => 2>
, при том что тип(arg: 1) => 2
на самом деле невозможно получить с помощьюA
и значение такого типа невозможно присвоить вA<_>
, за исключениемA<any>
. Поэтому при расстановкеany
в ограничения тоже следует быть осторожным.Ещё из интересных применений можно выделить например тайпгарды. Попробуем проверить что тип является тайпгардом, для простоты ограничимся одним параметром.
Тайпгард имеет вид:
То есть мы не можем просто так обобщить его через
TypeGuard<never, unknown>
, так какR
должен быть не ширеA
. Приходится использоватьany
.Это же будет актуально для других типов с подобными ограничениями.
Vitaly_js Автор
то, что я написал относится к использованию any как тип на месте. К дженерикам это не относится. Там any работает как супер-тип, поэтому любой тип может его расширять. Т.е. речь идет об отношении типов, а не типе на месте. В дженериках не нужно делать никакого приведения к типу.
А второй момент, использование any для дистрибутивности объединений - это официальный подход описанный в документации.
Основной "косяк" any при использовании в дженериках не работает. Поэтому, думаю, его так "легко" и используют в дженериках и документации.
нет, не получим. Потому что "any" мы приводим к T. Поэтому, получается совершенно безопасная реализация не загроможденная всякими служебными типами, которые нам все равно не нужны. И ваши примеры будут выдавать ошибки.
давайте посмотрим, что видит разработчик.
Я использовал распространенный шаблон для описания типа функции с любыми параметрами и любым возвращаемым значением. И этот шаблон использован на месте параметра. Семантически все верно. Любая функция может быть парметром декоратора. Поэтому все что я хотел показать отражено в типе. А связь о которой вы говорите должна быть отражена в имени декоратора.
вот как раз, что бы такой ерундой не страдать `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, поэтому спасибо узнал что-то новое. Но, если есть возможность я бы хотел увидеть, что-то менее синтетическое.
meonsou
Не понял что за "тип на месте". Вы пишете что
T extends { value: any }
это пример "выразительного использования"any
. Я просто поинтересовался чем именно он выразителен в сравнении, например, сunknown
. Ни о каком приведении речи не шло.Ну вот же, вы бы проверили хоть.
Ну и почему она не должна быть отражена в типе? Я тоже могу заявить что
const x: number
писать излишне, можно же всё отразить в имениconst number_x: any
.А ещё легко ломается, см. плейграунд выше. Мало того что
any
внутри функции, так ещё иas
.Потому что он проще и букв меньше
Потому что он, например, сохраняет дженерики в переданной функции (я писал об этом)
Ну и в целом странная аргументация. А места для дискуссии почему
any
вместо нормальных типов не остаётся значит?Я привёл примеры где
any
действительно может быть нужен и его проблематично/невозможно заменить.Примеры не слишком распространённые как раз потому что ситуация где действительно необходим
any
не распространённая. Примеры либо синтетические, либо километровые, но в любом случае достаточно сложные. В простых случаях уровняT extends { value: any }
я не вижу смысла использоватьany
вместоnever
/unknown
, потому что 1) это и так ничего не стоит 2)any
всё ещё может попасть по ногам. Лучше отучаться от его использования и знать где он действительно нужен (такое бывает редко).