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

"typeof" operator should prevent widening #22211

Closed
tvald opened this issue Feb 27, 2018 · 4 comments
Closed

"typeof" operator should prevent widening #22211

tvald opened this issue Feb 27, 2018 · 4 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@tvald
Copy link

tvald commented Feb 27, 2018

When someone uses typeof, it is unlikely that they want a widening type. This unexpectedly breaks type safety in various situations.

This was considered previously for all of 16 minutes in #16337. I think it warrants reconsideration.

TypeScript Version: 2.8.0-dev.20180208

Search Terms:
typeof widen widened widening

Code

type StaticA = 'a'
const staticA: StaticA = 'a'

const runtimeA = 'a'
type RuntypeA = typeof runtimeA
const runtypeA: RuntypeA = 'a'

const x = {
  staticA,
  runtypeA,
}

x.staticA = runtypeA // ok
x.staticA = x.runtypeA // fails, should be ok
x.runtypeA = 'asdjfha' // ok, should fail

Expected behavior:

test.ts(15, 3): error TS2322: Type '"asdjfha"' is not assignable to type '"a"'.

Actual behavior:

test.ts(14,1): error TS2322: Type 'string' is not assignable to type '"a"'.

Related Issues:
#16337

@tvald
Copy link
Author

tvald commented Feb 27, 2018

A real-world example that would benefit from this change is pelotom/runtypes, which breaks in situations which reduce to the example above. There are certainly other corner cases, such as the example in #16337, which will also unexpectedly break type safety.

@RyanCavanaugh
Copy link
Member

I should have left myself more notes in the linked issue, but IIRC at least one problem is that this would be a substantial breaking change.

We have to pick between the proposed change and the existing symmetry of these declarations being identical:

let x1 = runtimeA;
let x2: typeof runtimeA = runtimeA

Without a strong principled reason to choose one over the other, stare decisis is sort of the order of the day. If you had to convince someone we were correctly breaking their working code that relied on the current behavior, what would your argument be?

@mhegazy mhegazy added the Working as Intended The behavior described is the intended behavior; this is not a bug label Feb 27, 2018
@tvald
Copy link
Author

tvald commented Feb 28, 2018

Without a strong principled reason to choose one over the other, stare decisis is sort of the order of the day. If you had to convince someone we were correctly breaking their working code that relied on the current behavior, what would your argument be?

My strong principled reason would be that implicit type widening is inherently unsafe, since it silently and unexpectedly relaxes an invariant. It falls into the category of conveniences like ASI and aggressive type coercion that are actually loaded guns pointed at your foot. (See also: #20195)

But I would rather advance a more narrow argument here. :)

When an annotated type is widened, the compiler is stripping and violating a constraint explicitly specified by the author.

In your example above, it is counter-intuitive that a variable with an explicit type should behave the same as a variable with an implicit type. Rather, one expects that every variable with an explicit type will behave the same as every other variable with an explicit type.


To illustrate with a more complete example than above, which of these things is not like the others?

let x1 = runtimeA
let x2: typeof runtimeA = runtimeA
let x3: 'a' = runtimeA

The asymmetry between an implicit type and an explicit (non-typeof) type is intuitive:

let x1 = runtimeA
let x3: 'a' = runtimeA

The symmetry between an explicit (non-typeof) type and an explicit (typeof) type should be intuitive:

let x2: typeof runtimeA = runtimeA
let x3: 'a' = runtimeA

However, we're currently stuck with a counter-intuitive symmetry between an implicit type and an explicit (typeof) type:

let x1 = runtimeA
let x2: typeof runtimeA = runtimeA

Unfortunately, practical usage was difficult to research. A quick perusal of GitHub did reveal the following common but subtly-unsafe pattern:

const SomeTag = 'SomeTag'
type SomeTag = typeof SomeTag

Due to implicit type widening, this will in certain hard-to-predict situations be widened to string. The only safe pattern counter-intuitively requires explicit repetition:

const SomeTag = 'SomeTag'
type SomeTag = 'SomeTag'

@typescript-bot
Copy link
Collaborator

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
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