I love string literal union types in TypeScript. I came across a simple case where I was expecting the union type to be retained.
Here is a simple version:
let foo = false;
const bar = foo ? 'foo' : 'bar';
const foobar = {
bar
}
bar
is correctly typed as 'foo' | 'bar'
:
But foobar.bar
gets typed as string
:
Just curious why.
So @jcalz and @ggradnig do make good points. But then I realized my use case had an extra twist:
type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';
const foobar = {
bar
}
Interestingly, bar
does have a type of Status
. However foobar.bar
has a type of 'foo' | 'bar'
still.
It seems that the only way to make it behave how I was expecting is to cast 'foo'
to Status
like:
const bar = foo ? 'foo' as Status : 'bar';
In that case, the typing does work properly. I am OK with that.
The compiler uses some heuristics to determine when to widen literals. One of them is the following:
- The type inferred for a property in an object literal is the widened literal type of the expression unless the property has a contextual type that includes literal types.
So by default, that "foo" | "bar"
gets widened to string
inside the object literal you've assigned to foobar
.
You can now use const
assertions to ask for narrower types:
const foobar = {
bar
} as const;
/* const foobar: {
readonly bar: "foo" | "bar";
} */
The literally()
function in the rest of this answer may still be of some use, but I'd suggest using as const
where possible.
Note the part of the heuristic that says "unless the property has a contextual type that includes literal types." One of the ways to hint to the compiler that a type like "foo" | "bar"
should stay narrowed is to have it match a type constrained to string
(or a union containing it).
The following is a helper function I sometimes use to do this:
type Narrowable = string | number | boolean | symbol | object |
null | undefined | void | ((...args: any[]) => any) | {};
const literally = <
T extends V | Array<V | T> | { [k: string]: V | T },
V extends Narrowable
>(t: T) => t;
The literally()
function just returns its argument, but the type tends to be narrower. Yes, it's ugly... I keep it in a utils library out of sight.
Now you can do:
const foobar = literally({
bar
});
and the type is inferred as { bar: "foo" | "bar" }
as you expected.
Whether or not you use something like literally()
, I hope this helps you; good luck!