Привет, Хабр!

В разработке часто возникают ситуации, когда точность типов и нежелание допускать неясности в коде становятся первостепенными задачами. В таких случаях, разработчикам приходится искать инструменты, предоставляющие максимальную ясность и строгость в определении данных. Один из таких инструментов — ключевое слово as const. В данной статье мы рассмотрим, как as const может повысить уровень строгости и предсказуемости, а также рассмотрим практические примеры его использования для создания неизменяемых и точных типов.

Подробнее о “as const”

Когда вы используете as const для переменной или значения, TypeScript уточняет тип этой переменной до ее точного значения или комбинации литеральных типов. Это часто используется для создания неизменяемых значений и гарантирования того, что TypeScript будет рассматривать значения как конкретные литералы, а не расширять типы.

Пример:

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

const wallet = {
  key: "open_door_pls"
}

const openDoor = (key: "open_door_pls") => {
  //...
}

Но, когда мы пытаемся использовать функцию openDoor с ключом из нашего бумажника, почему-то происходит следующее:

const wallet = {
  key: "open_door_pls"
}

const openDoor = (key: "open_door_pls") => {
  //...
}

openDoor(wallet.key) // ERROR: Argument of type 'string' is not assignable to parameter of type '"open_door_pls"'

Почему же мы попали в такую ситуацию?

Всё дело в том, что wallet.key у нас никак не привязан к значению "open_door_pls”, и по сути является просто элементом с типом string, значение которого можно легко изменить на любое другое:

const wallet = {
  key: "open_door_pls"
}

wallet.key = "cucumber" // код отрабатывает без ошибок

Чтобы избежать такого поведения, и сделать все элементы wallet полноценными, неизменяемыми (readonly) значениями, мы можем воспользоваться конструкцией as const:

const wallet = {
  key: "open_door_pls"
} as const

wallet.key = "cucumber" // ERROR: Cannot assign to 'key' because it is a read-only property.

Элементы wallet привязались к своим значениям и теперь имеют флаг readonly.

Что в конечном итоге решило нашу проблему с параметром функции openDoor:

const wallet = {
  key: "open_door_pls"
} as const;

const openDoor = (key: "open_door_pls") => {
  //...
};

openDoor(wallet.key) // код отрабатывает без ошибок

Почему не Object.freeze()?

Проблема Object.freeze() заключается в том, что после заморозки объекта, readonly присваевается только элементам на верхнем уровне вложенности.

const car = Object.freeze({
  name: "Porshe Cayenne",
  equipment:{
    engine: "MDC.AB"
  }
});

car.equipment.engine = "F8CV"; // без проблем изменили двигатель

При использовании as const компилятор на любом уровне вложенности укажет, что программист пытается изменить константу.

const car = {
  name: "Porshe Cayenne",
  equipment:{
    engine: "MDC.AB"
  }
} as const;

car.equipment.engine = "F8CV"; // ERROR: Cannot assign to 'engine' because it is a read-only property.

Замена enum-ам?

as const превосходно подходит в качестве альтернативы enum.

О минусах enum можно подробно почитать в этой статье.

Пример перехода кода с enum на as const:

Код, с использованием enum:

enum Wallet {
  KEY = "open_door_pls"
};

const openDoor = (key: "open_door_pls") => {
  //...
};

openDoor(Wallet.KEY)

Код, переписанный на as const:

const wallet = {
  key: "open_door_pls"
} as const;

const openDoor = (key: "open_door_pls") =>{
  //...
};

openDoor(wallet.key);

Ещё одна действительно крутая особенность as const заключается в том, что использование as const позволяет быть очень гибким в обращении с ключевым объектом:

const friendsDict = {
  Alfred: "101 Pine Road, Toronto, ON M5A 1A1, Canada",
  Liam: "777 Sycamore Lane, Tokyo, 100-0001, Japan",
  Mia: "666 Willow Street, Paris, 75001, France",
} as const;

type FriendName = keyof typeof friendsDict; // "Alfred" | "Liam" | "Mia"
type FriendAddress = (typeof friendsDict)[keyof typeof friendsDict];
//"101 Pine Road, Toronto, ON M5A 1A1, Canada" | "777 Sycamore Lane, Tokyo, 100-0001, Japan" | "666 Willow Street, Paris, 75001, France"

Резюмируя

Мы рассмотрели конструкцию as const в TypeScript и её роль в создании более строгих и предсказуемых типов данных. Надеюсь, что данная статья помогла вам лучше понять, как использование as const может повысить уровень безопасности и ясности вашего кода.

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

До новых встреч в мире строгой типизации!

Ещё полезностей по теме

as const: the most underrated TypeScript feature

Enums considered harmful

Use 'as const' in TypeScript

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


  1. edtech
    06.01.2024 22:45
    +4

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

    type OpenDoorKey = 'open_door_pls';
    
    const wallet = {
      key: "open_door_pls" as OpenDoorKey,
    };
    
    function openDoor(key: OpenDoorKey): void {
      console.log("The door is now open!");
    }
    
    openDoor(wallet.key);
    


    1. meonsou
      06.01.2024 22:45

      Тогда лучше заменить as OpenDoorKey на as const satisfies OpenDoorKey чтобы нельзя было рандомную строку туда вписать


  1. SWATOPLUS
    06.01.2024 22:45
    +4

    1. Употребляйте термин строгая типизация корректно. В статье идёт речь про точные типы или литеральные типы. Строгая типизация это отсутствие неявного преобразования типов и отсутствие каламбура типов.

    2. Статье для полноты не хватает реализации метода deepFreeze с поддержкой точного типа. as const уточняет тип и выдает ошибки модификации во время транспиляции. Но если передать этот объект js-коду или сделать as any, то можно будет модифицировать этот объект. Поэтому важно не только делать тип as const но и защищать объект от посягательств кода третьих лиц.


    1. Evgeniiit
      06.01.2024 22:45

      Отличные замечания. А а какой момент может произойти модификация объекта? (В сорце? В рантайме? При транспиляции?)


    1. SolovevSerg
      06.01.2024 22:45
      +1

      Кстати, чтобы написать такую функцию deepFreeze, весьма полезен новый модификатор const для праметров типов в дженериках из TypeSscript 5. Вот простейший пример реализации с ним:

                        /* ???? */ 
      function deepFreeze<const T extends object>(obj: T): T {
          Object.freeze(obj);
      
          Object.values(obj)
              .filter(nested => nested !== null && typeof nested === 'object')
              .forEach(deepFreeze);
      
          return obj;
      }
      
      // const frozen: readonly ["a", {readonly b: 1;}]
      const frozen = deepFreeze(["a", {b: 1}]);

      Без него всякий раз пришлось бы лишний раз писать as const

      const frozen = deepFreeze(["a", {b: 1}] as const);

      Подробнее про модификатор const в документации TS.


  1. stryaponoff
    06.01.2024 22:45
    +2

    По поводу использования const assertion в качестве «замены enum»: рекомендую посмотреть в сторону конструкции `const enum` — она объединяет лучшее из обоих миров.


    1. ArchieSup Автор
      06.01.2024 22:45
      +1

      Хороший вариант. Главное помнить о подводных камнях:
      https://www.typescriptlang.org/docs/handbook/enums.html#const-enum-pitfalls


  1. cijic
    06.01.2024 22:45
    -2

    Начались костыли как в JS.

    Вам не кажется что писать const ... as const говорит об изначальном косяке в TS, который надо было исправить в любой версии, а не добавлять тавтологию?


    1. Format-X22
      06.01.2024 22:45

      Дело в том что const делает неизменной переменной, только вот объекты по ссылке передаются. Защита от изменения у ссылки остается, а вот у внутренностей нет. И хороший вопрос что лучше - защищать целиком или всё же только верхний уровень, удобство кардинально разное. В JS выбрали только ссылки, TS поддержали. Возможно имело бы смысл сделать final в дополнении к const и let, для полной заморозки, но пока дополнительно дописывается дабы не вводить новое ключевое слово.


  1. Format-X22
    06.01.2024 22:45
    +1

    Object.freeze работает на уровне JS и замораживает объект по настоящему, а вот as const лишь на уровне TS и не защищает от изменений вне компилятора TS. Механизмы очень разные, стоит помнить об этом.


    1. modelair
      06.01.2024 22:45

      любая возможность TS работает только в рамках компилятора, а не только as const. это ж всего-лишь декорации


      1. Format-X22
        06.01.2024 22:45

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


  1. StiPAFk
    06.01.2024 22:45

    все аргументы из статьи почему enum плох разваливаются об ТС5 (намберы пофиксили) или о понимание почему важно использовать Branding для перечисления.

    const enum лучшее решение.



  1. TEMN1J
    06.01.2024 22:45

    Смотрю со своей колокольни строго типизированных языков, и просто офигеваю...