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

Change default inference from arrays to tuples and primitives to literals #38831

Open
awerlogus opened this issue May 28, 2020 · 18 comments
Open
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@awerlogus
Copy link

I am working on functional project now and I'm getting a lot of errors related to too wide type inference for values. It looks like

function test(a: { data: 1 | 2 }) { return a }
const a = { data: 2 }
// Types of property 'data' are incompatible.
// Type 'number' is not assignable to type '2'.ts(2345)
const b = test(a)

There're really many errors like this, and the main problem is that you can convert type A to type B, but not vice versa.

type A = { data: 2 }
type B = { data: number }

So, I ask you to add a new compiler option (to not break existing code) that will make type checker infer types as narrow as possible. It will be very helpful for functional programming.

@awerlogus
Copy link
Author

Issue #38727 is related

@Nathan-Fenner
Copy link
Contributor

Also related: #16896

@jgbpercy
Copy link

It looks like you want as const:

function test(a: { data: 1 | 2 }) { return a }
const a = { data: 2 } as const;
const b = test(a)

@awerlogus
Copy link
Author

@jgbpercy

It looks like you want as const:

Yes, as const works, but I'm asking for type checker mode where all types will be inferred as const by default.

I use TypeScript based type check for jsdoc type declarations in .js files and I can't use <const> or as const there.

@RyanCavanaugh
Copy link
Member

See #32758

@awerlogus
Copy link
Author

awerlogus commented Jun 12, 2020

@RyanCavanaugh no, #32758 is not my proposal. They want, for example, use ReadonlyArray<number> instead of Array<number> for this case

const a = [1, 2, 3]

or use readonly modifier for object properties by default.

I'm asking for a type checker mode where types will be inferred as 'literal' (use literals instead of basic types) as possible.

// Got: Array<number>
// Proposal: [1, 2]
const a = [1, 2]
declare function b<T extends Array<unknown>>(...args: T): T

// Got: [number, number, number]
// Proposal: [1, 2, 3]
const c = b(1, 2, 3)
// Got: { data: number }
// Proposal: { data: 2 }
const b = { data: 2 }

etc

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@awerlogus
Copy link
Author

@RyanCavanaugh please reopen this issue. It is not duplicate.

@RyanCavanaugh
Copy link
Member

@awerlogus I don't see any measurable difference between these issues. Certainly we would implement that flag as applying equally to both arrays and objects - are you saying we would need two different flags, one for arrays, one for objects?

@awerlogus
Copy link
Author

@RyanCavanaugh as I said 2 times before, I don't ask you to add a compiler mode, where all types will be readonly. My proposal is a mode to make type checker prefer inferring entities as literals over basic types when it's possible. Using number literals over number type, string literals over string type, boolean literals over boolean type, tuples over arrays, etc.

Examples are available here: #38831 (comment)

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript and removed Duplicate An existing issue was already created labels Jun 29, 2020
@RyanCavanaugh RyanCavanaugh changed the title Narrow type check mode Change default inference from arrays to tuples and primitives to literals Jun 29, 2020
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jun 29, 2020

I guess I'd like to understand what you think isn't effectively read-only about the type [1, 2]. You can write to it, but only in a way that doesn't change its value...?

@jgbpercy
Copy link

I think, and correct me if I'm wrong @awerlogus , that you essentially want a mode where every expression has as const implicitly appended to it, in the absence of any explicit wider type?

@awerlogus
Copy link
Author

@jgbpercy the expression as const is being used to solve two different problems in the same time: the first is inferring types as literal as possible (that thing which I'm asking for in this issue), and the second is marking arrays or object properties as readonly (this is not my proposal). We need a new type checker mode to solve the first problem only. Other than that is completely as you said.

@RyanCavanaugh RyanCavanaugh reopened this Jun 29, 2020
@awerlogus
Copy link
Author

awerlogus commented Jun 29, 2020

@RyanCavanaugh I didn't say that we should or should not mark tuples or objects as readonly. I meant that it doesn't matter in the context of this issue. You may add that readonly mode in parallel. But if we will analyse the problem of mutable/readonly values, we may find that the type [1, 2] is not safe enough. The type of variable a may be easily broken by the code like

const a: [1, 2] = [1, 2]

// Legal
a.push(1)

// [1, 2]
const b = a

Readonly tuple will not allow it

const c: readonly [1, 2] = [1, 2]

// Error
c.push(1)

So, the question becomes: should we have any access to mutating array methods in case of mutable tuples?

Except for this, tuples and readonly tuples will work the same in context of literal elements. But it is not possible to convert readonly tuples to mutable ones, so we should prefer inferring all tuples as mutable by default then. If someone needs exactly readonly tuple, he can perform a safe type cast, it is not a problem. Or if he needs mutate values of the tuple, he can safely cast [1, 2] to [number, number].

Almost the same problems with objects:
There're functions like Object.defineProperty that can bypass the type system even if properties are marked as readonly.

const a: { readonly data: 2 } = { data: 2 }

// Should not be valid
Object.defineProperty(a, 'data', { value: 3 })

const b: { data: 2 } = { data: 2 }

// Should not be valid too
Object.defineProperty(b, 'data', { value: 3 })

It should be fixed, I think.

If we will need some mutable object, it will be not possible to get it from readonly one without any hacks, so we should prefer inferring objects by default as mutable too.


I think, good programming language should help developers write the better code. And in the good code all data types and its transformations are managed by functions. There're some types that expected to be passed into and returned from functions. Types of variables and constants should be inferred as narrow as possible to may be correctly passed to as many functions as possible. Let me show an example:

declare function a<T extends { readonly data: number }>(arg: T): T['data']
declare function b<T extends { readonly data: 1 | 2 }>(arg: T): T['data']

// Now: { data: number }
// Proposed: { data: 2 }
const c = { data: 2 }

Now variable c may be passed to the function a only (without of hacking type system). When the new type checker mode will be added, it will be available to pass c to both functions — a and b.

p.s. There's no as const in js

@lautarodragan
Copy link

@jgbpercy the expression as const is being used to solve two different problems in the same time: the first is inferring types as literal as possible (that thing which I'm asking for in this issue), and the second is marking arrays or object properties as readonly (this is not my proposal). We need a new type checker mode to solve the first problem only. Other than that is completely as you said.

@awerlogus would you consider editting the description of the issue, adding this info? I think it answers all questions about your proposal.

I also agree it may be nice for them to be two separate flags.

Also may consider renaming suggestion to "implicit as-const for type narrowing flag" or "--strict-type-narrowing flag"?

Question: are there cases in which this flag could be harmful?

Also: now that records and tuples are stage 2, could this (or even both) issues be (partially) addressed by that proposal instead, with no (or little) change to TS?

@awerlogus
Copy link
Author

@lautarodragan

@awerlogus would you consider editting the description of the issue, adding this info? I think it answers all questions about your proposal.

All this conversation is the issue description. So info described in that comment already exists here. I always read comments under issues I'm interested. If someone doesn't do that is means they're not interested enough.


are there cases in which this flag could be harmful?

If you prefer working with mutability it can make you cast types to more general ones too often. For example cast [1, 2] to [number, number] or even Array<number>. But the good part is that it doesn't violate the rules of the type system. Currently we have to use @ts-ignore or as any as ... to make existing types more literal because it is not correct to cast [number, number] to [1, 2].

Also even currently when we modelling function overloading without overloading (because it's designed bad) we may have some problems with types inferred too literally. For example let's try to rewrite this function

function add(a: number, b: number): number
function add(a: bigint, b: bigint): bigint
function add(a: any, b: any) { return a + b }

It will look like

function add<P extends [a: number, b: number] | [a: bigint, b: bigint]>(...data: P): P[0] {
  return data[0] + data[1]
}

If we call it, we will get return type equal to the first argument because P will be inferred as [1, 2]

// Got: 1
const a = add(1, 2)

So it makes us generalize return type of the function and use P[0] extends bigint ? bigint : number instead of just P[0].

I think such cases of generalization necessity will occur more often when using the flag.

This example also produces an error:

function add<P extends [a: number, b: number] | [a: bigint, b: bigint]>(...data: P): P[0] extends bigint ? bigint : number {
  // Operator '+' cannot be applied to types 'number | bigint' and 'number | bigint'.ts(2365) 
  return data[0] + data[1]
}

@RyanCavanaugh do we have an issue describing the error above?


now that records and tuples are stage 2, could this (or even both) issues be (partially) addressed by that proposal instead, with no (or little) change to TS?

It's a good question. How about mutable objects containing enums? This example will still not work after adding records and tuples

function test(a: { data: 1 | 2 }) { return a }
const a = { data: 2 }
// Types of property 'data' are incompatible.
// Type 'number' is not assignable to type '2'.ts(2345)
const b = test(a)

@Nokel81
Copy link

Nokel81 commented Jan 28, 2021

I have to say that I would definitely like something like this to exist. My use case involves my most common use of typing functions, namely type Array.prototype.map to be a tuple instead of an array of either type.

Example:

function getOtherData(from:T): R { ... }

const sourceArray: T[] = ...;
...
sourceArray
    .map(elem => [elem, getOtherData(elem)]
    .map([elem, other] => { ... }); // here both `elem` and `other` have type `T | R`

@PabloLION
Copy link

So, I ask you to add a new compiler option (to not break existing code) that will make type checker infer types as narrow as possible. It will be very helpful for functional programming.

"make type checker infer types as narrow as possible" seems like the idea of immutability. And is discussed at least in one other issue #32758 .

I'm asking for a type checker mode where types will be inferred as 'literal' (use literals instead of basic types) as possible.
(from #38831 (comment))

I cannot see the difference between the 'literal' proposed by @awerlogus , and the literal type from 'readonly'. Could you explain more?

I still think this issue can be closed without further information (note that it has been 2 years without reply). @RyanCavanaugh @typescript-bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants