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

https://i.sstatic.net/XssIM.png

However, foobar.bar is being typed as string:

https://i.sstatic.net/Ssi5q.png

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

"Error encountered: Unable to resolve dependency tree" message appears when attempting to run npm install

Encountering dependency errors while trying to execute the npm install command for my Angular application. As a newcomer to TypeScript and Angular, I'm unsure of the next steps to take. Any suggestions? Attempted solutions include clearing the npm ca ...

Is it possible for a class method in Typescript to act as a decorator for another method within the same

Can we implement a solution like this? class A { private mySecretNumber = 2; decorate (f: (x :number) => number) { return (x: number) => f(this.mySecretNumber * x); } @(this.decorate) method (x: number) { return x + 1; } } I h ...

Angular 6 does not automatically include the X-XSRF-TOKEN header in its HTTP requests

Despite thoroughly reading the documentation and searching for answers on various platforms, I am still facing issues with Angular's XSRF mechanism. I cannot seem to get the X-XSRF-TOKEN header automatically appended when making a POST request. My si ...

In Typescript, ambient warnings require all keys in a type union to be included when defining method parameter types

Check out this StackBlitz Example Issue: How can I have Foo without Bar, or both, but still give an error for anything else? The TypeScript warning is causing confusion... https://i.stack.imgur.com/klMdW.png index.ts https://i.stack.imgur.com/VqpHU.p ...

Determine in React whether a JSX Element is a descendant of a specific class

I am currently working with TypeScript and need to determine if a JSX.Element instance is a subclass of another React component. For instance, if I have a Vehicle component and a Car component that extends it, then when given a JSX.Element generated from ...

Using Angular's dependency injection in a project that has been transpiled with Babel

I am currently attempting to transpile my Angular 6 project, which is written in TypeScript, using the new Babel 7. However, I am facing challenges with getting dependency injection to function properly. Every time I try to launch the project in Chrome, I ...

Leveraging scanner-js within an Angular2 environment

Exploring ways to incorporate Scanner-JS into my Angular2 project, a tool I discovered while tinkering with the framework. Being a novice in Angular2, this question might be elementary for some. I successfully installed scanner-js via npm npm install sc ...

What is the best way to implement custom serialization for Date types in JSON.stringify()?

class MyClass { myString: string; myDate: Date; } function foo() { const myClassArray: MyClass[] = .... return JSON.stringify(myClassArray); // or expressApp.status(200).json(myClassArray); } foo will generate a JSON string with the date format o ...

Converting a string into a Date in Typescript while disregarding the timezone

Upon receiving a date in string format like this (e.g.): "11/10/2015 10:00:00" It's important to note that this is in UTC time. However, when creating a Date object from this string, it defaults to local time: let time = "11/10/2015 10:00:00"; let ...

Discovering the number of items that have been filtered in isotope-layout using React and Typescript

Currently, I am utilizing the isotope-layout library within a React (Typescript) project. Although I have successfully implemented filtering on my page, I am unsure of how to retrieve the count of the filtered items. Upon loading the page, Isotope is init ...

Ways to dynamically generate a generic that expands a union class type

Reviewing the code snippet: const events = { a: ['event1' as const, 'event2' as const], b: ['event3' as const, 'event4' as const], }; class SomeClass< T extends AnotherClass<typeof events[keyof typeof ev ...

Tips for customizing the legend color in Angular-chart.js

In the angular-chart.js documentation, there is a pie/polar chart example with a colored legend in the very last section. While this seems like the solution I need, I encountered an issue: My frontend code mirrors the code from the documentation: <can ...

Is there a way to use Regex to strip the Authorization header from the logging output

After a recent discovery, I have come to realize that we are inadvertently logging the Authorization headers in our production log drain. Here is an example of what the output looks like: {"response":{"status":"rejected",&quo ...

What is the best way to update the color of a label in a Mantine component?

When using the Mantine library, defining a checkbox is done like this: <Checkbox value="react" label="React"/> While it's possible to change the color of the checkbox itself, figuring out how to change the color of the label ...

Testing Jasmine with objects that contain optional properties

In the IData interface, there are optional properties available. interface IData { prop1: string, prop2?: string } setObj(){ prop1 = 'abc'; prop2 = 'xyz'; let obj1 : IData = { prop1: this.prop1, ...

Unit testing Angular components can often uncover errors that would otherwise go unnoticed in production. One common

I need assistance writing a simple test in Angular using Shallow render. In my HTML template, I am utilizing the Async pipe to display an observable. However, I keep encountering the following error: Error: InvalidPipeArgument: '() => this.referenc ...

The functionality of the disabled button is malfunctioning in the Angular 6 framework

Just starting out with angular 6 and I'm using formControl instead of FormGroup for business reasons. app.component.html <button class="col-sm-12" [disabled]="comittee_Member_Name.invalid && comittee_Member_Number.invalid && c ...

The proper method for referencing TypeScript compiled files with the outDir option

I am currently working on a simple app that consists of two .ts files. These files are compiled using the following tsconfig.js file: { "compilerOptions": { "target": "ES5", "module": "commonjs", "sourceMap": true, "emitDecoratorMetadata ...

Chai is unable to differentiate between different class types

When using Chai to compare if a returned value of type SimpleModel matches with the expected type SimpleModel, I encountered an error even though my IDE indicated that the types are correct: AssertionError: expected {} to be a simplemodel The setup for t ...

Using TypeScript in React, how can I implement automation to increment a number column in a datatable?

My goal is to achieve a simple task: displaying the row numbers on a column of a Primereact DataTable component. The issue is that the only apparent way to do this involves adding a data field with indexes, which can get disorganized when sorting is appli ...