[Разработка веб-сайтов, JavaScript, TypeScript] TypeScript: Раскладываем tsconfig по полочкам. Часть 2
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В прошлой статье я рассказал о различных особенностях некоторых общих настроек TypeScript. В данной статье речь пойдёт о так называемых «флагах строгости».На самом деле, TypeScript из коробки мало чем отличается от JavaScript. Поэтому если изначально не подтюнить конфиг проекта, то большая часть преимуществ языка не будет задействована. Смысл использования TypeScript в таком виде есть, но небольшой.Если выразить смысл всей статьи в одном предложении, то текст можно свести к следующему: откройте свой tsconfig.json, установите флаг strict в секции compilerOptions в значение true. Без режима строгости использование TypeScript это практически пустая трата времени. Однако цель предыдущей и данной статей заключается не в том, чтобы сказать как «правильно» делать, а прояснить некоторые тонкости конфигурации. Поэтому погрузимся в детали данного вопроса.В официальном референсе tsconfig.json его многочисленные опции поделены на секции. Из них две секции: Strict Checks и Linter Checks – содержат только опции тех самых флагов строгости. Помимо ещё часть интересующих нас сегодня флагов сокрыта в самой большой группе опций Advanced.Группа Strict ChecksПожалуй, флаги данной категории наиболее важные из всех. Вот они: strict, alwaysStrict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, strictBindCallApply. Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение – true.Первое, что здесь может броситься в глаза, глядя на список опций, это наличие флагов с похожими именами, которые сбивают с толку - strict и alwaysStrict. На самом деле флаги выполняют совершенно разные функции.alwaysStrictРекомендован: всегда / сложность: легко.Флаг alwaysStrict включает добавление строки "use strict" в каждый скомпилированный файл. Другими словами, alwaysStrict включает строгий режим JavaScript и никак не связан с проверкой типов TypeScript.strictРекомендован: всегда / сложность: сложно / может быть заменён набором других флагов.Флаг strict напрямую связан с проверкой типов. Его включение автоматически активирует абсолютно все флаги секции Strict Checks, включая и alwaysStrict. Это именно то, о чём я говорил в самом начале.У такого подхода есть как минимум один недостаток – неочевидность. Устанавливая strict: true, нет наглядного представления, какие именно проверки включены и какие опции вообще существуют. Для проектов, которые с самого начала пишутся на TypeScript это не так принципиально, как для проектов, которые поэтапно портируются с JavaScript.В процессе портирования существующего приложения нет возможности сразу включить все проверки. Приходится активировать их по одной шаг за шагом. Иногда даже случаются сложности, из-за которых приходится откатывать ранее установленные флаги обратно в false.Есть небольшая особенность работы флага strict – список подконтрольных ему флагов может пополняться по мере выхода новых версий TypeScript. Подобные моменты если случаются, то редко и всегда освещаются в release notes если, конечно, вы их читаете перед обновлением версии.Лично я предпочитаю указывать список флагов явным образом:
{
"compilerOptions": {
"alwaysStrict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"strictBindCallApply": true,
}
}
Рассмотрим, что делает каждый из флагов и обсудим какие из них стоит включить в первую очередь, если активировать сразу все не получается.noImplicitAnyРекомендую: всегда / сложность: среднеЭто первый флаг, который необходимо активировать в начале работы над проектом с TypeScript.Для начала несколько слов про any. Это специальный тип, который назначается всем переменным, если их тип не был задан явно и не может быть выведен компилятором автоматически. Данный тип создан для обратной совместимости с JavaScript. С точки зрения TypeScript, все переменные в JavaScript это any, поскольку в нём нет системы типов.Для компилятора тип any означает «это может быть что угодно, передаю управление и ответственность разработчику». Так как компилятор не знает тип переменной, с такой переменной можно делать всё что угодно. В этом месте TypeScript полностью лишён своей силы:
// тип задан явно
let a: number = 5
// тип выведен
let b = 'hello'
// тип не указан и не может быть выведен
// value будет неявно объявлено как any
function someFunction (value) {
// поэтому ошибка в этой строке останется незамеченной
console.log(value.subtr(3))
}
Иначе говоря, если тип переменной не определён, значит он any, значит это JavaScript, а не TypeScript код. Т. е. количество неопределённых типов в коде, является реальным показателем отношения TypeScript кода к JavaScript коду. Флаг noImplicitAny подсвечивает все такие места, для того чтобы разработчик не забывал указывать типы.Однако это не означает, что теперь вовсе нельзя использовать any. Это означает лишь, что в подобных ситуация разработчик должен явно (implicit) писать any, если по каким-то причинам не получается правильно описать тип переменной или если указание типа переменной приводит к ошибкам в другом участке кода.
// явно помечаем код как потенциально не безопасный через any
function someFunction (value: any) {
console.log(value.subtr(3))
}
Явное присутствие any напоминает разработчику о том, что код не доработан и эти места должны быть улучшены со временем. В этом заключается суть флага noImplicitAny.Дополнительно усилить эффект можно включив ESLint правило no-explicit-any. Правило будет отмечать места с any как warning, каждый раз привлекая внимание разработчика.strictNullChecksРекомендую: всегда / сложность: среднеФлаг активирует одну из самых мощных особенностей TypeScript. Рекомендую включить его сразу после noImplicitAny.JavaScript содержит два нижних значения – undefined и null, для которых в TypeScript есть одноимённые типы. По иерархии считается, что все остальные типы происходят от них. Таким образом по принципу наследования вместо переменной любого типа: string, boolean, number и т. д – можно передать значение undefined или null:
function someFunction (value: number) {
// можно получить неожиданный результат если value будет undefined или null
return value * 2
}
someFunction(5)
// по умолчанию можно так
someFunction(null)
// и можно так
someFunction(undefined)
Подобное поведение справедливо для почти всех (если не всех) языков программирования. На практике это может вызывать неудобства в виде написания дополнительных проверок на null (undefined). Но гораздо большей проблемой является помнить о необходимости покрывать код данными проверками. Думаю, Java разработчики с их NullPointerException как никто другой понимают эту проблему.Включение опции strictNullChecks меняет правила игры. С данным флагом компилятор больше не позволяет передавать undefined или null туда, где ожидаются переменные других типов, если это не разрешено явно. Рассмотрим пример:
function someFunction (value: number) {
// value всегда будет только number
return value * 2
}
someFunction(5)
// следующие вызовы функции невозможны
someFunction(null)
someFunction(undefined)
Это позволяет не обрабатывать ситуации с undefined и null в тех участках кода, где возникновение данных значений невозможно. При этом в тем местах, где возникновение подобных ситуаций возможно, компилятор не позволит забыть о проверках. Это очень сильный механизм, существование которого сложно переоценить.
// Символ «?» разрешает undefined, а «| null» - null
function someFunction (value?: number | null) {
if (value == null) {
return 0
}
return value * 2
}
Есть только один потенциально опасный момент. В нашем статически типизированном коде всё будет работать как часы. Однако приложения часто работают с динамически получаемыми данными (например, данными от сервера), типы которых могут быть описаны одним образом, а на практике всё может быть совсем иначе. Другими словами, в типах описано, что некоторое значение не может быть null, а сервер его пришлёт. Избежать подобной ситуации можно введя практику проверять ответы от сервера. Например, с помощью json-схем валидаторов.Чуть более коварной может быть ситуация с внешними библиотеками. В частности, с теми, который изначально написаны на JavaScript и тайпинги для них существуют отдельно от кода. В таких случаях потенциальные несовпадения реального кода с типами могут привести к возникновению вышеописанной ситуации.Здесь можно найти таблицу совместимости типов с включенным и выключенным режимом strictNullChecks – Any, unknown, object, void, undefined, null and never assignability.strictPropertyInitializationРекомендую: всегда / сложность: легко / связан с strictNullChecksФлаг strictPropertyInitialization следит, чтобы объявленные свойства класса всегда были инициализированы:
class User {
name: string
// email не инициализирован ни здесь, ни в конструкторе
// компилятор подскажет, что нужно установить значение
email: string
constructor (name: string) {
this.name = name
}
}
Опция является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен.strictFunctionTypesРекомендую: всегда / сложность: легкоУстановка флага strictFunctionTypes: true включает более строгую проверку сигнатур функций. В целом, данная опция выглядит достаточно обязательной для использования:
interface StringOrNumberFunc {
(value: string | number): void
}
function someFunction (value: string) {
console.log(value)
}
// сигнатуры не совпадают
// string | number не эквивалентны string
let func: StringOrNumberFunc = someFunction
func(10)
func('10')
noImplicitThisРекомендую: всегда / сложность: легкоПри использовании this проверяет, что контекст выполнения известен. Рассмотрим такой пример:
class SomeClass {
multiplier: number = 5
createSomeFunction (value: number) {
return function () {
// контекст потерян - здесь this НЕ является объектом класса SomeClass
return value * this.multiplier
}
}
}
Объект this не известен, так как function не пробрасывает контекст автоматически. В данном случае это можно исправить, заменив function на arrow function.Рассмотрим пример независимой функции, которая по каким-то причинам должна использовать внешний this:
function sayHello (name: string) {
console.log(this.helloWord + ' ' + name)
}
В данном случае this также неизвестен. Выполнить функцию с внешним this можно с помощью bind, call, apply. А научить функцию понимать контекст можно следующим образом:
// укажем тип this первым «аргументом»
// фактически это не будет считаться аргументом функции
function sayHello (this: HelloStyle, name: string) {
console.log(this.helloWord + ' ' + name)
}
// объявим варианты приветствий
interface HelloStyle {
helloWord: string
}
class HawaiiStyle implements HelloStyle {
helloWord = 'Aloha'
}
class RussianStyle implements HelloStyle {
helloWord = 'Привет,'
}
// теперь вызовем
sayHello.bind(new HawaiiStyle())('World')
sayHello.call(new RussianStyle(), 'World')
sayHello.apply(new RussianStyle(), ['World'])
Во всех случаях тип контекста известен и даже тайпхинтинг работает как следует.strictBindCallApplyРекомендую: всегда / сложность: среднеФлаг strictBindCallApply – включает более строгую проверку сигнатур при использовании соответствующих методов: bind, call, apply.
function someFunction (value: string) {
console.log(value)
}
someFunction.call(undefined, '10')
// да, для таких случаев нужен отдельный флаг
someFunction.call(undefined, false)
Кроме некоторых специфичных ситуаций при работе с данным флагом проблем не возникает. В случае возникновения сложных ситуаций рекомендую выключить проверку в месте возникновения проблемы через комментарий // @ts-ignore вместо полного отключения правила.Подведение итогов Strict Checks
- здесь действительно все флаги очень полезные. С ними TypeScript раскрывает свой потенциал и приносит максимум пользы
- все флаги можно включить разом через strict: true или каждый по отдельности. Второй вариант отлично подходит для смешанных кодовых баз, где часть кода ещё не переведена на TypeScript.
- alwaysStrict не про строгий режим TypeScript, а про строгий режим JavaScript. Т. е. про "use strict".
- флаги первоочерёдной важности это noImplicitAny и strictNullChecks в сочетании с strictPropertyInitialization. Затем идут strictFunctionTypes и noImplicitThis. А напоследок strictBindCallApply.
Группа Linter ChecksНазвание группы говорит само за себя – подразумевается, что флаги этой группы проверяют не соответствие типов, а качество кода. Своего рода правила как в ESLint. И действительно некоторые правила секции Linter Checks можно заменить на аналогичные правила ESLint. Полный список флагов данной секции:
{
"compilerOptions": {
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
}
}
Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение – true.Я неспроста отделил флаги noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess пустой строкой от других флагов группы. Дело в том, что они находятся здесь только по причине того, что концептуально не могут располагаться в группе Strict Checks. По задумке в этой секции находятся флаги, которые подконтрольны опции strict.Но эти флаги слишком специфичные для того, чтобы включать их по умолчанию. Практически так и сказано в анонсах noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess. Однако это не делает их правилами линтинга, они проверяют типы и по факту относятся к Strict Checks, только не включаются автоматически при включении флага strict.Остальные же флаги группы полноправно находятся в этой секции и имеют аналогичные правила в ESLint. В некоторых случаях с временными ограничениями, так как ещё не все правила перенесены в ESLint из TSLint. Соответствия правил можно посмотреть на странице TSLint Migration Guide и в репозитории ESLint rules for TSLint.Рассмотрим работу каждого из флагов.noPropertyAccessFromIndexSignatureРекомендую: всегда / сложность: легко / есть нюанс / нельзя заменить линтеромФлаг noPropertyAccessFromIndexSignature запрещает обращаться к свойствам объекта через точку aka dot notation, если свойства объекта описаны не явно, а через произвольные параметры (aka arbitrarily-named properties, index signatures).
interface User {
// явно указанные параметры
login: string
email: string
// произвольные параметры
[key: string]: string
}
const user: User = {
login: 'hello',
email: 'hello@example.com'
}
// c noPropertyAccessFromIndexSignature: true
// данная строка приведёт к ошибке
const username = user.name
// но так обращаться всё ещё можно
const username2 = user['name']
Вместо точечной нотации мы используем скобочную нотацию aka bracket notation. Идея флага примерно такая же как у noImplicitAny - привлечь внимание к потенциально проблемным местам.Единственной причиной не использовать данную опцию – наличие двух видов синтаксиса для обращения к свойствам объекта. Однако на мой взгляд в этом нет проблемы так как хоть синтаксис и разный, у каждого из них есть своё назначение. Всегда используем dot notation и, если случается исключение, переходим на скобочную нотацию. При этом код становится безопаснее. Поэтому я рекомендую использовать этот флаг.На самом деле есть ещё одна причина, но она решается флагом noUncheckedIndexedAccess.noUncheckedIndexedAccessРекомендую: по ситуации / сложность: средне / есть серьёзный нюанс / связан с strictNullChecks / нельзя заменить линтеромЕщё раз взглянем на предыдущий пример.
interface User {
// явно указанные параметры
login: string
email: string
// произвольные параметры
[key: string]: string
}
const user: User = {
login: 'hello',
email: 'hello@example.com'
}
// Тип переменной username - string
const username = user['name']
Как мы видим, тип переменной username является string. И это не совсем правильно так как мы можем обратиться к любому свойству, даже если его не существует. Можно описывать произвольные поля следующим образом: [key: string]: string | undefined. Это решит проблему, но добавит ручной работы. Плюс, из-за человеческого фактора можно периодически забывать дописывать undefined.Флаг noUncheckedIndexedAccess сделает эту работу за нас. Теперь компилятор не позволит использовать полученные таким образом переменные без предварительной проверки.Рассмотрим ещё один пример. На этот раз с массивом:
const strings: string[] = ['hello']
// здесь переменная number будет иметь тип string
const number = strings[100]
Однако очевидно, что в данном случае переменная будет undefined. С включенным noUncheckedIndexedAccess в подобных ситуациях тип переменной будет определяться как string | undefined.Казалось бы, мы подчеркнули скобочной нотацией «магические» свойства объекта, мы можем использовать их только после проверок на undefined – всё красиво и безопасно. Что может пойти не так?Если кратко:
const strings: string[] = ['hello']
// в данном случае переменная number также будет иметь тип string | undefined
const number = strings[0]
Или другой пример:
function upperCaseAll(strings: string[]) {
for (let i = 0; i < strings.length; i++) {
// здесь будет ошибка компиляции
console.log(strings[i].toUpperCase());
}
}
Другими словами, при данной конфигурации в обмен на безопасность в некоторых случаях придётся платить проверками, которые не несут смысла только для того, чтобы укротить строптивый компилятор. Выглядит это крайне костыльно.Определённо код с noUncheckedIndexedAccess становится значительно безопаснее, а это главная цель использования TypeScript. Однако компромисс может выглядеть достаточно серьёзным, поэтому я не могу рекомендовать флаг всем разработчикам.Флаг является своего рода дополнением к флагу strictNullChecks поэтому работает только когда второй флаг тоже включен.noImplicitReturnsРекомендую: всегда / сложность: легко / правило ESLint: consistent-return, пока не реализованоФлаг проверяет, чтобы все ветки функции возвращали значение:
function lookupHeadphonesManufacturer(color: 'blue' | 'black'): string {
if (color === 'blue') {
return 'beats'
}
// забыли return
}
Аналогично правилу ESLint consistent-return, которое в данный момент ещё не реализовано для TypeScript.noFallthroughCasesInSwitchРекомендую: если не используется ESLint / сложность: легко / правило ESLint: no-fallthroughФлаг проверяет наличие break в операторе switch/case:
switch (value) {
case 0:
console.log('even')
// забыли break
case 1:
console.log('odd')
break
}
Заменяется правилом no-fallthrough.noUnusedLocalsРекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-varsКод проверяется на наличие неиспользуемых переменных:
function createKeyboard (modelID: number) {
// объявлена неиспользуемая переменная
const defaultModelID = 23
return {
type: 'keyboard',
modelID
}
}
Данное правило крайне неудобно при использовании в процессе разработки. В процессе написания кода он может находится в «разобранном» состоянии, а наличие неиспользуемых переменных будет вызывать ошибку компиляции.Можно переопределять флаг для development окружения или использовать правило ESLint no-unused-vars.noUnusedParametersРекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-varsКод проверяется на наличие неиспользуемых аргументов функций и методов:
function createDefaultKeyboard (modelID: number) {
const defaultModelID = 23
return {
type: 'keyboard',
modelID: defaultModelID
}
}
Данное правило также как noUnusedParameters мешает удобному процессу разработки. Можно переопределять флаг для development окружения или добавить дополнительные параметры к проверке ESLint no-unused-vars.Подведение итогов Linter Checks
- нет единого флага, которым можно активировать все опции секции
- опции noPropertyAccessFromIndexSignature и noUncheckedIndexedAccess по факту относятся к Strict Checks, только не активируются автоматически при включении главного флага strict
- остальные флаги секции: noFallthroughCasesInSwitch, noUnusedLocals, noUnusedParameters можно заменить правилами линтера. Только правило для noImplicitReturns временно не поддерживается
Группа AdvancedЭто самая большая секция и кажется, что в неё поместили все опции, которым не нашлось отдельных групп. Из них часть опций относится к настройкам строгости компилятора. Вот они:
{
"compilerOptions": {
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noImplicitUseStrict": false,
"suppressExcessPropertyErrors": false,
"suppressImplicitAnyIndexErrors": false,
"noStrictGenericChecks": false,
}
}
Первые два флага allowUnreachableCode и allowUnusedLabels опять отделены пустой строкой не случайно – они отбились от Linter Checks.Внезапно по умолчанию эти флаги имеют значение true, однако для строгости должны быть false – ровно наоборот.Запомнить это поведение достаточно легко. У предыдущих опций такого рода в названиях фигурировал префикс no, т. е. «запретить допущения». У данных же флагов префикс allow – «разрешить допущения». По-хорошему для консистентности стоит переименовать эти опции в noUnreachableCode и noUnusedLabels и отправить в соответствующую группу.Остальные флаги: noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors можно было бы вынести в новую группу и назвать её Base Strict Checks. И вот почему:Эти флаги по умолчанию false и должны быть false для строгости!Вот мы и нашли те опции, которые включены по умолчанию и составляют типобезопасность TypeScript из коробки!По сути, вышеперечисленные опции (кроме noImplicitUseStrict) включают явные нарушения типизации, что в некоторых случаях можно использовать как временное решение при портировании сложных участков кода с JavaScript.Ни один флаг из данной четвёрки не придётся переопределять в 99.9% случаев.Теперь буквально по паре слов об этой псевдо-группе опций.allowUnreachableCodeРекомендую: всегда для production / сложность: легко / правило ESLint: no-unreachable, пока не реализованоФлаг запрещает недосягаемый код – код, написанный после операторов return, throw, break, continue:
function fn (n: number) {
if (n > 5) {
return true
} else {
return false
}
// недосягаемый код
return true
}
Данный флаг будет удобно выключать для development. Аналогично правилу линтера no-unreachable, которое не реализовано для TypeScript.allowUnusedLabelsРекомендую: для production если не используется ESLint / сложность: легко / правило ESLint: no-unused-labelsОпция запрещает неиспользуемые лэйблы. Не думаю, что можно встретить данную синтаксическую конструкцию в наши дни, но тем не менее:
function verifyAge (age: number) {
if (age > 18) {
// тот самый неиспользуемый label
verified: true
}
}
Заменим правилом линтера no-unused-labels.noImplicitUseStrictНастроен по умолчанию, изменять противопоказано / связан с alwaysStrictФлаг автоматически добавляет "use strict" если target версия меньше, чем ES6. Опция alwaysStrict в свою очередь делает тоже самое, но для любых target. Вряд ли когда-нибудь возникнет необходимость переопределять значение данного флага.Если же обоим флагам noImplicitUseStrict и alwaysStrict установить значение true, то возникнет ошибка компиляции, так как настройки противоречат друг другу.suppressExcessPropertyErrorsНастроен по умолчанию, изменять противопоказаноФлаг проверяет, чтобы объект не мог содержать свойства, которые не были описаны в его структуре:
interface Point {
x: number
y: number
}
const p: Point = {
x: 1,
y: 3,
// свойство z не объявлено в интерфейсе Point
z: 10
}
Можно временно выключать данную настройку при миграции кодовой базы с JavaScript, если в коде много подобных моментов. Но после нужно обязательно восстановить исходное значение. Для разовых инцидентов лучше использовать // @ts-ignore.suppressImplicitAnyIndexErrorsНастроен по умолчанию, изменять противопоказано / связан с noImplicitAnyФлаг проверяет, что используя скобочную нотацию невозможно обращаться к свойствам объекта, которые в нём не объявлены ни явно, ни через произвольные параметры. Вспомним пример флага noUncheckedIndexedAccess и удалим из него произвольные параметры:
interface User {
// явно указанные параметры
login: string
email: string
// закомментируем произвольные параметры
// [key: string]: string
}
const user: User = {
login: 'hello',
email: 'hello@example.com'
}
// теперь обратиться к name нельзя
const username = user['name']
Казалось бы, другого поведения здесь и быть не может. Но как я и говорил, последняя четвёрка флагов порой разрешает включить нарушение типизации для каких-то крайних случаев. Например, данный флаг часто инвертируют, когда не знают, как расширять встроенные в TypeScript тайпинги или тайпинги внешних библиотек. Это плохая практика и в таких случаях правильно использовать declaration merging.Рассмотрим пример. Мы подключили некую библиотеку, допустим Google Maps, через тэг script. Попробуем создать объект Pin:
const pin = new window.google.maps.Pin(59.9386, 30.3141)
Конечно, здесь будет ошибка, так как встроенные тайпинги ничего не знают о свойстве google. Использовать // @ts-ignore проблематично, так как его придётся писать чуть ли на каждой строчке кода. Поэтому часто можно встретить такое решение – ставим suppressImplicitAnyIndexErrors: true (разрешить допущение) и пишем код следующим образом:
const pin = new window['google']['maps']['Pin'](59.9386, 30.3141)
Это имеет место быть в проектах на промежуточной стадии портирования. Но я бы не рекомендовал прибегать к подобной практике даже в этом случае. Подобная проблема решается использованием объединения деклараций:
// произвольный файл merging.d.ts
interface Pin {
// описание
}
interface PinConstructor {
new(lat: number, lng: number): Pin
}
interface Window {
google: {
maps: {
Pin: PinConstructor
}
}
}
// использование в коде
const pin = new window.google.maps.Pin(59.9386, 30.3141)
Данная практика применима и к внешним библиотекам, например для расширения req и res объектов фреймворка Express.noStrictGenericChecksНастроен по умолчанию, изменять противопоказаноПереопределение данного флага может сделать компилятор «более лояльным» при работе с generics:
type A = <T, U>(x: T, y: U) => [T, U]
type B = <S>(x: S, y: S) => [S, S]
function f (a: A, b: B) {
// OK
b = a
// должна быть ошибка, но компилятор проигнорирует
a = b
}
Подведём итоги Advanced
- Секция состоит из большого количества не связанных между собой флагов. Часть из них относится к строгости компилятора. Эти флаги можно перераспределить в тематические секции
- Флаги allowUnreachableCode и allowUnusedLabels относятся к линтингу, их можно было бы перенести в Linter Checks
- Опции noImplicitUseStrict, noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors те самые, которые включены в TypeScript по умолчанию и составляют типобезопасность из коробки.
- Про остальные четыре флага можно забыть
- Флаг noImplicitUseStrict является вариацией флага alwaysStrict.
- Опции noStrictGenericChecks, suppressExcessPropertyErrors и suppressImplicitAnyIndexErrors выключают некоторые базовые проверки синтаксиса, с целью облегчения портирования сложных участков кода с JavaScript на TypeScript.
ЗаключениеВ качестве заключения я бы хотел написать шпаргалку со всеми упомянутыми флагами, которые обеспечивают максимальную строгость и наглядность:Файл tsconfig-checks.json:
{
"compilerOptions": {
// Strict Checks
"alwaysStrict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"strictBindCallApply": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
// Linter Checks
"noImplicitReturns": true, // https://eslint.org/docs/rules/consistent-return ?
"noFallthroughCasesInSwitch": true, // https://eslint.org/docs/rules/no-fallthrough
"noUnusedLocals": true, // https://eslint.org/docs/rules/no-unused-vars
"noUnusedParameters": true, // https://eslint.org/docs/rules/no-unused-vars#args
"allowUnreachableCode": false, // https://eslint.org/docs/rules/no-unreachable ?
"allowUnusedLabels": false, // https://eslint.org/docs/rules/no-unused-labels
// Base Strict Checks
"noImplicitUseStrict": false,
"suppressExcessPropertyErrors": false,
"suppressImplicitAnyIndexErrors": false,
"noStrictGenericChecks": false,
}
}
Почему tsconfig-checks.json? В первой части статьи мы обсуждали, что для того, чтобы не смешивать настройки строгости с другими опциями, мы прибегнем к наследованию конфигов. Так и поступим.Файл tsconfig.json:
{
// настройки строгости
"extends": "./tsconfig-checks.json",
"compilerOptions": {
// все остальные настройки
}
}
А для удобства разработки, чтобы нам недостижимый код и неиспользуемые переменные не ломали компиляцию, можно сделать следующим образом.Файл tsconfig-dev.json:
{
"extends": "./tsconfig.json",
"compilerOptions": {
// выключим некоторый линтинг, чтобы не ломалась компиляция
"noUnusedLocals": false,
"noUnusedParameters": false,
"allowUnreachableCode": true,
"allowUnusedLabels": true
}
}
На этом всё. В будущем планирую разобраться с опциями оптимизации и производительности и обязательно поделиться с вами. До новых встреч!Статья основана на моём треде в коллективном аккаунте @jsunderhood.P.S: В своём аккаунте @barinbritva тоже иногда пишу про TypeScript и разработку.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, JavaScript, ReactJS, TypeScript] Кэш или стэйт, пробуем React-query
- [Высокая производительность, Разработка веб-сайтов, PHP, Magento] PHP-SPX простой профайлер трейсер для PHP
- [Разработка веб-сайтов, CSS, Программирование] Инструменты для аудита CSS (перевод)
- [Программирование, Анализ и проектирование систем, Проектирование и рефакторинг, Управление разработкой, TypeScript] Чем меня не устраивает гексагональная архитектура. Моя имплементация DDD – многоуровневая блочная архитектура
- [JavaScript, Космонавтика, Лайфхаки для гиков] Отслеживание и визуализация положения МКС с помощью 30 строк JavaScript-кода (перевод)
- [Разработка веб-сайтов, JavaScript, HTML] Шпаргалка по JS-методам для работы с DOM
- [Разработка веб-сайтов, CSS] CSS – строго типизированный язык программирования (перевод)
- [Планшеты] Планшет как жанр: как я познакомилась с Huawei
- [Читальный зал, Научно-популярное, Биотехнологии, Здоровье] Свет внутри: неинвазивная биолюминесцентная визуализация
- [Разработка веб-сайтов, PHP, Symfony, Конференции] Прямой эфир про тесты, трейты, devops в монолите, переход на Go и KPHP с казанского PHP-митапа
Теги для поиска: #_razrabotka_vebsajtov (Разработка веб-сайтов), #_javascript, #_typescript, #_typescript, #_konfiguratsija (конфигурация), #_tips, #_tricks, #_opyt (опыт), #_best_practices, #_razrabotka_vebsajtov (
Разработка веб-сайтов
), #_javascript, #_typescript
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 24-Ноя 22:55
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В прошлой статье я рассказал о различных особенностях некоторых общих настроек TypeScript. В данной статье речь пойдёт о так называемых «флагах строгости».На самом деле, TypeScript из коробки мало чем отличается от JavaScript. Поэтому если изначально не подтюнить конфиг проекта, то большая часть преимуществ языка не будет задействована. Смысл использования TypeScript в таком виде есть, но небольшой.Если выразить смысл всей статьи в одном предложении, то текст можно свести к следующему: откройте свой tsconfig.json, установите флаг strict в секции compilerOptions в значение true. Без режима строгости использование TypeScript это практически пустая трата времени. Однако цель предыдущей и данной статей заключается не в том, чтобы сказать как «правильно» делать, а прояснить некоторые тонкости конфигурации. Поэтому погрузимся в детали данного вопроса.В официальном референсе tsconfig.json его многочисленные опции поделены на секции. Из них две секции: Strict Checks и Linter Checks – содержат только опции тех самых флагов строгости. Помимо ещё часть интересующих нас сегодня флагов сокрыта в самой большой группе опций Advanced.Группа Strict ChecksПожалуй, флаги данной категории наиболее важные из всех. Вот они: strict, alwaysStrict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, strictBindCallApply. Все вышеперечисленные флаги по умолчанию имеют значение false, а для того, чтобы было строго нужно установить противоположное значение – true.Первое, что здесь может броситься в глаза, глядя на список опций, это наличие флагов с похожими именами, которые сбивают с толку - strict и alwaysStrict. На самом деле флаги выполняют совершенно разные функции.alwaysStrictРекомендован: всегда / сложность: легко.Флаг alwaysStrict включает добавление строки "use strict" в каждый скомпилированный файл. Другими словами, alwaysStrict включает строгий режим JavaScript и никак не связан с проверкой типов TypeScript.strictРекомендован: всегда / сложность: сложно / может быть заменён набором других флагов.Флаг strict напрямую связан с проверкой типов. Его включение автоматически активирует абсолютно все флаги секции Strict Checks, включая и alwaysStrict. Это именно то, о чём я говорил в самом начале.У такого подхода есть как минимум один недостаток – неочевидность. Устанавливая strict: true, нет наглядного представления, какие именно проверки включены и какие опции вообще существуют. Для проектов, которые с самого начала пишутся на TypeScript это не так принципиально, как для проектов, которые поэтапно портируются с JavaScript.В процессе портирования существующего приложения нет возможности сразу включить все проверки. Приходится активировать их по одной шаг за шагом. Иногда даже случаются сложности, из-за которых приходится откатывать ранее установленные флаги обратно в false.Есть небольшая особенность работы флага strict – список подконтрольных ему флагов может пополняться по мере выхода новых версий TypeScript. Подобные моменты если случаются, то редко и всегда освещаются в release notes если, конечно, вы их читаете перед обновлением версии.Лично я предпочитаю указывать список флагов явным образом: {
"compilerOptions": { "alwaysStrict": true, "noImplicitAny": true, "strictNullChecks": true, "strictPropertyInitialization": true, "strictFunctionTypes": true, "noImplicitThis": true, "strictBindCallApply": true, } } // тип задан явно
let a: number = 5 // тип выведен let b = 'hello' // тип не указан и не может быть выведен // value будет неявно объявлено как any function someFunction (value) { // поэтому ошибка в этой строке останется незамеченной console.log(value.subtr(3)) } // явно помечаем код как потенциально не безопасный через any
function someFunction (value: any) { console.log(value.subtr(3)) } function someFunction (value: number) {
// можно получить неожиданный результат если value будет undefined или null return value * 2 } someFunction(5) // по умолчанию можно так someFunction(null) // и можно так someFunction(undefined) function someFunction (value: number) {
// value всегда будет только number return value * 2 } someFunction(5) // следующие вызовы функции невозможны someFunction(null) someFunction(undefined) // Символ «?» разрешает undefined, а «| null» - null
function someFunction (value?: number | null) { if (value == null) { return 0 } return value * 2 } class User {
name: string // email не инициализирован ни здесь, ни в конструкторе // компилятор подскажет, что нужно установить значение email: string constructor (name: string) { this.name = name } } interface StringOrNumberFunc {
(value: string | number): void } function someFunction (value: string) { console.log(value) } // сигнатуры не совпадают // string | number не эквивалентны string let func: StringOrNumberFunc = someFunction func(10) func('10') class SomeClass {
multiplier: number = 5 createSomeFunction (value: number) { return function () { // контекст потерян - здесь this НЕ является объектом класса SomeClass return value * this.multiplier } } } function sayHello (name: string) {
console.log(this.helloWord + ' ' + name) } // укажем тип this первым «аргументом»
// фактически это не будет считаться аргументом функции function sayHello (this: HelloStyle, name: string) { console.log(this.helloWord + ' ' + name) } // объявим варианты приветствий interface HelloStyle { helloWord: string } class HawaiiStyle implements HelloStyle { helloWord = 'Aloha' } class RussianStyle implements HelloStyle { helloWord = 'Привет,' } // теперь вызовем sayHello.bind(new HawaiiStyle())('World') sayHello.call(new RussianStyle(), 'World') sayHello.apply(new RussianStyle(), ['World']) function someFunction (value: string) {
console.log(value) } someFunction.call(undefined, '10') // да, для таких случаев нужен отдельный флаг someFunction.call(undefined, false)
{
"compilerOptions": { "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, } } interface User {
// явно указанные параметры login: string email: string // произвольные параметры [key: string]: string } const user: User = { login: 'hello', email: 'hello@example.com' } // c noPropertyAccessFromIndexSignature: true // данная строка приведёт к ошибке const username = user.name // но так обращаться всё ещё можно const username2 = user['name'] interface User {
// явно указанные параметры login: string email: string // произвольные параметры [key: string]: string } const user: User = { login: 'hello', email: 'hello@example.com' } // Тип переменной username - string const username = user['name'] const strings: string[] = ['hello']
// здесь переменная number будет иметь тип string const number = strings[100] const strings: string[] = ['hello']
// в данном случае переменная number также будет иметь тип string | undefined const number = strings[0] function upperCaseAll(strings: string[]) {
for (let i = 0; i < strings.length; i++) { // здесь будет ошибка компиляции console.log(strings[i].toUpperCase()); } } function lookupHeadphonesManufacturer(color: 'blue' | 'black'): string {
if (color === 'blue') { return 'beats' } // забыли return } switch (value) {
case 0: console.log('even') // забыли break case 1: console.log('odd') break } function createKeyboard (modelID: number) {
// объявлена неиспользуемая переменная const defaultModelID = 23 return { type: 'keyboard', modelID } } function createDefaultKeyboard (modelID: number) {
const defaultModelID = 23 return { type: 'keyboard', modelID: defaultModelID } }
{
"compilerOptions": { "allowUnreachableCode": false, "allowUnusedLabels": false, "noImplicitUseStrict": false, "suppressExcessPropertyErrors": false, "suppressImplicitAnyIndexErrors": false, "noStrictGenericChecks": false, } } function fn (n: number) {
if (n > 5) { return true } else { return false } // недосягаемый код return true } function verifyAge (age: number) {
if (age > 18) { // тот самый неиспользуемый label verified: true } } interface Point {
x: number y: number } const p: Point = { x: 1, y: 3, // свойство z не объявлено в интерфейсе Point z: 10 } interface User {
// явно указанные параметры login: string email: string // закомментируем произвольные параметры // [key: string]: string } const user: User = { login: 'hello', email: 'hello@example.com' } // теперь обратиться к name нельзя const username = user['name'] const pin = new window.google.maps.Pin(59.9386, 30.3141)
const pin = new window['google']['maps']['Pin'](59.9386, 30.3141)
// произвольный файл merging.d.ts
interface Pin { // описание } interface PinConstructor { new(lat: number, lng: number): Pin } interface Window { google: { maps: { Pin: PinConstructor } } } // использование в коде const pin = new window.google.maps.Pin(59.9386, 30.3141) type A = <T, U>(x: T, y: U) => [T, U]
type B = <S>(x: S, y: S) => [S, S] function f (a: A, b: B) { // OK b = a // должна быть ошибка, но компилятор проигнорирует a = b }
{
"compilerOptions": { // Strict Checks "alwaysStrict": true, "noImplicitAny": true, "strictNullChecks": true, "strictPropertyInitialization": true, "strictFunctionTypes": true, "noImplicitThis": true, "strictBindCallApply": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, // Linter Checks "noImplicitReturns": true, // https://eslint.org/docs/rules/consistent-return ? "noFallthroughCasesInSwitch": true, // https://eslint.org/docs/rules/no-fallthrough "noUnusedLocals": true, // https://eslint.org/docs/rules/no-unused-vars "noUnusedParameters": true, // https://eslint.org/docs/rules/no-unused-vars#args "allowUnreachableCode": false, // https://eslint.org/docs/rules/no-unreachable ? "allowUnusedLabels": false, // https://eslint.org/docs/rules/no-unused-labels // Base Strict Checks "noImplicitUseStrict": false, "suppressExcessPropertyErrors": false, "suppressImplicitAnyIndexErrors": false, "noStrictGenericChecks": false, } } {
// настройки строгости "extends": "./tsconfig-checks.json", "compilerOptions": { // все остальные настройки } } {
"extends": "./tsconfig.json", "compilerOptions": { // выключим некоторый линтинг, чтобы не ломалась компиляция "noUnusedLocals": false, "noUnusedParameters": false, "allowUnreachableCode": true, "allowUnusedLabels": true } } =========== Источник: habr.com =========== Похожие новости:
Разработка веб-сайтов ), #_javascript, #_typescript |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 24-Ноя 22:55
Часовой пояс: UTC + 5