Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4.2 breaks string literal conditional type checking. #42644

Closed
akutruff opened this issue Feb 4, 2021 · 5 comments
Closed

4.2 breaks string literal conditional type checking. #42644

akutruff opened this issue Feb 4, 2021 · 5 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@akutruff
Copy link

akutruff commented Feb 4, 2021

Bug Report

In TypeScript 4.1, a type can be constrained conditionally to be a string literal. In 4.2 conditionals can not enforce string literals

type StringLiteral<T> = T extends `${string & T}` ? T : never;

🔎 Search Terms

string literal, conditional types, breaking change

🕗 Version & Regression Information

  • This is a breaking change
  • This changed between versions 4.1 and 4.2
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about string literals
  • I was unable to test this on prior versions because string template literals were introduced in 4.1

⏯ Playground Link

Playground 4.1
Playground 4.2

💻 Code

// We want to only allow string literals
type StringLiteral<T> = T extends `${string & T}` ? T : never;

const literalToConstrainedLiteral : StringLiteral<'bob'> = 'bob'; // Should be allowed as bob is a string literal

// 4.1 and 4.2: Produces a GOOD compiler error as bob is constrained to be a literal. StringLiteral<'bob'> resolves to 'bob'
const stringToConstrainedLiteral : StringLiteral<'bob'> = ('bob' as string); 

// 4.1: GOOD: StringLiteral<string> resolves to never.  
// 4.2: BAD: StringLiteral<string> resolves to string.  No compiler error produced.
const stringToString : StringLiteral<string> = ('bob' as string); 

// use case: Ensure that someone is only registering via a string literal.  Useful when using callback maps based on discriminated unions.
function registerAction<T>(action : StringLiteral<T>) {}

// This is good since 'clicked' is a literal
registerAction('clicked');

//4.1: GOOD: Produces a compiler error
//4.2: BAD: Compiler allows this
registerAction('clicked' as string);  

🙁 Actual behavior

TypeScript 4.2 does not produce compiler errors when the type is too wide.

🙂 Expected behavior

Produces compiler errors when the type is too wide.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Feb 4, 2021

Seems like we didn't used to simplify `${string}` to string, which was probably a bug. I understand the desire for an "only assignable to string literals" type, but I think the new behavior makes sense. I just can't look at the code that's provided and understand why `${string & T}` should ever reject string in the cases where T is instantiated with string.

@akutruff
Copy link
Author

akutruff commented Feb 4, 2021

Hmm, this was my train of thought - Since string literal templates should only allow literals as input then it makes sense that only a literal passed into a template can actually resolve to a literal type.

type AB<T extends string> = T extends `${string & T}AB` ? T : never;  
type ABTest = AB<'foo'>; // makes sense that this is never.  The condition is: 'foo' extends 'fooAB'

type A<T extends string> = T extends `${string & T}A` ? T : never;  
type ATest = A<'foo'>; // makes sense that this is never.  The condition is: 'foo' extends 'fooA'

type Same<T> = T extends `${string & T}` ? T : never;  
type SameTest = Same<'foo'>; // Should be 'foo' because -  The condition is: 'foo' extends 'foo'

playground

Next is for the sake of consistency and maintaining the spirt of literals being a narrowing mechanism.

//4.1: resolves in intellisense to `${string}`
//4.2: resolves in intellisense to string
type Foo = `${string & string}`; 

//4.1: resolves in intellisense to `${number}`
//4.2: resolves in intellisense to `${number}`
type Num = `${number & number}`; 

playground

I am using a string literal template, and I would hope that a string literal template would ALWAYS resolve to a string literal, and not promote the contents of the ${} outside the literal itself. If it doesn't make sense for ${number} to widen, then neither should ${string}.

type NotReallyALiteral = `${string}`; 
const someValue : NotReallyALiteral = ('something' as string);

The above just doesn't seem safe to allow.

Side note: this breaks all of my type declaration code if this is not a regression.

@RyanCavanaugh
Copy link
Member

@ahejlsberg thoughts?

@ahejlsberg
Copy link
Member

ahejlsberg commented Feb 5, 2021

Daniel is correct, we used to not reduce `${string}` to string, but we now do--and it's the right thing to do. There should be no observable difference in behavior between those two types.

I'd suggest writing the StringLiteral<T> type as this instead:

type StringLiteral<T> = T extends string ? string extends T ? never : T : never;

That works in both 4.1 and 4.2 and really makes more sense--it expresses anything that extends string but isn't string itself.

@akutruff
Copy link
Author

akutruff commented Feb 5, 2021

Thanks, and that's a super clever alternative. Thank you all for your time on this!

@akutruff akutruff closed this as completed Feb 5, 2021
@DanielRosenwasser DanielRosenwasser added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

4 participants