microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.77k stars 12.46k forks source link

never extends `${infer P}` yield a different result than never extends `${infer P}/` #50215

Open tylim88 opened 2 years ago

tylim88 commented 2 years ago

Bug Report

πŸ”Ž Search Terms

never extends ${infer P} yield different result than never extends ${infer P}/

πŸ•— Version & Regression Information

TS 4.7

type is not what expected

⏯ Playground Link

playground

πŸ’» Code

type a = never extends `${infer P}` ? P : 1  // never
//   ^?
type b = never extends `${infer P}/` ? P : 2 // string
//   ^?

type a1 = never extends `${infer P extends string}` ? P : 1 // never
//   ^?
type b1 = never extends `${infer P extends string}/` ? P : 2 // string
//   ^?

type a2 = never extends `${infer P extends number}` ? P : 1 // never
//   ^?
type b2 = never extends `${infer P extends number}/` ? P : 2 // number
//   ^?

type a3 = never extends `${infer P extends null}` ? P : 1 // never
//   ^?
type b3 = never extends `${infer P extends null}/` ? P : 2 // null
//   ^?

type a4 = never extends `${infer P extends undefined}` ? P : 1 // never
//   ^?
type b4 = never extends `${infer P extends undefined}/` ? P : 2 // undefined
//   ^?

type a5 = never extends `${infer P extends boolean}` ? P : 1 // never
//   ^?
type b5 = never extends `${infer P extends boolean}/` ? P : 2 // boolean
//   ^?

type a6 = never extends `${infer P extends bigint}` ? P : 1 // never
//   ^?
type b6 = never extends `${infer P extends bigint}/` ? P : 2 // bigint
//   ^?

πŸ™ Actual behavior

  1. never extends ${infer P}/ yield the same result as never extends ${infer P}
  2. never extends ${infer P extends X}/ yield X but never extends ${infer P extends X} always yield never

πŸ™‚ Expected behavior

  1. never extends ${infer P}/ should yield never like never extends ${infer P}
  2. never extends ${infer P extends X}/ yield never like never extends ${infer P extends X}

are these intended behaviors? kind of unintuitive

RyanCavanaugh commented 2 years ago

This would seem to follow from the PR text

https://github.com/microsoft/TypeScript/pull/40336

Type inference supports inferring from a string literal type to a template literal type. For inference to succeed the starting and ending literal character spans (if any) of the target must exactly match the starting and ending spans of the source. Inference proceeds by matching each placeholder to a substring in the source from left to right: A placeholder followed by a literal character span is matched by inferring zero or more characters from the source until the first occurrence of that literal character span in the source. A placeholder immediately followed by another placeholder is matched by inferring a single character from the source.

I don't really see a defect here; zero-length strings are weird to reason about and you could make an argument for changing either of these behaviors. Without a motivating use case to drive which one, changing it for the sake of changing it is just risk without benefit.

jcalz commented 2 years ago

What zero-length strings are we looking at here? Are you saying that we could interpret `${infer P}` as having a zero-length string between `${infer P} and `?

Would it be fair to say that one should have no particular expectations about what will come out of a pathological case like never extends F<infer T> ? T : never in general?

tylim88 commented 2 years ago

update: written some silly stuff, dont bother

RyanCavanaugh commented 2 years ago

it is simply counter intuitive, because with never, we always expecting negative case

What? never is defined to be the subtype of all types.

type M = never extends string ? true : false; // true
tylim88 commented 2 years ago

@RyanCavanaugh lmao, sorry, had a brain fart

RyanCavanaugh commented 2 years ago

Thinking about it more, I think the correct behavior is actually fairly straightforward when comparing this to object types (demonstrated below). Probably what's going wrong here is that we have an assumption somewhere that for any T legal to be there in the first place, ${T} === T. That's true for string subtypes, but isn't true for never.

// A = true
type A = never extends { x: string } ? true : false;
// B = unknown
type B = never extends { x: infer T } ? T : "no";
// C = unknown
type C = never extends [infer T, unknown] ? T : "no";
// D = unknown
type D = never extends [unknown, infer T, unknown] ? T : "no";
// E = string
type E = never extends `${infer X}#` ? X : "no";
// F = never, ?. Should be 'string'
type F = never extends `${infer X}` ? X : "no";
tylim88 commented 2 years ago

@RyanCavanaugh

sorry for that silly post, it is morning and my brain still being dumb

ok long thing short

I think it is normal to expect the P from never extends ${infer P}/? P : 1 and never extends ${infer P extends X}/? P : 1 is never

update: judging from the last post, seem like it is going to be P from never extends ${infer P}? P : 1 is string and never extends ${infer P extends X}? P : 1 is X instead