Клавиша / esc
Несколько разных персонажей в стиле «Найди отличие в картинке», каждый чем-то отличается от других
Иллюстрация: Кира Кустова

TypeScript и статическая типизация

Как TypeScript и статическая типизация помогают писать код.

Время чтения: 6 мин

Кратко

Скопировано

В JavaScript слабая динамическая типизация. Это означает две вещи:

  1. При операции с переменными разных типов они будут автоматически приведены к одному типу.
  2. Любая переменная может произвольно менять свой тип во время выполнения программы. (За исключением строго типизированных массивов, например Uint8ClampedArray)

Эти свойства языка часто усложняют разработку больших и надёжных приложений. Поэтому появились решения, которые добавляют в JavaScript строгую статическую типизацию. «Строгая» означает запрет автоматического приведения типов, «статическая» значит, что проверка типов происходит на этапе сборки приложения или написания кода.

Самое популярное решение в этой области — TypeScript. Другие, менее популярные — Rescript, Flow и Hegel.

Как пользоваться

Скопировано

Настройка

Скопировано

TypeScript — это язык, очень похожий на JavaScript. Браузеры и Node.js не могут выполнять TypeScript, поэтому его необходимо преобразовать в JavaScript.

Современные системы сборки умеют работать с TypeScript: Vite и Parcel поддерживают его из коробки, в Webpack и Rollup нужно будет добавить дополнительные плагины или лоадеры. Настройки TypeScript задаются в специальном файле — tsconfig.json. Структура файла tsconfig.json описана на сайте TypeScriptLang.org.

        
          
          {  "compilerOptions": {    // Папка, куда попадёт результат    "outDir": "./dist",  },  // Какие файлы следует компилировать  "include": ["src/**/*"],  // Внешние зависимости, обычно, исключают из списка компилируемых файлов  "exclude": ["node_modules"]}
          {
  "compilerOptions": {
    // Папка, куда попадёт результат
    "outDir": "./dist",
  },
  // Какие файлы следует компилировать
  "include": ["src/**/*"],
  // Внешние зависимости, обычно, исключают из списка компилируемых файлов
  "exclude": ["node_modules"]
}

        
        
          
        
      

После этого можно писать код в файлах с расширением .ts вместо .js.

Язык

Скопировано

Главное отличие TypeScript от JavaScript — возможность добавлять аннотации типов к переменным, аргументам функций и их возвращаемым значениям.

        
          
          // Теперь переменной age можно присвоить только числоlet age: number// Будет работатьage = 43// Выдаст ошибкуage = '34'
          // Теперь переменной age можно присвоить только число
let age: number

// Будет работать
age = 43

// Выдаст ошибку
age = '34'

        
        
          
        
      

Ошибки несовпадения типов будут заметны на стадии написания кода. То есть не нужно запускать программу, чтобы узнать об ошибках в типизации.

Сравнение процесса доставки в JS и TS

Проверка корректности типов — это разновидность статического анализа.

Примерно таким же образом можно типизировать параметры функции:

        
          
          function sayMyName(name: string) {  console.log(`Привет, ${name}`)}// Будет работатьsayMyName('Игорь')// Привет, Игорь// Выдаст ошибкуsayMyName(42)
          function sayMyName(name: string) {
  console.log(`Привет, ${name}`)
}

// Будет работать
sayMyName('Игорь')
// Привет, Игорь

// Выдаст ошибку
sayMyName(42)

        
        
          
        
      

В TypeScript можно типизировать не только параметры функции, но и возвращаемое значение.

        
          
          function getCurrentDate(): Date {  // Будет работать  return new Date()}function getCurrentDate(): Date {  // Выдаст ошибку  return 'now'}
          function getCurrentDate(): Date {
  // Будет работать
  return new Date()
}

function getCurrentDate(): Date {
  // Выдаст ошибку
  return 'now'
}

        
        
          
        
      

Другая особенность TypeScript — строгость типизации. Он запрещает операции с переменными разных типов, чтобы не допустить неоднозначности результата.

        
          
          const age = 43const name = 'Mary'// Выдаст ошибку, складывать числа и строки нельзяconst result = age + name
          const age = 43
const name = 'Mary'

// Выдаст ошибку, складывать числа и строки нельзя
const result = age + name

        
        
          
        
      

Как понять

Скопировано

TypeScript — надмножество JavaScript. На практике это означает, что любой JavaScript-код является корректным TypeScript-кодом. А вот обратное неверно.

Главное преимущество строгой статической типизации — возможность найти ряд ошибок ещё на этапе написания кода. Например, классическая ошибка, когда в переменной не оказывается значения, приводит в JavaScript к ошибке во время выполнения, когда код уже работает в браузере.

        
          
          function generateEmail(user) {  return `${user.name}@mycompany.com`}// При вызове функции программист передает другой объект,// и происходит ошибка во время выполнения, пользователь её замечаетgenerateEmail({ fullName: 'Пётр Сергеевич Никольский' })
          function generateEmail(user) {
  return `${user.name}@mycompany.com`
}

// При вызове функции программист передает другой объект,
// и происходит ошибка во время выполнения, пользователь её замечает
generateEmail({ fullName: 'Пётр Сергеевич Никольский' })

        
        
          
        
      

Если переписать этот пример на TypeScript, потенциальная проблема исчезнет:

        
          
          // Аргумент функции всегда должен быть объектом,// у которого есть строковое поле namefunction generateEmail(user: { name: string }) {  return `${user.name}@mycompany.com`}// При вызове функции программист передает другой объект,// происходит ошибка во время сборки, программист её замечает и исправляетgenerateEmail({ fullName: 'Петр Сергеевич Никольский' })// Редактор кода или линтер укажет ошибку примерно такого содержания:// Argument of type '{ fullName: string; }' is not assignable to parameter of type '{ name: string; }'.
          // Аргумент функции всегда должен быть объектом,
// у которого есть строковое поле name
function generateEmail(user: { name: string }) {
  return `${user.name}@mycompany.com`
}

// При вызове функции программист передает другой объект,
// происходит ошибка во время сборки, программист её замечает и исправляет
generateEmail({ fullName: 'Петр Сергеевич Никольский' })

// Редактор кода или линтер укажет ошибку примерно такого содержания:
// Argument of type '{ fullName: string; }' is not assignable to parameter of type '{ name: string; }'.

        
        
          
        
      

Чем раньше обнаруживается ошибка, тем легче её исправить. Поэтому статическая типизация, статический анализ и написание автоматизированных тестов — это правила хорошего тона в современной разработке приложений. По сути, статическая типизация (как и статический анализ) добавляет в процесс разработки программы ещё один шаг — проверку типов, который выполняется перед сборкой проекта и может помочь найти ошибки на раннем этапе.

Сравнение этапов доставки приложения без TypeScript и с ним

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

TypeScript — это язык с опциональной типизацией. Он не заставляет программиста указывать типы, можно просто писать код как раньше. TypeScript постарается сам определить типы из контекста, и дать подсказки. Если контекст непонятен языку, он пометит тип переменной как any. Это означает, что в ней может лежать значение любого типа.

        
          
          // Видимо, переменная должна иметь тип numberconst age = 12// Язык не знает, какой тип имеет аргумент name,// он пометит его как anyfunction sayMyName(name) {  console.log(`Привет, ${name}`)}
          // Видимо, переменная должна иметь тип number
const age = 12

// Язык не знает, какой тип имеет аргумент name,
// он пометит его как any
function sayMyName(name) {
  console.log(`Привет, ${name}`)
}

        
        
          
        
      

Эта особенность языка называется выводом типов и присутствует во многих современных языках программирования. К сожалению, она не слишком развита в TypeScript, и чаще всего приходится всё-таки «подсказывать» компилятору типы. В других языках, например, в Scala, она развита сильнее, там типы приходится указывать значительно реже.

На практике

Скопировано

Дмитрий Скрыльников советует

Скопировано

Если у вас нет возможности использовать TypeScript, а типизировать код хочется, то можно использовать JavaScript с аннотациями типов в JSDoc.

        
          
          /** @type {number} */let age;/** * @param {string}  name */function sayMyName(name) {...}/** * @returns {Date} */function getCurrentDate() {...}
          /** @type {number} */
let age;

/**
 * @param {string}  name
 */
function sayMyName(name) {...}

/**
 * @returns {Date}
 */
function getCurrentDate() {...}

        
        
          
        
      

Это, конечно, не полноценная проверка типов, а только документация и удобное автодополнение в IDE:

Пример автодополнения из среды разработки VSCode

Игорь Камышев советует

Скопировано

В TypeScript существует «строгий режим» — он вынуждает указывать типы в тех случаях, когда язык не может определить их сам.

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

        
          
          {  "compilerOptions": {    // Запрещает класть в переменную null без явного объявления    "strictNullChecks": true,    // Делает вызовы методов bind, call, apply строго типизированными    "strictBindCallApply": true,    // Делает более строгими типы функций    "strictFunctionTypes": true,    // Запрещает объявление непустого поля класса без инициализации    "strictPropertyInitialization": true,  },}
          {
  "compilerOptions": {
    // Запрещает класть в переменную null без явного объявления
    "strictNullChecks": true,
    // Делает вызовы методов bind, call, apply строго типизированными
    "strictBindCallApply": true,
    // Делает более строгими типы функций
    "strictFunctionTypes": true,
    // Запрещает объявление непустого поля класса без инициализации
    "strictPropertyInitialization": true,
  },
}

        
        
          
        
      

Если проект был написан на JavaScript, и TypeScript внедряется туда постепенно, строгий режим может сильно замедлить этот процесс.