Change default inference from arrays to tuples and primitives to literals
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.
Issue #38727 is related
Also related: #16896
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)
@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.
See #32758
@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
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
@RyanCavanaugh please reopen this issue. It is not duplicate.
@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?
@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: https://github.com/microsoft/TypeScript/issues/38831#issuecomment-643007664
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...?
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?
@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 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
@jgbpercy the expression
as constis 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?
@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)
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`
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 https://github.com/microsoft/TypeScript/issues/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 https://github.com/microsoft/TypeScript/issues/38831#issuecomment-643007664)
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