The Typescript "as const" trick
Adam Coster

Adam Coster @adamcoster

About: Multidisciplinary hacker, doctor of biology, developer of full stack webs, maker of games. (he/him)

Location:
Saint Louis, MO, USA
Joined:
Oct 10, 2019

The Typescript "as const" trick

Publish Date: Aug 24 '20
41 5

Some time ago when I was first learning Typescript, I came across a snippet in a tutorial somewhere that looked something like this:

const myArray = ['hello','world',10] as const;
Enter fullscreen mode Exit fullscreen mode

Weird, right? Obviously it's a const, so what's the point of the as const?

If you use a Typescript-aware editor like VSCode, you'll see that the hover-text types for these two cases are completely different:

// shows up as: `const myArray: (string | number)[]`
const myArray = ['hello','world',10];

// shows up as: `const myArray: readonly ["hello", "world", 10]`
const myArray = ['hello','world',10] as const;
Enter fullscreen mode Exit fullscreen mode

In the first case we're treating the array as the const, and Typescript helpfully infers what kinds of things can go into that array.

In the second case the whole thing becomes constant, so it gets that readonly flag and we see the exact stuff that we put into that array, in the exact order, as the type!

So why is this useful?

Unfortunately, this doesn't prevent you from using mutators on your as const array in typescript (e.g. if you try to .push() something onto it, Typescript won't get upset). So it's a lie unless you wrap it in an Object.freeze.

One thing I've found it to be extremely useful for, however, is iterating over a defined subset of object keys:

const myObject = {
  hello: 'world',
  number: 10,
  anArray: [1,2,3],
  nested: {something: 'else'}
}

// Without using `as const`:
for(const field of ['hello','world']){
  // The type of `field` is just 'string'
}

// With `as const`:
for(const field of ['hello','world'] as const){
  // The type of `field` is 'hello'|'world'
}
Enter fullscreen mode Exit fullscreen mode

That difference between having exact versus general type information can make all the difference between something being difficult or easy in Typescript.

Unfortunately, JSDocs don't have support for this, so using this trick in vanilla JavaScript requires a workaround:

/** Thanks to {@link https://github.com/microsoft/TypeScript/issues/30445#issuecomment-671042498} */

/**
 * Identity function. Coerces string/number literals to value-as-type.
 * @template {string|number} T
 * @param {T} v
 * @return {T}
 */
function toConst(v) {
  return v;
}

const five = toConst(5);
// --> Type shows up as 5 instead of "number"

/**
 * Creates an array from the given arguments, type as a constant tuple.
 * @template {(string|number)[]} T
 * @param {T} v
 * @return {T}
 */
function toConstTuple(...v) {
  return v;
}

const tuple = toConstTuple("Hello","World",10);
// --> Type shows up as ["Hello","World",10] instead of (string|number)[]
Enter fullscreen mode Exit fullscreen mode

It's a little weird to wrap your values in a function that does nothing but let the Typescript language server give you a readonly type. But it works, at least for these limited cases.

Comments 5 total

  • Orta
    OrtaAug 24, 2020

    Nope, looks like the JSDoc types parser doesn't know about as const at all, you'd need to re-type it inside an @types to get the literal types

    /** @type {Readonly<{ b: "thingy"}>} */
    const b = {
      b: "thingy"
    }
    
    b.b // will be "thingy" in the type system
    
    Enter fullscreen mode Exit fullscreen mode
    • Adam Coster
      Adam CosterAug 24, 2020

      Yep, that's the headache I've been trying to avoid. It seems that with Typescript it's generally possible to avoid DRY violations between types and expressions, but it's pretty hard to avoid with JSDoc.

  • xeho91
    xeho91Jul 3, 2021

    Thank you!

    I noticed the same across the code. Unfortunately, the TypeScript documentation didn't help explain how it exactly helps.

    Your article explained straight to the point in a quick time. Kudos!

  • Orta
    OrtaFeb 23, 2022

    Somehow I ended up back in this article while looking for something else, so it only seems right to note that JSDoc as consts exists nowadays

    const b = /** @type {const} */ ({
      b: "thingy"
    })
    
    b.b // will be "thingy" in the type system
    //^?
    
    Enter fullscreen mode Exit fullscreen mode

    ( Also a big fan of your work, I completed Crashlands back when it came out on iOS )

  • Marko Bjelac
    Marko BjelacMar 2, 2023

    Unfortunately, this doesn't prevent you from using mutators on your as const array in typescript (e.g. if you try to .push() something onto it, Typescript won't get upset). So it's a lie unless you wrap it in an Object.freeze.

    Maybe they made an improvement after you published this, but since 4.5.4 this is not possible:

    const arr = [1, 2, 3] as const;
    
    arr.push(4);
    // TS2339: Property 'push' does not exist on type 'readonly [1, 2, 3]'.
    
    arr.length = 0;
    // TS2322: Type '0' is not assignable to type '3'.
    
    Enter fullscreen mode Exit fullscreen mode
Add comment