Home > Software design >  Typescript const a = 'a' is not equal to const a = 'a' as const and const a:
Typescript const a = 'a' is not equal to const a = 'a' as const and const a:

Time:08-11

Ok, another thing where TypeScript confuses me:

Normally we can skip type annotation because we know that TypeScript will infer the type for us.

But it turns out with or without type annotation does make real difference in some cases:

const a = 'a' // 'a'
//    ^?
const c = [a] // string[]
//    ^?

const a1 = 'a' as const // 'a'
//    ^?
const c1 = [a1] // 'a'[]
//    ^?

const a2:'a' = 'a' // 'a'
//    ^?
const c2 = [a2] // 'a'[]
//    ^?

playground

So now I know how it works.

But my question: why is in case c, it is widen to string[] and how is it useful?

NOTE: This is not about how array or tuple works, nor is about how as const and type annotation work. The question is about why c does not follow c1 and c2, or c1 and c2 do not follow c, why not everything is tuple or everything is array, why c has to be different?

c also widen to string which is not preferrable because widening mean not safer, what is the point keeping c widen?

CodePudding user response:

The details of how this works are mostly laid out in microsoft/TypeScript#10676. String literals like "a" are always initially given literal types by the compiler, but depending on context, these types are either considered "widening" or "non-widening". Widening literal types will automatically get widened (so "a" will become string) in certain circumstances, while non-widening types will not (so "a" will stay "a").

In the following:

const a = 'a' // 'a'
const c = [a] // string[]

The type inferred for the a variable is a widening literal type "a", because the initializer "a" occurs in an expression. And so the type inferred for [a] is string[], because a is of a widening type.

On the other hand,

const a2:'a' = 'a' // 'a'
const c2 = [a2] // 'a'[]

the type inferred for a2 is a non-widening literal type "a", because you annotated it as such, and that "a" occurs in a type not an expression. Indeed, annotating variables with literal types is the recommended way to get a non-widening type instead of a widening one.

Finally, in

const a1 = 'a' as const // 'a'
const c1 = [a1] // 'a'[]

the type of a1 is a non-widening "a" type because const assertions always result in non-widening types as described in microsoft/TypeScript#29510.

So that explains exactly why you're seeing what you're seeing.


The question about why it is useful is harder to answer authoritatively, because utility is subjective. You can read through microsoft/TypeScript#10676 and microsoft/TypeScript#11126 and issues linked to them to get more context.

The general issue is that sometimes people want a value like "a" to be treated literally as an unchanging value, whereas other times people want it to be treated as just a string. There's a tradeoff there. And unless people write out their intent explicitly, the compiler has to infer what this intent is, necessarily using heuristics that can sometimes be wrong.

If I write const a = "a" then I know it will never change, so the unchanging and narrow "a" type is appropriate. Treating a as string isn't wrong, but it throws away information.

If I write let b = "b" then presumably I'm going to change that value (otherwise I'd have used const) and so the string type is more appropriate.

If I write const c = [a] then even though a is the type "a", it's not clear that I expect c to always and forever contain only elements of the type "a". That's... strange, right? Without more context it's hard to imagine why someone would want an array of the letter "a". The type string[] is much more common and so inferring c as string[] is reasonable.

If you go out of your way to tell the compiler that a is of type "a" then the heuristics use this. If you have const a: "a" = "a", now the compiler has some indication that you really care about that literal type, and now const c = [a] infers type "a"[] for c. I still think this is a strange type (I suppose you can manipulate the number of "a"s in the array) but that's what inference gives you. If I don't like this I can write const c: string[] = [a] and clarify my intent.

Whether or not one this behavior useful is, again, a matter of opinion. I can only give my own opinion (it is mostly useful and occasionally it gets things wrong, at which point I can use explicit annotations) and point to the documented GitHub issues.

CodePudding user response:

It boils down to how most programs are written by developers, and how TypeScript team tries to adjust to their supposed intentions.

The very most common case is your first example:

const a = 'a' // 'a'
//    ^?
const c = [a] // string[]
//    ^?

// typically later:
c.push("some string content")

Should TS infer a tuple, or an array of literal value ('a' as opposed to string type), it would be too strict and many common array usages would become false positive (TS erroring against the actual and legit developer's intention).

As for the other 2 examples (c1 and c2), the TS team decided to infer an array of literal value, because the developer does give a much bigger hint about a literal value being handled.

Of course, this assumption may be wrong, in which case the developer needs to explicitly type the array (e.g. Array<'a'|'b'>).

  • Related