Логотип Directum

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

Можно начать разрабатывать код на TypeScript или включить в проект Flow. TypeScript – это компилируемая версия JavaScript, разработанная компанией Microsoft. Flow, в отличие от TypeScript, это не язык, а инструмент, который позволяет анализировать код и проверять типы. В сети можно найти множество статей и видео об этих подходах, а также руководство по тому, как начать использовать типизацию. В этой статье мы бы хотели рассказать, почему нам не подошел Flow, и как мы начали переходить на Typescript.

Немного истории


В 2016 году мы начали разрабатывать веб-клиент на базе React/Redux для нашей ECM системы. Для проверки типизации был выбран Flow по следующим причинам:

  1. React и Flow – это продукты одной компании Facebook.
  2. Flow более активно развивался.
  3. Flow легко интегрируется в проект.

Но проект рос, количество команд-разработки увеличилось, и проявился ряд проблем при использовании Flow:

  1. Фоновый режим проверки типов Flow использовал слишком много ресурсов ПК. В результате некоторые разработчики отключали его и запускали проверку по необходимости.
  2. Возникали ситуации, когда для приведения кода в соответствие с Flow тратилось столько же времени, сколько и на написание самого кода.
  3. В проекте стал появляться код, необходимый только для прохождения проверки Flow. Например, двойная проверка на null:

     foo() {
        if (this.activeFormContainer == null) {
            return;
       }
       // to do something
       if (this.activeFormContainer != null) // only for Flow
              this.activeFormContainer.style.minWidth = '100px';
     }
    
  4. Большинство разработчиков использовало редактор кода Visual Studio Code, в котором у Flow не такая хорошая поддержка, как у TypeScript. Во время разработки не всегда срабатывало автодополнение (IntelliSense), а также нестабильно работала навигация по коду. Хотелось бы иметь такое же удобство разработки, как при написании на С# в Visual Studio.

У некоторых разработчиков появилась идея попробовать перейти на TypeScript. Для того чтобы проверить идею перехода и убедить руководство, решили попробовать прототип.

Прототип


На прототипах мы хотели проверить две идеи:

  1. Попробовать перевести весь проект целиком.
  2. Настроить проект так, чтобы можно было использовать параллельно и Flow, и Typescript.

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

  1. Доделывать предстояло еще много! И пока мы будем дорабатывать проект, остальные команды будут продолжать разрабатывать новую функциональность, править баги, писать тесты. К тому же пришлось бы потратить немало времени для слияния файлов.
  2. Даже если бы мы перевели таким способом проект, то какой объем работы пришлось бы проделать нашим тестировщикам!

Хотя мы и отказались от этого варианта, на нем мы получили полезный опыт. Стал ясен примерный объем работ, который нужно проделать для перевода каждого файла. Вот как примерно выглядит перевод простого React-компонента.
Сравнение кода на Flow и TypeScript


Как видно, изменений не так много. В основном, они заключаются в следующем:

  • убрать //@flow;
  • заменить type на более привычный interface;
  • добавить модификаторы доступа;
  • заменить типы на типы из ts-библиотек (из примера на картинке: обработчики событий и сами события).

Реализация по второй идее позволила бы продолжить разработку, но уже на TypeScript, и в фоновом режиме потихоньку переводить существующую кодовую базу. Это давало ряд преимуществ:

  1. Легко переводить, без страха что-то упустить.
  2. Легко тестировать.
  3. Легко сливать изменения.

Но было не до конца ясно, можно ли настроить проект для работы с двумя видами типизации параллельно. Поиск в интернете ни к чему конкретному не привел, поэтому стали разбираться сами. В теории, анализатор Flow проверяет только файлы с расширением js/jsx и содержащие комментарий:

//@flow
или
/* @flow */

Для компилятора TypeScript файлы должны иметь расширение ts/tsx. Из чего следует, что оба подхода к типизации должны работать одновременно и не мешать друг другу. На основании этого мы настроили окружение проекта. Используя опыт от первого прототипа, перевели пару файлов. Скомпилировали проект, запустили клиент — всё заработало как раньше!

Зеленый свет


И вот в один прекрасный день — день планирования спринта, у нашей команды в бэклоге появляется User Story “Начать переход на TypeScript”, с следующим перечнем работ:

  1. Настроить webpack.
  2. Настроить tslint.
  3. Настроить тестовое окружение.
  4. Перевести файлы на TypeScript.

Настройка webpack


Первым делом нужно научить webpack обрабатывать файлы с расширением ts/tsx. Для этого добавили правило в секцию rules конфигурационного файла. Изначально использовался ts-loader:

// webpack.config.js
const rules = [
    ...
    {
      test: /\.(ts|tsx)?$/,
      loader: 'ts-loader',
      options: {
        transpileOnly: true
      }
    }
];

Чтобы ускорить сборку, отключили проверку типов: transpileOnly: true, т.к. IDE и так указывает на ошибки во время написания кода.

Но когда приступили к переводу наших Redux-экшенов, стало ясно, что для их работы необходим плагин babel-plugin-transform-class-display-name. Этот плагин добавляет всем классам статическое свойство displayName. Экшены после перевода стали обрабатываться только ts-loader, а это не позволило применить к ним плагины babel. В результате, мы отказались от ts-loader и расширили существующее правило для js/jsx, добавив babel/preset-typescript:

// webpack.config.js
const rules = [
    {
      test: /\.(ts|tsx|js|jsx)?$/,
      exclude: /node_modules|lib/,
      loader: 'babel-loader?cacheDirectory=true'
    },
    ...
];

// .babelrc.js
  const presets = [
    [
      "@babel/preset-env",
      {
        "modules": !isTest ? false : 'commonjs',
        "useBuiltIns": false
      }
    ],
    "@babel/typescript",
    "@babel/preset-react",
  ];

Для правильной работы компилятора TypeScript нужно добавить конфигурационный файл tsconfig.json, он был взят из документации.

Настройка Tslint


Написанный с использованием Flow код дополнительно проверялся с помощью eslint. Для TypeScript есть его аналог — tslint. Изначально хотелось все правила из eslint перенести в tslint. Была попытка синхронизации правил через плагин tslint-eslint-rules, но большинство правил не поддерживается. Также есть возможность использовать eslint для проверки ts-файлов с помощью typescript-eslint-parser. Но, к сожалению, к eslint-у можно подключить только один парсер. Если использовать только ts-parser для всех видов файлов, появляется много непонятных ошибок как в js-файлах, так и в ts. В результате, использовали рекомендуемый набор правил, расширенный под наши требования:

// tslint.json
  "extends": ["tslint:recommended", "tslint-react"]

Перевод файла на TypeScript


Теперь все готово, и можно приступать к переводу файлов. Для начала решили перевести небольшой React-компонент, который используется по всему проекту. Выбор пал на компонент “Кнопка”.

Кнопки в проекте

В процессе перевода столкнулись с проблемой: не все сторонние библиотеки имеют типизацию TypeScript, например, bem-cn-lite. На ресурсе TypeSearch от Microsoft библиотеку типов для нее найти не удалось. почти для всех необходимых библиотек мы нашли и подключили ts-библиотеки типов. Одним из решений было подключение через require:

const b = require(‘bem-cn-lite’);

Но при этом проблема с отсутствием типов не решилась. Поэтому мы сгенерировали «заглушку» для типов самостоятельно, воспользовавшись утилитой dts-gen:

dts-gen -m bem-cn-lite

Утилита сгенерировала файл с расширением *.d.ts. Файл поместили в папку @types и настроили tsconfig.json:

// tsconfig.json
    "typeRoots": [
      "./@types",
      "./node_modules/@types"
    ]

Далее, по аналогии с прототипом, мы перевели компонент. Скомпилировали проект, запустили клиент — всё заработало! Но сломались тесты.

Настройка тестового окружения


Для тестирования приложения мы используем Storybook и Mocha.

Storybook используется для визуального регрессионного тестирования (статья). Как и сам проект, он собирается с помощью webpack и имеет свой конфигурационный файл. Поэтому для работы с ts/tsx-файлами его нужно было сконфигурировать по аналогии с конфигурацией самого проекта.

Пока мы использовали ts-loader для сборки проекта, у нас перестали запускаться тесты Mocha. Для решения этой проблемы в тестовое окружение необходимо добавить ts-node:

// mocha.opts
--require @babel/polyfill
--require @babel/register
--require test/index.js
--require tsconfig-paths/register
--require ts-node/register/transpile-only
--recursive
--reporter mochawesome
--reporter-options reportDir=../../bin/TestResults,reportName=js-test-results,inlineAssets=true
--exit 

Но после перехода на Babel от этого можно было избавиться.

Проблемы


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

  1. Импорт компонентов/функций из разных типов файлов.
  2. Перевод компонентов высшего порядка.
  3. Потеря истории изменений.

Импорт компонентов/функций из разных типов файлов


При использовании компонентов/функций из разных типов файлов появилась необходимость указывать расширение файла:

import { foo } from ‘./utils.ts’

Избавиться от этого позволяет добавление допустимых расширений в конфигурационные файлы webpack и eslint:

// webpack.config.js
resolve: {
   …
   extensions: [ '.tsx', '.ts', '.js' ]
 }

// .eslintrc.js
"import/resolver": {
  "node": {
    "extensions": [
      ".js",
      ".jsx",
      ".ts",
      ".tsx",
      ".json"
    ]
  }
}

Перевод компонентов высшего порядка


Из всех типов файлов больше всего проблем вызвал перевод компонентов высшего порядка (Higher-Order Component, HOC). Это функция, которая на вход принимает компонент и возвращает новый компонент. Применяется в основном для повторного использования логики, например, это может быть функция, добавляющая возможность выделять элементы:

const MyComponentWithSeletedItem = withSelectedItem(MyComponent);

Или наиболее известная connect, из библиотеки Redux. Типизация таких функций не тривиальная и требует подключения дополнительной библиотеки для работы с типами. Подробно описывать процесс перевода не буду, так как в сети можно найти много руководств на эту тему. Если вкратце, то проблема заключается в том, что такая функция – абстрактная: на вход может принять любой компонент, с любым набором свойств. Это может быть компонент «Кнопка» со свойствами title и onClick или компонент «Картинка» со свойствами alt и imgUrl. Набор этих свойств нам заранее не известен, известны лишь те свойства, которые добавляет сама функция. Для того, чтобы компилятор TypeScript не ругался при использовании компонентов, полученных с помощью таких функций, нужно «вырезать» свойства, которые добавляет функция из возвращаемого типа.

Для этого нужно:

  1. Вынести в интерфейс эти свойства:

    interface IWithSelectItem {
      selectedItem: number;
      handleSelectedItemChange: (id: number) => void;
    }
    
  2. Удалить все свойства, которые входят в интерфейс IWithSelectItem из интерфейса компонента. Для этого можно воспользоваться операцией Diff<T, U> из библиотеки utility-types.

    React.ComponentType<Diff<TPropsComponent, IWithSelectItem>>
    

Потеря истории изменений


Для работы с исходниками, например, выполнение code review, мы используем Team Foundation Server. При переводе файлов мы столкнулись с одной неприятной особенностью. В пул реквестах вместо одного измененного файла появляется два:

  • удаленный – старая версия файла;
  • созданный – новая версия.

    Как это выглядит в Pull Request

Такое поведение наблюдается, если изменений в файле много (similarity < 50%), например для небольших по объему файлов. Для решения этой проблемы пробовали использовать:

  • команду git mv;
  • выполнять два коммита: первый – это изменение расширения файла, второй — с непосредственными исправлениями.

Но, к сожалению, оба подхода нам так и не помогли.

Итоги


Использовать Flow или же TypeScript — решает каждый для себя сам, оба подхода имеют свои плюсы и минусы. Мы для себя выбрали TypeScript. И на своем опыте убедились: если вы выбрали один из подходов и вдруг осознали, даже спустя три года, что он вам не подходит, то всегда можно его поменять. А для более гладкого перехода можно настроить проект, как и мы, на параллельную работу.

На момент написания статьи мы еще не полностью перешли на TypeScript, но основную часть — «ядро» проекта – мы уже переписали. В кодовой базе можно найти примеры перевода всех видов файлов, начиная от простого react-компонента и заканчивая компонентами высшего порядка. Также было проведено обучение среди всех команд разработчиков, и теперь каждая команда в рамках своей задачи на тех долг переводит часть проекта.

Мы планируем завершить переход до конца года, перевести тесты и storybook, и, возможно даже написать несколько своих tslint-правил.

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

Комментарии (39)


  1. polRk
    01.08.2019 17:05

    Доя подключения bem следует использовать bem-react от яндекса, он написан на typescript и весит меньше килобайта


  1. miraage
    01.08.2019 17:07
    +1

    Большой проект — это насколько? 50k+ sloc? 100k+?
    Компонентов сколько мигрировали? Какие codemod может посоветуете?
    Какие инструменты использовали для унификации type-guards при работе с thunk/saga?


    1. teager Автор
      02.08.2019 09:09

      Размер проекта примерно 114k+. 110 компонентов на ts (~200 еще остались на flow). По codemod, к сожалению, посоветовать ничего не могу, как и по type-guards (thunk используем только для getState())


  1. qbz
    01.08.2019 17:40

    1. dream_designer
      01.08.2019 18:53

      Может сначала стоит изучить инструмент? Это статический анализатор, а не магическая коробка, тип списка после `filter`не становится вдруг списком чисел, так как на выходе filter тип списка тот же, что и на входе.


      1. qbz
        01.08.2019 18:59

        Если вы про то, что filter и map можно завернуть в тот же reduce или про то, что isNumber не выносить за условие — то это бред, это не повышает производительность написания продукта ни разу. Если про что-то другое — поясните.


    1. ilyapirogov
      01.08.2019 19:41

      1. qbz
        01.08.2019 20:04

        Мде, печаль. С TS не работал, но думал это только Flow такой.


    1. Pongo
      01.08.2019 20:24

      В тс это решается тайпгуардами: пример.


      1. ilyapirogov
        01.08.2019 20:30

        Верно, я как раз посмотрел на сингнатуру filter https://github.com/microsoft/TypeScript/blob/master/src/lib/es5.d.ts#L1141 и увидел, что filter его таки использует. Так что, в принципе, будет работать даже такой вариант:


        [1, null, 3].filter((n): n is number => typeof n === 'number').map(n => n * 10);


      1. mayorovp
        02.08.2019 16:17

        С тайпгуардами отдельная проблема: сами-то они не тайпчекаются. Можно и так написать, и компилятор ничего не скажет:


        function isNumber(x : unknown) : x is number {
            return typeof x === "string";
        }


  1. disappearedstar
    01.08.2019 18:19

    > Но, к сожалению, к eslint-у можно подключить только один парсер.
    Можно переопределять парсер для разных типов файлов, см. пример

    > Проблема: Импорт компонентов/функций из разных типов файлов.
    Разве ж это проблема, если она решается парой строчек в конфигах?


    1. teager Автор
      02.08.2019 09:10

      Спасибо за подсказку про парсеры, будем иметь ввиду. А про импорт согласен, проблема действительно не сложная, просто в начале перехода не сразу на нее обратили внимание. Добавил в статью именно чтобы показать разный уровень сложности проблем


  1. Aquahawk
    02.08.2019 08:45

    Я сейчас вот с таким проектом работаю:

    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    JSON                            29             20              0         617229
    TypeScript                    3482          20179         148729         490138
    JavaScript                      35           1805           1438          18802
    HTML                            17            230              7           1757
    C                                1             95             28            580
    CSS                              7             50             45            476
    C/C++ Header                     1              9              3             27
    Bourne Shell                     1              1              0              6
    SVG                              1              0              0              1
    -------------------------------------------------------------------------------
    SUM:                          3574          22389         150250        1129016
    -------------------------------------------------------------------------------

    Тут огромный JSON это просто выгрузка из БД для независимости тестов от БД.
    Из 500K Тайпскрипт кода 200K это новое двигло и конвертер кода, 300К сконвертированный код приложения которое в оригинале написано на ActionScript 3.0

    И как же я рад что в 2016 году выбрал typescript. Ключевой момент для меня — открытость разработки, возможность обсуждать фичи, пул реквест который правит багу был смержен в мастер за 4 (!) часа. Закрытые процессы FB и близко рядом не стоят. Ну и вообще по общему впечатлению не жалею о выборе ни разу. Начал присматриваться к ts с версии 1.3 и хочу сказать что развитие впечатляющее.


    1. eugef
      02.08.2019 12:13

      Подскажите, чем вы такой отчет по проекту сделали?



  1. VolCh
    02.08.2019 10:29

    Два или три года на Flow, потом на чистом JS, теперь больше года на TS.


    Проблемы плюс-минус одни и те же, кроме поддержки в IDE.


    Код только для TS (тайпгварды или явные as/any/object) и тут приходится писать примерно в том же обёме, причём некоторые вроде очевидные проверки TS не ловит, например, при типе возвращаемого значения T|T[]|null|undefined (привет, react-select) простой проверки !Array.isArray недостаточно чтобы исключить T[] из обработки. Кажется, выражения условия isT(val) || val === null || val === undefined тоже не понимает как исключение T[].


    Некоторые типы писать очень сложно с одной стороны, а с другой типобезопасность в моём понимании из-за структурной типизации не очень: ошибки, когда при копипасте забыл заменить user на Organization, не ловит, если нужны только id и name


    В целом пока типы ведут себя так как ожидаешь, то профит от использования TS/Flow есть — ловит глупые ошибки. Но непонятно компенсирует ли это ситуации, когда полдня или больше тратишь на попытки обойти сообщения о ошибках типов системой типов без явного приведения или громоздких честных тайпгвардов, а в конце-концов просто делаешь какой-то хак.


    1. shaukote
      02.08.2019 12:39

      с другой типобезопасность в моём понимании из-за структурной типизации не очень: ошибки, когда при копипасте забыл заменить user на Organization, не ловит, если нужны только id и name
      Если использовать Flow с классами (вместо простых объектов), то такой проблемы не будет — Flow применяет к классам номинативную типизацию (или номинальную, не уверен, как лучше).


      1. Pongo
        02.08.2019 15:53

        В тс можно добиться похожего при помощи брендированных типов (есть еще несколько способов создания брендов)


        1. shaukote
          02.08.2019 16:16

          Интересно, не знал про такое. Хотя, как по мне, выглядит это, мммм, немножно как костыли. :)

          Я лично, напротив, не вижу проблемы в структурной типизации. Но может кому-то будет полезно узнать, что не обязательно страдать со структурной типизацией, когда она тебе поперёк горла.


    1. mayorovp
      02.08.2019 16:24

      например, при типе возвращаемого значения T|T[]|null|undefined (привет, react-select) простой проверки !Array.isArray недостаточно чтобы исключить T[] из обработки

      Странно, а у меня работает.


      Но на самом деле, это баг и работать оно не должно: ведь не любой T[] пройдет проверку isArray...


      1. VolCh
        02.08.2019 20:08

        Я попытался упростить для примера, видимо где-то что-то упустил.


        Разобрался что упустил, вам должно понравиться: не просто T[] а readonly T[]: не работает


        Вот этот баг похоже https://github.com/microsoft/TypeScript/issues/17002 с патчем https://github.com/microsoft/TypeScript/pull/28916


  1. shaukote
    02.08.2019 12:15

    Не совсем понял, как осуществляется взаимодействие сосуществующих TS и Flow?
    Допустим, в TS-файле импортируется и используется компонент, написанный на JS+Flow, откуда TS узнаёт сигнатуру этого компонента?


    1. VolCh
      02.08.2019 12:19

      А откуда узнаёт о сигнатуре JS-компонента?


      1. shaukote
        02.08.2019 12:23

        Не понял вопрос.
        Если речь про JS-компонент без аннотаций типов — понятно, что неоткуда, нужны отдельные libdef'ы.
        Но тут-то речь о существующей кодовой базе, в которой у компонентов уже есть аннотации типов на Flow.


        1. VolCh
          02.08.2019 14:15

          Нужные отдельные libdef или включение allowjs в том или ином виде. Аннотации Flow легко вырезаются и превращаются в обычные JS.


  1. shaukote
    02.08.2019 12:21

    двойная проверка на null
    Звучит странно и непонятно, хотелось бы больше деталей. У меня не получилось воспроизвести эту проблему, всё работает и без лишнего if'а.


    1. teager Автор
      02.08.2019 14:42

      Спасибо, немного поправил пример. Правда во flow песочнице ошибка так и не воспроизводиться (пробовал менял версию на нашу), а в проекте падает ошибка:
      Cannot call `this.activeFormContainer.style` because property ` style ` is missing in null or undefined [1].


      1. shaukote
        02.08.2019 16:32

        Спасибо, так стало понятнее.
        Не очень ясно, почему type refinements неправильно отрабатывает конкретно здесь, но в целом проблемы такого рода не так редки, как хотелось бы.
        FGJ, у TS с его type guards такие проблемы тоже есть (может, не этом конкретном случае, но на каком-то другом).


  1. rboots
    02.08.2019 16:29
    +1

    Тоже переводили проект с Flow на TypeScript. Мотивация — плохая диагностика ошибок и слишком большие усилия на поддержку типизации. С TypeScript работа пошла гораздо бодрее.
    Во Flow сначала привлекли зависимые типы и большая строгость, но все преимущества обесцениваются сырой реализацией.


    1. shaukote
      02.08.2019 16:34

      А что вы понимаете под «зависимыми типами» во Flow?


      1. rboots
        02.08.2019 20:26

        en.wikipedia.org/wiki/Dependent_type
        Сейчас действительно не смог их найти в документации к Flow, но, если я правильно помню, одно время говорили об их поддержке.


  1. ViacheslavRud
    02.08.2019 21:00

    "JavaScript – это один из языков с динамической типизацией. Такие языки удобны для быстрой разработки приложений"
    Каким образом они более удобны для быстрой разработки?
    Не первый раз слышу подобную фразу но всегда без объяснения этого удобства.


    1. arvitaly
      03.08.2019 01:07

      Не нужна компиляция, но сейчас есть куча инструментов для запуска компилируемых языков на лету, тот же ts-node, в частности. В GoLang это идет из коробки go run.


      1. rboots
        05.08.2019 17:20

        Вы путаете языки с динамической типизацией и интерпретируемые языки


    1. rboots
      05.08.2019 17:17
      -1

      Они удобны тем, что типы выводит компилятор и снимает с вас эту работу. Это может показаться мелочью, но когда вы передаёте какой-то сложный класс по цепочке функций, а потом переоперделяете или добавляете второй со слегка изменяющимся интерфейсом — это экономит уйму времени. Это так же имеет и недостатки, так как в больших приложениях компилятор может легко вывести типы, а программист — нет, поэтому явное указание типов просто как документация и помощь для IDE бывает полезным. Отсюда и появились языки вроде TypeScript. Можно сказать, что типизованные языки хороши для энтерпрайз, с большими приложениями и долгим сроком поддержки, а динамическая типизация — для стартапов, с коротким релизным циклом и высокой изменчивостью. Лично я всё равно миксую TypeScript и JavaScript, так как даже в рамках одного приложения разные модули могут обладать разными требованиями к стабильности и изменчивости, но это путь не для всех.


      1. VolCh
        05.08.2019 00:41

        Среди мэйнстрим языков не могу вспомнить языков динамической типизацией с компилятором, выводящим типы. Пример можно? Чтоб именно компилятор по умолчанию и именно типы выводил, а не присвавиал тип динамического выражения?


      1. morsic
        05.08.2019 00:44
        +1

        Вывод типов независит от компилируемости/интерпретируемости


    1. VolCh
      05.08.2019 00:38

      Можно менять типы данных в рантайме, а не пользоваться исключительно заданными или выведенными в компайлтайме. Возможность менять что-то удобнее невозможности, не так ли?


      Плюс очень-очень часто путают динамическую типизацию с нестрогой, с, например, неявным приведением к булевой истине значений отличных от ноля, null, пустой строки… Если что, Си — язык со статической, но не строгой типизацией. Некоторые относят его вообще к языкам без типизации как ассемблер.