What causes TypeScript to convert a string literal union type to a string when it is assigned in an object literal?

I am a big fan of string literal union types in TypeScript. Recently, I encountered a situation where I expected the union type to be preserved.

Let me illustrate with a simple example:

let foo = false;
const bar = foo ? 'foo' : 'bar';

const foobar = {
    bar
}

The variable bar is correctly typed as 'foo' | 'bar':

However, foobar.bar is being typed as string:

This discrepancy got me thinking.

Update

After considering points made by @jcalz and @ggradnig, I realized that my use case had an additional complexity:

type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';

const foobar = {
    bar
}

Interestingly, bar does have the correct type of Status, but foobar.bar still has a type of 'foo' | 'bar'.

It appears that to align with my expectations, I need to cast 'foo' to Status like this:

const bar = foo ? 'foo' as Status : 'bar';

With this adjustment, the typing behaves as desired and I am satisfied with it.

Answer №1

When deciding to widen literals, the compiler applies various heuristics. One such heuristic is:

  • 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 default, a

"foo" | "bar"
gets widened to string within the assigned object literal for foobar.


UPDATE FOR TS 3.4

In TypeScript 3.4, you have the option to utilize const assertions to request narrower types:

const foobar = {
    bar
} as const;
/* const foobar: {
    readonly bar: "foo" | "bar";
} */

While the literally() function discussed later in this answer may still be relevant, it's recommended to use as const when possible.


Note the condition in the heuristic stating "unless the property has a contextual type that includes literal types." To indicate to the compiler that a type like

"foo" | "bar"
should remain narrowed, you can have it match a constrained type to string (or a union containing it).

Below is a utility function that can help achieve 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 simply returns its argument, but the type tends to be narrower. While it may not be elegant, I typically keep it in a utilities library hidden from view.

Now you can use:

const foobar = literally({
  bar
});

and the type will be inferred as

{ bar: "foo" | "bar" }
, aligning with your expectations.

Whether or not you employ something like literally(), I trust this information proves beneficial; best of luck!

Answer №2

This is due to the fact that TypeScript treats let and const differently. Constants are always assigned a "narrower" type, which in this case refers to literals. On the other hand, variables (and non-readonly object properties) are given a "widened" type, specifically string, following the rule associated with literal types.

Even though your second assignment involves a constant, the property of that constant is actually mutable as it is a non-readonly property. Without providing a specific "contextual type", the narrow inference is lost and you end up with the broader type of string.

If you want to delve deeper into literal types, you can check out this link. Here's an excerpt:

The inferred type for a const variable or readonly property without a type hint is directly based on the initializer value.

For a let variable, var variable, parameter, or a non-readonly property with an initializer but no explicit type, the widened literal type of the initializer is used.

To clarify further:

The type automatically determined for a property within an object literal is the expanded literal type of the expression, unless there is already a contextual type containing literal types.

Additionally, if you assign a contextual type to the constant, that type will extend to the variable as well:

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'

Answer №3

Providing a solution for the updated query using a three-value literal union type:

type Status = 'foo' | 'bar' | 'baz';
let foo = false;
const bar: Status = foo ? 'foo' : 'bar';

The declared type of bar is Status, but it undergoes control flow analysis and gets narrowed down to only two possible values out of the three, which are 'foo' | 'bar'.

If you define another variable without specifying a type, TypeScript will automatically infer the type based on the assignment to bar, not the explicitly declared type:

const zoo = bar; // const zoo: "foo" | "bar"

Unless you use type assertion like as Status, there's no way to disable type inference from control flow analysis other than stating the type explicitly where necessary:

const foobar: {bar: Status} = {
    bar // now has the type Status
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Can a Typescript class type be defined without explicitly creating a JavaScript class?

I am exploring the idea of creating a specific class type for classes that possess certain properties. For example: class Cat { name = 'cat'; } class Dog { name = 'dog'; } type Animal = ???; function foo(AnimalClass: Animal) { ...

How can I obtain my .apk file?

I want to convert my app into .apk format. I inserted the following scripts on my "package.json" page: "build:development:android": "ionic cordova build android" and "build:production:android": "ionic cordova build android --prod --release". However, I ...

Encountering a problem with Webpack SASS build where all files compile successfully, only to be followed by a JavaScript

Being a newcomer to webpack, I am currently using it to package an Angular 2 web application. However, I am encountering errors related to SASS compilation and the ExtractTextPlugin while working on my project. Here is a snippet from my webpack configurat ...

Utilizing Mongoose RefPath in NestJS to execute populate() operation

After registering my Schema with mongoose using Dynamic ref, I followed the documentation available at: https://mongoosejs.com/docs/populate.html#dynamic-ref @Schema({ collection: 'quotations' }) export class QuotationEntity { @Prop({ r ...

Nested formArrays within formArrays in Angular 4

I've been working on implementing a FormArray inside another FormArray, but it doesn't seem to be functioning correctly. I also tried the solution provided in the link below, but it didn't work for me. How to get FormArrayName when the Form ...

Tips for organizing your Typescript code in Visual Studio Code: avoid breaking parameters onto a

Currently, I am working on an Angular project using Visual Studio Code and encountering an irritating issue with the format document settings for Typescript files. It keeps breaking parameters to a new line: Here is an example of the code before formattin ...

Delivering static HTML routes in a React App using Typescript

I am working on a React app with functional components, and everything is working perfectly with its own CSS. Now, I have a separate static HTML file (FAQ) with its own CSS and design that I want to incorporate as a new route at /FAQ. I don't want th ...

What is the best way to invoke a function in a functional React component from a different functional React component?

I need to access the showDrawer method of one functional component in another, which acts as a wrapper. What are some best practices for achieving this? Any suggestions or insights would be appreciated! const TopSide = () => { const [visible, se ...

Ways to speed up the initial loading time in Angular 7 while utilizing custom font files

Storing the local font file in the assets/fonts folder, I have utilized 3 different types of fonts (lato, raleway, glyphicons-regular). https://i.stack.imgur.com/1jsJq.png Within my index.html under the "head" tag, I have included the following: <lin ...

The cause of Interface A improperly extending Interface B errors in Typescript

Why does extending an interface by adding more properties make it non-assignable to a function accepting the base interface type? Shouldn't the overriding interface always have the properties that the function expects from the Base interface type? Th ...

Assign a predetermined value to a dropdown list within a FormGroup

I have received 2 sets of data from my API: { "content": [{ "id": 1, "roleName": "admin", }, { "id": 2, "roleName": "user", }, { "id": 3, "roleName": "other", } ], "last": true, "totalEleme ...

Exploring the wonders of Angular 2: Leveraging NgbModal for transclusion within

If I have a modal template structured like this: <div class="modal-header"> <h3 [innerHtml]="header"></h3> </div> <div class="modal-body"> <ng-content></ng-content> </div> <div class="modal-footer"& ...

Monitoring changes within the browser width with Angular 2 to automatically refresh the model

One of the challenges I faced in my Angular 2 application was implementing responsive design by adjusting styles based on browser window width. Below is a snippet of SCSS code showing how I achieved this: .content{ /*styles for narrow screens*/ @m ...

The variable is accessed before it is initialized in the context of Next.js and Server Actions

Currently, I am utilizing the new Data Fetching feature in Next JS to retrieve data from an API and store it in a variable named 'contact.' However, I am facing the issue of receiving an error message stating that "variable 'contact' is ...

Having difficulty initializing a new BehaviourSubject

I'm struggling to instantiate a BehaviourSubject I have a json that needs to be mapped to this Typescript class: export class GetDataAPI { 'some-data':string; constructor (public title:string, public description:string, ...

Creating a versatile JavaScript/TypeScript library

My passion lies in creating small, user-friendly TypeScript libraries that can be easily shared among my projects and with the open-source community at large. However, one major obstacle stands in my way: Time and time again, I run into issues where an NP ...

What is the best way for me to examine [...more] closely?

import * as Joi from 'joi'; import 'joi-extract-type'; const schema = { aaaaaaa: Joi.number() .integer() .positive() .allow(null), bbbbbb: Joi.number() .integer() .positive() .all ...

Angular2 - adding the authentication token to request headers

Within my Angular 2 application, I am faced with the task of authenticating every request by including a token in the header. A service has been set up to handle the creation of request headers and insertion of the token. The dilemma arises from the fact t ...

Passing the array as query parameters and retrieving it using the angular getAll function is the most efficient way

When using this function, I extract the ids of items and aim to send them as an array for retrieval with getAll(). const queryParams: Record<string, string[]> = selectedItems.reduce( (acc, curr, index) => ({ ...acc, [&apo ...

Guide on specifying a type for a default export in a Node.js module

export const exampleFunc: Function = (): boolean => true; In the code snippet above, exampleFunc is of type Function. If I wish to define a default export as shown below, how can I specify it as a Function? export default (): boolean => true; ...