В языках программирования у любого кусочка данных (переменной, константы, аргумента функции) есть тип. Тип определяет множество возможных значений и операции, которые с ними можно проводить. Например, в этом участке псевдокода переменная age
имеет тип «число»:
number age = 12 // Тип указан явно перед именем переменной
В разных языках по-разному устроена работа с типами. Важно понимать, чем отличаются разные системы типизации, чтобы умело и к месту использовать тот или иной язык.
Вообще, существуют «бестиповые языки», где вся работа с типами возлагается на разработчика. Но таких языков немного и они довольно редкие. Один из примеров — язык ассемблера (очень низкоуровневый язык программирования).
В первую очередь системы типов делятся по способу приведения типов.
Приведение типов — это превращение данных одного типа в данные другого типа. Например, строку «12» довольно легко превратить в число 12:
number age = toNumber("12")
Некоторые языки сами приводят типы, делают это скрыто от программиста. Такое поведение принято назвать слабой (или нестрогой) типизацией. Обычно, это позволяет в одном выражении использовать переменные любых типов и не беспокоиться об их приведении. Часто это приводит к удивительным последствиям.
print("12" + 13) // Выведет 1213
print(12 + "13") // Выведет 25
С другой стороны стоят языки, которые требуют явно определить, что следует делать с данными, чтобы перевести их в другой тип. Они полностью отдают эту работу программисту. Это поведение называется сильной (или строгой) типизацией.
print("12" + 13) // Ошибка!
print("12" + toString(13)) // Выведет 1213
print(toNumber("12") + 13) // Выведет 25
Иногда бывает сложно провести черту между сильной и слабой типизацией. Во многих языках некоторые преобразования происходят автоматически, а некоторые требуют явного приведения типов. Строгость типизации — это шкала, и язык может располагаться на ней где угодно.
Другая важная классификация делит языки на статически типизированные и динамически типизированные.
В статически типизированном языке каждая переменная имеет определенный тип на всем протяжении жизни, он не может измениться во время выполнения. То есть все типы известны ещё на этапе написания кода. Если же во время выполнения попытаться присвоить переменной одного типа значение другого типа, произойдет ошибка. Причём такие ошибки можно найти без запуска программы.
number age = 44
age = "Not so old" // Ошибка!
Динамически типизированные языки работают иначе. У каждой переменной всё ещё есть тип. Но он может легко меняться по ходу исполнения программы. На практике это означает, что в конкретный момент времени мы достоверно не знаем, данные какого типа находятся в переменной.
age = 44 // Тип перменной — число
age = "Not so old" // Тип переменной — строка
Проблема в том, что ничего не лучше. Каждая система типизации решает разные проблемы, у каждой свои плюсы и минусы. Динамическая типизация проще и удобнее на ранних этапах разработки программы, статическая типизация обеспечивает более высокую степень надёжности. С другой стороны, слабая типизация позволяет более комфортно писать код, не заботиться о преобразованиях типов и отдать это на откуп языку, а сильная позволяет лучше контролировать исполнение программы и иметь больше уверенности в корректности написанного.
Мир программирования разнообразен, в нём можно встретить языки с абсолютно любыми комбинациями систем типов, например:
При этом для многих языков появляются инструменты, позволяющие брать лучшие практики из всех систем типизации. Например, в Python появились опциональные аннотации типов, которые позволяют принести немного статической типизации. Аналогичные проекты есть для JavaScript (TypeScript, Flow) и Ruby (Sorbet).
Читайте также Как проверять типы данных в JavaScript с помощью JSDoc: подробное руководство.