Open denis-migdal opened 1 month ago
I'll discuss a little the behavior of the "frozen" keyword that could help to solve issues with readonly
(I can open a new issue if necessary).
Let assume frozen T = Freeze<T>
.
1) the frozen state needs to be contagious : Freeze<T> & U = Freeze<T&U>
.
It can be implemented :
[isFrozen]: never
property acting as a flag (ofc not ideal) : Freeze<T> = T & {[isFrozen]: never}
.2) we need to make some call signatures non callable. It can be implemented :
never
, but in reality the function still exists, and it will forbid all of its call signature.never
, which is close to runtime behavior as the function is callable but raises an exception, however, TS won't warn us when we call it.this
by never
: the error message isn't quite explicit, but it seems to work.3) We need to mark the call signature so that they will be callable in the general case, but non callable when the type is frozen. It can be implemented :
foo(this: NonConst<this>, ...)
with NonConst<T> = T extends Freeze<{}> ? never : T
. It works, but not efficient as it'd makes all class generics. Also, we mark the non-const when we should mark the const call signatures.PoC : Playground Link
Update: it seems TS is already able to assert generics types of base types:
interface A {
push(...args: number[]): number;
pop(): number|undefined;
}
type PickMatching<V, T> = { [K in keyof T]: (T&V)[K] };
type Z = A extends PickMatching<Array<infer T>, A> ? T[] : never; // number[]
interface A2 {
[key: number]: number;
push(...args: number[]): number;
pop(): number|undefined;
}
type Z2 = A2 extends PickMatching<Array<infer T>, A> ? T[] : never; // number[]
I updated my Array.isArray()
workaround to include it.
Then, for type guards, should it be done by TS itself, or by devs in the function signature ?
π Search Terms
in:title frozen in:title freeze
I searched for issues on
Array.isArray()
and found a lot of them, too much to list them all. 3 weeks ago I suggested something that could lead to a possible fix on an existing issue.β Viability Checklist
β Suggestion
The type guard for
Array.isArray()
is currently erroneous and the fix potentially quite complex. In retrospect I think the potential fix I suggested previously is more a "workaround" (still, you can get a look at it), and that there are issues on type guards.I'd like to discuss here what should be the behavior of
Array.isArray()
on different cases, and to discuss possibilities to simplify the current "workaround" by improving type guards behavior.Summary :
frozen
keywords puttingnever
to some properties (alternatively could also be afrozen
state e.g. to some functions, preventing their call/making them non-callable) to act as a constraint as opposed toreadonly
which is only a partial interface. This could help solving the current issue of usingreadonly
as a constraint, which generate lot of unsoundness + would solve use ofreadonly
arrays withArray.isArray()
.First, for the sake of simplicity, let's assume :
In the general case :
Of course, type guards need to do more than that, and are currently doing more, but not enough.
Union:
Currently, Type guards and
&
seem to behave as expected. Currently,(T1|T1)&U
is distributed as(T1&U) | (T2&U)
to remove somenever
then factorized when the type is printed.Child class :
Currently, Type guards behave as expected, but
&
doesn't (but not an issue).Base type
Currently, Type guards behave as expected, but
&
doesn't (but not an issue).Readonly:
There is 2 ways to see
readonly
:readonly T & T = readonly T
.readonly T & T = T
.For TS, it is saw as a partial interface, therefore :
readonly T & T = T
. Which is quite confusing as, in practice, we mainly use it as a constraint, but this is a design choice, why not.The issue is that the type of Μ
Object.freeze([])
is also areadonly []
, when this is not a partial interface, BUT a constraint. This is an inconsistency in the design, which could be solved with e.g. afrozen
keyword :frozen number[]
which would set some properties/methods asnever
instead of simply removing them :Currently,
Array.isArray(readonly T[])
, assertT
as beingany[]
, which is wrong for 2 reasons :readonly
information is lost.I argue that as
Array.isArray(Object.freeze([]))
returns true, so we shouldn't remove thereadonly
keyword. But should it be at the type guard level, or at theArray.isArray()
call signature level ?On one side
readonly
is only a partial interface, and on the other side, it is often used as a constraint (afrozen
keyword would solve this). On another side,readonly
is at the type level, when the type guard function is based on the value during execution.Therefore, without
frozen
there is 4 solutions :readonly
at theArray.isArray()
level, and require other devs to do so for their type guards.readonly
at the type guard level, withIs<readonly T, U> = readonly (T&U)
, which would be ambiguous as readonly isn't a constraint, but a partial interface.T extends readonly U
, makesU
implicitlyreadonly
.Is<T extends readonly U, U> = T & readonly U
otherwiseIs<T,U> = T&U
, which might also be confusing.Is<T, U extends T> = T
and requires an explicit cast to get aU
(which would always be legit), which would be horrible.And here the good stuff, with generics...
Base type + generics
We should try to assert the generic types :
I think a type deduction is technically possible in lot of cases, an would simplify lot of type guards using generics. The issue is to assert when the following step would be legal in a type guard :
Maybe if, and only if,
U<unknown&number> extends U<unknown>
?We could even be more generic :
When we can't deduce, I suggest:
Is<unknown, U<V>> = U<V>
Is<any, U<V>> = U<V>
, butU<any>
ifV = unknown
.Is<{}, U<V>> = U<V>
.This issue also occurs with
readonly unknown[]
, as it can be seen as a base type ofunknown[]
.π Motivating Example
This would lead to more precise type deduction in type guards.
π» Use Cases
Deduce type more precise types.
Deduced types are incorrect/not precised.
Complex type guards.