Petal

How to benefit from const assertion in your TypeScript code?

Const assertion or "as const" has been supported in TypeScript since late 2019, yet many developers still don’t know about it. What are the benefits it brings, and can they improve any project?


Ondrej PolesnyPublished on Apr 3, 2023

In this article, I’ll explain const assertion in TypeScript, show how to use it, and explain the benefits it brings. I will also share two examples of how we use const assertion on this website and touch on the support for strongly typed content resolution using the Kontent.ai model generator.

What is const assertion?

Let’s say we have a variable:

let text = "Hello"

We can always change the value of the variable:

text = "Hello world"

And the type of the text variable will be a string. Now, when we use const assertion:

let text = "Hello" as const

We effectively create a type "Hello", not string. And while you can still change the value, you can only change it to "Hello" and not anything else.

The same can be applied to objects and arrays:

const person = {
    name: "Ondrej",
    age: 34
}

You can now change the person object’s properties.

person.name = "Peter"

But if you use the const assertion:

const person = {
    name: "Ondrej",
    age: 34
} as const

The object’s properties’ values are now read-only. Const assertion looks at the used values, infers the most specific type possible, and applies it.

Now, why is this beneficial, and when can you use it? It mainly helps with:

  • Type safety
    You can ensure that a variable or property has a specific value and nothing else and catch potential errors at compile time.
  • Debugging
    You can make objects and their properties used in multiple places throughout your project read-only, which helps avoid errors and unintentional mutations at a compile time. You will know you’re making a mistake while you’re writing the code.
  • Readability
    You no longer need to explicitly define types everywhere in your code when they can be inferred from the values.

Using const assertion in a real project

Let me show you two examples of how we use const assertion in our project:

export const SearchCategories = {
	KontentAi: {
		title: 'Kontent.ai',
		tag: 'global-search-result__tag--kontent',
		priority: 5
	},
	BlogPost: {
		title: 'Blog post',
		tag: 'global-search-result__tag--blog',
		priority: 3
	},
        ...
} as const

export type SearchCategoryTitle = typeof SearchCategories[keyof typeof SearchCategories]['title']

This code defines search categories – their titles, tags CSS classes, and priority.

Now, the const assertion helps here narrow down the type of the search categories’ titles which we then use in an interface that unifies data for a search engine:

export interface ISearchRecord {
	objectID: string
	title: string
	text: string
	category: SearchCategoryTitle
	...
}

The extracted type SearchCategoryTitle is essentially this:

type SearchCategoryTitle = "Kontent.ai" | "Blog post" | ...

As opposed to a simple string if we didn’t use as const.

This is great as we get the type safety on the level of the search categories titles. It prevents any developer to assign a custom value that would break the front-end part of the search functionality.

Safe rich text resolution with const assertion

Another example is the marketing landing pages on this site. Editors can compose them using predefined components, and we resolve those components in code when we’re pre-building the site. During that process, we need to:

  • Make sure each used component in the CMS is allowed for the UMLP page; that is, we have a React component for it
  • Find the React component corresponding to the used content type and render it using the respective content item data
Kontent.ai

We do this using a mapper from content type to React component:

export const umlpMapper = {
	[contentTypes.umlp_element___breadcrumbs.codename]: UmlpBreadcrumbs,
	[contentTypes.umlp_element___hero_form.codename]: UmlpHeroForm,
	...
}

type UmlpCodenames = keyof typeof umlpMapper

So where is the const assertion? It’s actually in the model generator. Note that we’re not using string literals to define content types’ codenames. Instead, we’re taking them from the generated contentTypes file, which indeed marks them with as const keyword.

export const contentTypes = {
    ...
    umlp_element___breadcrumbs: {
        codename: 'umlp_element___breadcrumbs',
        elements: { ... }
        ...
    }
} as const

In this case, the type UmlpCodenames will be this:

// type UmlpCodenames = "umlp_element___breadcrumbs" | "umlp_elements___hero_form"

If there was no const assertion in the generated file, the type would look like this:

// type UmlpCodenames = string | number

Which would be useless for any additional type of safety checks. Now, having the type more specific, we can do a number of checks at compile time and ensure that each React component receives the data in the format it expects. We do that using a list of UmlpBinding types and a type guard:

type UmlpBinding<C, M, F> = { codename: C; model: M; Component: F }
type UmlpReactComponents = typeof umlpMapper[keyof typeof umlpMapper] // React.FC<...> | React.FC<...> | ...

// initialize all possible UMLP component bindings
type CodenameModelBinding = {
	[k in UmlpComponentCodenames]: UmlpBinding<
		k,
// All supported React components have common 'model'
// property where they accept the content item data.
// Here, we're extracting its type for the binding.
		React.ComponentProps<typeof umlpMapper[k]>['model'], 
		typeof umlpMapper[k]
	>
}[UmlpCodenames]

// type guard for checking existence of the binding
export const isUmlpComponentBinding = (
	binding: UmlpBinding<string, IContentItem, UmlpComponents>
): binding is CodenameModelBinding => (Object.keys(umlpMapper) as string[]).includes(binding.codename)

...

// usage while resolving components
const Binding = {
	codename: contentItem.system.type,
	model: contentItem,
	Component: umlpMapper[contentItem.system.type],
}

if (!isUmlpComponentBinding(Binding)) {
...
}

Now, this code sample is way beyond the const assertion part of this article, so I won’t go into too much detail here. However, const assertion allowed us to:

  • Introduce compile-time type checks for resolved components even though we didn’t know which components and data we’ll receive until runtime
  • Remove 1000 lines long large switch statement that used to handle these components and their data one by one

Conclusion

In this article, I explained what const assertion or as const is and how it can help with type safety, readability, and debugging of your code. I also shared two examples of how we use const assertion in our web project. Const assertion is a very powerful tool that has the potential to make your code safer and your development more effective.

If you’re interested in learning more about any of the examples mentioned above or just want to talk to other developers in the Kontent.ai community, get in touch on our Discord server.

Written by

Ondrej Polesny

As Developer Evangelist, I specialize in the impossible. When something is too complicated, requires too much effort, or is just simply not possible, just give me a few days and we will see :).

More articles from Ondrej