Pull to refresh

TypeScript: не выходите за рамки

Reading time4 min
Views4K

О том, как снизить риск дефектов программы на TypeScript, величину технического долга проекта и, одновременно, улучшить читаемость кода TypeScript.

Фабрика кэша

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

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

const createCache = <T>()=>{
  const items:T[] = [];
  return {
    getItem: (key:number)=>items[key],
    setItem: (key:number, item:T)=>{items[key]=item;},
  };
};

Обратитесь к песочнице и выясните каким образом TypeScript типизирует результат создания кэша.

const {getItem, setItem} = createCache<string>();

В частности getItem имеет тип (key: number) => string. Правда ли что в кэше всегда есть нужное нам значение? Нет! Ведь значение в кэш нужно еще положить. До этого момента результат getItem не возвращает строку.

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

Быть может ошибочное суждение о типе результата доступа по индексу характерно для TypeScript только в отношении массива? Посмотрим.

Индекс ассоциативного массива (чисто объекта)

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

interface Dictionary<T> {
  [term: string]: T;
}
const createCache = <T>()=>{
  const items:Dictionary<T> = {};
  return {
    getItem: (key:string)=>items[key],
    setItem: (key:string, item:T)=>{items[key]=item;},
  };
};

const {getItem, setItem} = createCache<string>();

И все равно getItem, по мнению TypeScript, возвращает строку.

Риск дефекта

Уже два примера убеждают нас, что TypeScript где-то не дорабатывает. Как компилятор, так и сервисы в редакторе, в один голос твердят, что результатом работы функции getItem будет строка. Мы, поверив компилятору, попытаемся работать с неинициализированным значением дальше и, рано или поздно, наткнемся на ошибку во время исполнения. Ошибка-то будет где-то у клиента. Пойди, найди причину!

Ручное управление

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

const createCache = <T>() => {
  const items: T[] = [];
  return {
    getItem: (key: number) => items[key] as T | undefined,
    setItem: (key: number, item: T) => { items[key] = item; },
  };
};

const { getItem, setItem } = createCache<string>();

Замечание. в отличие от c#, например, в TypeScript утверждение типа `as type` не влечет за собой никаких накладных расходов во время исполнения программы. Этот оператор используется на этапе статического анализа кода и просто удаляется после компиляции. В результирующую программу он не попадает.

В отрывке приведенном ниже, сразу после инициализации считается, что переменная item содержит значение типа string | undefined. Нам приходится провести проверку полученного значения, тем самым устранить риск дефекта.

TypeScript дальше проявляет свою сообразительность и понимает, методом исключения, раз не undefined, значит string и радостно компилирует программу без ошибок.

const { getItem, setItem } = createCache<string>();

const item = getItem(0);
if(typeof item !== "undefined"){
  console.log(item.toUpperCase());
}

Но мы можем поступить лучше.

Флаги строгости: noUncheckedIndexedAccess

Компилятор tsc TypeScript-а управляется флагами из tsconfig.json. Cреди прочих флагов есть флаги "строгости". Для включения многих из них достаточно указать флаг strict. Установка этого флага неявно включает, в частности, все флаги, имена которых начинаются со слова strict.

Флаг noUncheckedIndexedAccess включается отдельно. Но когда он включен, TypeScript автоматически объединяет тип значения, полученного доступом по индексу, и тип undefined, таким образом заставляя нас перепроверить наличие полезного значения каждый раз, когда мы извлекаем что-то через квадратные скобки.

В этой песочнице, на закладке TSConfig установлен флаг noUncheckedIndexedAccess и как результат, тип функции getItem магически улучшился до: (key: number) => string | undefined. Ручная настройка определения функции не потребовалась.

В сухом остатке

Разработка программы для повседневного использования, а не для игры в песочнице, часто сопровождается накоплением технического долга. В погоне за скоростью, разработчик может принять решение позднее вернуться к коду и добавить аннотацию типа T | undefined. И, конечно, не возвращается, пока не придет время исправлять обнаруженный заказчиком дефект.

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


И, вот когда с типами все стало лучше, пользователь отправил в кэш значение с ключом (-1)... Как быть, ведь в TypeScript нет безнаковых целых чисел?... Об этом в следующий раз.

Tags:
Hubs:
Rating0
Comments10

Articles