Типизация в программировании: статическая и динамическая, сильная и слабая

Типизация в программировании: статическая и динамическая, сильная и слабая Полезное

Типизация — это набор правил языка программирования о том, какие типы у значений и выражений, когда язык проверяет совместимость типов и насколько охотно сам преобразует один тип в другой. От типизации зависит, поймаете вы ошибку "5" + 2 до запуска программы, во время запуска или не поймаете вовсе.

Типизацию описывают несколькими независимыми осями, ниже разберем каждую на запускаемых примерах. Код проверен на Python 3.9+ и Node.js 22.

Мини-словарь

  • Тип — множество допустимых значений плюс операции над ними.
  • Проверка типов — поиск операций, недопустимых для типа. Ее делает компилятор, анализатор или среда выполнения.
  • Неявное преобразование (coercion) — язык сам превращает значение в другой тип, чтобы операция выполнилась.
  • Вывод типов (type inference) — компилятор сам вычисляет тип по выражению, хотя вы его не написали.

Четыре оси типизации

Ось Вопрос Варианты Строгость термина
Время проверки Когда ловится ошибка типа? Статическая (до запуска) / динамическая (во время выполнения) Строгий
Неявные преобразования Сложит ли язык строку с числом сам? Сильная / слабая Нестрогий, это шкала
Запись типов Нужно ли писать тип в коде? Явная / неявная (вывод типов) Строгий
Совместимость Что считается «тем же» типом? Номинальная (по имени) / структурная (по форме) Строгий

Оси независимы. Статический язык может выводить типы сам (Kotlin, TypeScript), а динамический — не делать почти никаких неявных преобразований (Python). Поэтому «статическая = явная» и «динамическая = слабая» — частые, но неверные упрощения.

Статическая и динамическая типизация

При статической типизации типы проверяются до запуска: компилятором (Java, C#, Go) или анализатором (tsc для TypeScript, mypy для Python). Программа с ошибкой типа не соберется или анализатор выдаст ошибку.

При динамической типизации тип привязан к значению, а не к переменной, и проверка идет в момент выполнения операции. Ошибка проявится, только когда выполнение дойдет до проблемной строки:

def price_label(price, currency):
    if currency == "RUB":
        return str(price) + " руб."
    return price + " " + currency  # ошибка: price - число


print(price_label(100, "RUB"))
try:
    print(price_label(100, "USD"))
except TypeError as e:
    print("TypeError:", e)

Результат:

100 руб.
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Первый вызов прошел, хотя в функции есть ошибка: ветка с USD не выполнялась. В статическом языке такой код не скомпилировался бы. Динамический язык находит ошибку типа позже и только на выполненных путях, поэтому в нем важнее тесты.

Граница: статическая проверка ловит не все. Деление на ноль, выход за границы массива и логические ошибки остаются ошибками времени выполнения.

Сильная и слабая типизация: шкала, а не два лагеря

У терминов «сильная» и «слабая» (strong/weak) нет общепринятого строгого определения. Обычно ими описывают, насколько охотно язык неявно преобразует несвязанные типы. Поэтому честнее говорить «сильнее» или «слабее», чем делить языки на два лагеря.

JavaScript выполняет много неявных преобразований:

console.log("5" + 2);         // строка "52": число превращено в строку
console.log("5" - 2);         // 3: строка превращена в число
console.log("5" * "2");       // 10
console.log(null + 1);        // 1: null стал 0
console.log(undefined + 1);   // NaN
console.log(1 == "1", 1 === "1");  // true false
console.log(Number("5") + 2); // 7: явное преобразование

Оператор + со строкой склеивает, а - и * приводят к числу. Отсюда правило практики: сравнивать через === и приводить типы явно.

Python считается языком с сильной типизацией, но и он не абсолютно «строгий»:

print(1 + 2.5)       # int расширяется до float
print(True + 1)      # bool - подкласс int
print("5" * 2)       # повтор строки, не арифметика
try:
    print("5" + 2)
except TypeError as e:
    print("TypeError:", e)
print(int("5") + 2)  # явное преобразование

Результат:

3.5
2
55
TypeError: can only concatenate str (not "int") to str
7

Строку с числом Python не складывает, но число с числом разных видов — складывает, а True ведет себя как 1. Похожая картина в Java: "a" + 1 дает "a1", хотя Java обычно называют строго типизированной.

C — статический язык, но с большим числом неявных преобразований, поэтому его часто относят к слабым:

#include <stdio.h>

int main(void) {
    int total = 7, count = 2;
    double avg = total / count;           /* деление целых, затем в double */
    double avg2 = (double)total / count;  /* явное приведение */
    int truncated = 3.9;                  /* дробная часть отброшена */
    printf("%f %f %d\n", avg, avg2, truncated);
    return 0;
}

Вывод: 3.000000 3.500000 3. Код соберется; предупреждение о потере точности зависит от компилятора и флагов (-Wconversion). Статическая типизация не гарантирует отсутствия неявных преобразований.

Явная и неявная типизация

Явная типизация — тип пишется в коде: int x = 7; в C. Неявная — тип не пишется. В динамических языках тип есть у значения (x = 7 в Python), в статических работает вывод типов: компилятор вычисляет тип и проверяет его так же строго, как написанный руками.

Язык Запись с выводом типа Что получилось
C# var n = 5; n имеет тип int, присвоить строку нельзя
Java 10+ var names = new ArrayList<String>(); ArrayList<String>, только для локальных переменных
TypeScript let n = 5; number

var в C# и Java не делает язык динамическим: тип фиксируется при компиляции и больше не меняется.

Номинальная и структурная типизация

При номинальной типизации совместимость определяется именем и объявленным наследованием: в Java класс подходит под интерфейс, только если написано implements. При структурной подходит любой объект с нужными полями и методами. Так устроены TypeScript и интерфейсы Go.

interface Point { x: number; y: number; }

function length(p: Point): number {
  return Math.hypot(p.x, p.y);
}

const a = { x: 3, y: 4, label: "A" };
console.log(length(a)); // 5: объект подходит по форме, лишнее поле не мешает

// length({ x: 3 });          // ошибка tsc: нет обязательного поля y
// const n: number = "5";     // ошибка tsc: string нельзя присвоить number

Нюанс: объектный литерал с лишним полем, переданный прямо в вызов, tsc отклонит (excess property check); через переменную, как a, — нет.

В динамических языках родственная идея — утиная типизация: есть нужный метод — его можно вызвать. В Python ее статически проверяют через typing.Protocol.

Постепенная типизация: TypeScript и type hints в Python

Постепенная (gradual) типизация позволяет добавлять статические типы в динамический код по частям, а тип any (Any в Python) отключает проверку для конкретного значения.

Главная граница: аннотации проверяет отдельный инструмент до запуска, а не среда выполнения.

from __future__ import annotations


def add_tax(price: float, rate: float = 0.2) -> float:
    return price * (1 + rate)


print(add_tax(100))       # 120.0
print(add_tax("100", 1))  # подсказки не проверяются при запуске

Вывод: 120.0 и 100100 — строка "100" умножилась на 2. Python выполнил код, а mypy или pyright до запуска сообщили бы, что передан str вместо float. В TypeScript похоже: типы стираются при сборке, поэтому данные из сети или JSON.parse нужно проверять во время выполнения.

Языки на осях

Таблица — ориентир для типичного кода, а не приговор: у каждого языка есть режимы и исключения.

Язык Проверка Неявные преобразования Запись типов Совместимость
Python Динамическая (+ mypy/pyright) Мало Неявная + аннотации Утиная, Protocol
JavaScript Динамическая Много Неявная Утиная
TypeScript Статическая при сборке, постепенная Как в JS при выполнении Вывод + явная Структурная
Java Статическая Мало Явная + var Номинальная
C# Статическая (плюс dynamic по желанию) Мало Явная + var Номинальная
C Статическая Много Явная Номинальная для структур
PHP Динамическая Много, меньше с strict_types=1 Неявная + объявления Номинальная
Go Статическая Почти нет (int и int64 не смешать) Явная + := Структурная (интерфейсы)

Частые мифы

  • «Статический язык всегда быстрее». Известные заранее типы помогают оптимизации, но скорость определяет реализация (JIT-движок V8 заметно ускоряет динамический код).
  • «В динамическом языке нет типов». Типы есть у значений, проверка просто происходит позже.
  • «Сильная типизация = статическая». Python динамический, но не складывает строку с числом; C статический, но многое преобразует сам.

Выводы

  • Типизация описывается несколькими независимыми осями: время проверки, неявные преобразования, запись типов, совместимость.
  • Статическая типизация ловит ошибки типов до запуска, динамическая — только на выполненных путях кода.
  • «Сильная» и «слабая» — нестрогие термины, это шкала неявных преобразований, а не два класса языков.
  • Вывод типов (var, auto, let) не делает язык динамическим.
  • Постепенная типизация (TypeScript, type hints) проверяется инструментом до запуска и не защищает данные, пришедшие во время выполнения.

Где применяется / связь с практикой

Освойте тему на практике

Частый сценарий на практике — JavaScript-проект, в который постепенно добавляют TypeScript: сначала allowJs и типы на границах модулей, затем strict, затем отказ от any. Разобрать систему типов TypeScript с дженериками, сужением типов и настройкой компилятора можно на курсе по TypeScript. Отдельные темы бесплатно разбирают на открытых уроках Otus.

FAQ

Какая типизация лучше для первого языка?
Однозначного ответа нет: динамический язык быстрее дает первый результат, статический раньше показывает ошибки.

Можно ли сделать JavaScript строго типизированным без перехода на TypeScript?
Частично: JSDoc-комментарии с типами и опция checkJs позволяют tsc проверять обычные .js-файлы.

Что такое тип any и почему его избегают?
any в TypeScript отключает проверку для значения: с ним можно делать что угодно, и ошибки типов снова появляются только во время выполнения. Безопаснее unknown, который требует проверки перед использованием.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)