question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if youā€™re still stuck at the end, weā€™re happy to hop on a call to see how we can help out.

Intrinsic types remove widening on literal types

See original GitHub issue

Bug Report

šŸ”Ž Search Terms

intrinsic, uppercase, lowercase, widening

šŸ•— Version & Regression Information

Occurs in Nightly (5.0.0-dev.20221118) and every version I tried going back to 4.1.5

āÆ Playground Link

Playground link with relevant code

šŸ’» Code

declare const x: boolean;
declare function capitalize<T extends string>(it: T): Capitalize<T>;

// example 1
let greeting1 = "hello";
if(x) {
  greeting1 = "goodbye"; // allowed
}

// example 2
let greeting2 = capitalize("hello");
if(x) {
  greeting2 = capitalize("goodbye"); // type error
}

This example is adapted from @RyanCavanaughā€™s example here.

šŸ™ Actual behavior

Reassigning a new string to greeting1 succeeds, but reassigning one to greeting2 fails.

šŸ™‚ Expected behavior

The widening behavior for standard literal types (like "hello") should not be different from the behavior of the type produced by Capitalize<"hello">; if the widening behavior were the same, greeting2 would also have its type inferred as string, and would accept a new string being assigned to it.

Having inconsistent widening like this seems unintuitive and possibly unintentional. Concretely, it also makes it impossible to use an intrinsically-typed value in a let statement (as shown), which creates a similar problem in @RyanCavanaughā€™s original example in #44268.

Issue Analytics

  • State:open
  • Created 10 months ago
  • Comments:14 (9 by maintainers)

github_iconTop GitHub Comments

1reaction
Andaristcommented, Nov 19, 2022

The widening that happens here is completely different though. The wrapping capitalize call changes everything here. You always~ get an inferred type from the given expression based on the surrounding context.

When assigning a string to a let variable the type of this variable becomes string when you pass it through a function that can infer a different type based on its arguments then u will get the type returned by that function. It matches your expectations here - otherwise, you wouldnā€™t use a Capitalize<T> there as Capitalize<string> is roughly equivalent to just string. So clearly you have wanted to get a literal type there.

From that moment the type of that variable is fixed - it wonā€™t widen on the following assignments. Itā€™s basically the same case as this one:

declare const x: boolean;

let greeting1 = "hello";

if (x) {
  greeting1 = 42; // Type 'number' is not assignable to type 'string'.(2322)
}

This one is actually ā€œfixableā€ by using ā€œautoā€ types like here (by not annotating the greeting1ā€™s type):

declare const x: boolean;

let greeting1
greeting1 = "hello";

if (x) {
  greeting1 = 42; 
}

declare function acceptStr(a: string): void
declare const x: boolean;

let greeting1
greeting1 = "hello";

if (x) {
  acceptStr(greeting1) // ok, the auto type is `string`
  greeting1 = 42; // ok, `number` gets pushed locally to the automatically created union~
  acceptStr(greeting1) // not ok, the auto type is `string | number`: Argument of type 'number' is not assignable to parameter of type 'string'.(2345)
}

So, in a way, you could leverage the auto types in your example: TS playground. This would look quite weird though, so I would advise you to just provide an explicit generic argument like here.

Perhaps what youā€™d like to request here is to annotate a variable as ā€œautoā€ - so you wouldnā€™t have to add a generic type param and you wouldnā€™t have to split the initial variable declaration and the initial value assignment

0reactions
ethanresnickcommented, Dec 2, 2022

Hey @RyanCavanaugh, sorry to hear you were sick. Welcome back, and I hope you were able to have a nice thanksgiving.

There arenā€™t long-standing years-missed bugs around widening/nonwidening literal types ā€“ edge cases, yes? But itā€™s not fundamentally broken in some obviously-fixable way; the behavior in https://github.com/microsoft/TypeScript/pull/24310 has been present for four years now, without substantial complaint as far as Iā€™m aware.

I agree: thereā€™s clearly not a huge amount of pain caused by the current system. My hope would still be that at least some of these edge cases are avoidable, without complicating the underlying rules, but I confess that I canā€™t think of concrete proposal right now that would definitely improve things enough to justify the breakage.

I think @fatcerberus is right that undoing #24310 would address the OP and wouldnā€™t produce problems on your ā€œGordian Knotā€ example ā€” but it would reintroduce the issue that #24310 was used to solve, and thatā€™s where Iā€™m stumped.

In the example from #23649:

export function keys<K extends string, V>(obj: Record<K, V>): K[] {
  return Object.keys(obj) as K[]
}

declare const langCodeSet: Record<"fr" | "en" | "es" | "it" | "nl", true | undefined>;
const langCodes = keys(langCodeSet);

langCodes is supposed to be an array of string literals, rather than string[].

Had #24310 never been introduced, I think I wouldā€™ve tried to accomplish that in some other way, rather than cueing off of the extends string on the generic. E.g., maybe #23649 couldā€™ve been solved with a rule saying that the literal type inferred for K shouldnā€™t be widening because K is used in an object key position (in the Record mapped type). Iā€™m not saying that wouldā€™ve been an ideal (or even workable) rule, but just that maybe there were potential alternatives before #24310 was added.

However, given that #24310 has been introduced, I bet a bunch of people now are relying on getting literal types in all sorts of places where they were previously being widened, so rolling back #24310 now would probably be too disruptive.

I looked in my own code and found a function like this:

function gqlSuccessResult<T extends object, U extends string>(result: T, name: U) {
  return {  
    __typename: name, 
    ...result 
  };
}

In that example, Iā€™m relying on #24310 to give me a literal type for the inferred type of the __typename key, and I canā€™t think of an alternate rule (that doesnā€™t require any code changes) that would do that.

Had #24310 not been introduced, I mightā€™ve proposed addressing that use case like this:

function gqlSuccessResult<T extends object, U extends string>(result: T, name: U) {
  return {  __typename: name as const, ...result };
}

In the above snippet, Iā€™m extending the set of places where as const can legally appear; instead of being allowed only on literals, Iā€™m saying itā€™d be allowed on name because nameā€™s type is constrained such that it might have a literal type, and the as const there would mean ā€œdonā€™t make the literal type widenā€.

But, again, now that people are relying on #24310, I doubt the breakage would be justified.

So, idk, maybe Iā€™ll keep noodling on this and, for now, Iā€™ll just open a separate issue for the one concrete bug that I think this thread identified, which is that foo as typeof foo should always be a no-op (imo).

Read more comments on GitHub >

github_iconTop Results From Across the Web

Widening and Narrowing in Typescript | manual - GitHub Pages
Literal widening: treat a literal type as a primitive one. Narrowing: remove constituents from a union type. Instanceof narrowing: treat a type as...
Read more >
TypeScript-New-Handbook/Widening-and-Narrowing.md at ...
Literal widening : treat a literal type as a primitive one. Narrowing: remove constituents from a union type. Instanceof narrowing: treat a type...
Read more >
Type Widening and Narrowing in TypeScript
Literal widening in TypeScript is when a literal type gets treated as its base type. When you declare a variable using the const...
Read more >
Documentation - TypeScript 4.1
Template Literal Types. String literal types in TypeScript allow us to model functions and APIs that expect a set of specific strings.
Read more >
Literal Type Widening in TypeScript - Marius Schulz
#Usefulness of Non-Widening Literal Types ... TypeScript infers the type string[] for the array. Therefore, array elements like first and secondĀ ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found