可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
Update
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.
回答1:
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
.
Note the part 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!
回答2:
That's because let
and const
are handled differently by TypeScript. Constants are always treated with the "narrower" type - in this case the literals. Variables (and non-readonly object properties) are treated with the "widened" type - string
. This is a rule that comes along with literal types.
Now, while your second assignment may be a constant, the property of that constant is in fact mutable - it's a non-readonly property. If you don't provide a "contextual type", the narrow inference gets lost and you get the wider string
type.
Here you can read more about literal types. I may quote:
The type inferred for a const variable or readonly property without a type annotation is the type of the initializer as-is.
The type inferred for a let
variable, var
variable, parameter
, or non-readonly property
with an initializer and no type annotation is the widened literal type of the initializer.
And even more clearly:
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.
By the way, if you provide the contextual type for the constant, the type will be passed on to the variable:
const bar = foo ? 'foo' : 'bar';
let xyz = bar // xyz will be string
const bar: 'foo' | 'bar' = foo ? 'foo' : 'bar';
let xyz = bar // xyz will be 'foo' | 'bar'
回答3:
To answer the updated question with three-value literal union type:
type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';
Declared type of bar
is Status, but it's inferred type is still narrowed by control flow analysis to only two possible values out of three, 'foo' | 'bar'
.
If you declare another variable without a type, TypeScript will use inferred type for bar
, not the declared type:
const zoo = bar; // const zoo: "foo" | "bar"
Without resorting to type assertion as Status
, there's no way to turn off type inference based on control flow analysis other than explicitly declaring the type at the place where you need it:
const foobar: {bar: Status} = {
bar // has Status type now
}