Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
1.2k views
in Technique[技术] by (71.8m points)

why is TypeScript converting string literal union type to string when assigning in object literal?

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':

enter image description here

But foobar.bar gets typed as string:

enter image description here

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.


UPDATE FOR TS 3.4

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!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to WuJiGu Developer Q&A Community for programmer and developer-Open, Learning and Share
...