-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Add a mutating
return type operator to indicate whether a function modifies a property indirectly
#29346
Comments
I've, personally, felt the need for tracing of certain effects (mutations included) before. The other effect we often see needed is weather an argument is immediately invoked or not. Effects are interesting because they'd force us to track refinements about localized symbol information alongside the types - and that information is only useful when those symbols are in scope (eg, if a function type that mutates an internal variable flows into an external scope, nothing can witness the mutation, so the refinement is no longer needed). Correctly tracking this information would require encoding a lot of symbolic information into types and do correspondingly more analysis during flow control analysis. |
Yeah, this is much more specific than that, a fairly small (but useful) subset of effects. It doesn't really reify it into a type, just warns callers. Conceptually, it's closer to Swift's The explicit dependency here you have to list is so, in theory, you don't need to
Yeah, definitely. But the flow control analysis for my proposal I don't believe should have to change significantly. It's just a few main things:
I wouldn't be against full effect types and such, if you all are willing to commit to it, though... 😉 |
declarations like this are cool and all, how are you going to use them (enforce them, constrain your code because of them)? note aside, i have a hard feeling on the proposed syntax when you say: lastly and sadly, what are the chances if we still don't have essentials like HKT's, etc |
That's a linter problem, not a language problem. The key here is that
I used the generic pseudo-type to illustrate a possible alternative solution, and then showed where the pitfalls were in it.
IIUC the TS design team has been very cautious to not introduce those. Instead, they prefer to introduce either clear syntax for it, like
This has 0% to do with HKTs, and in fact, most of the use cases I've encountered are with concrete types and interfaces, not abstract ones. |
@weswigham Here's a userland alternative that might work: add a type Mutates<T, KS extends keyof T> =
T extends {readonly [P in KS]: T[P]} ? never : T
interface Array<T> {
splice(this: Mutates<this, number | "length">, index: number, remove: number, ...replacements: T[]): T[]
push(this: Mutates<this, number | "length">, value: T): number
pop(this: Mutates<this, number | "length">): T
}
interface Set<T> {
add(this: Mutates<this, "size">, value: T): this
delete(this: Mutates<this, "size">, value: T): boolean
clear(this: Mutates<this, "size">): void
}
interface Map<K, V> {
set(this: Mutates<this, "size">, key: K, value: V): this
delete(this: Mutates<this, "size">, key: K): boolean
clear(this: Mutates<this, "size">): void
}
// Little bit of setup code for the interesting stuff.
// An internal marker symbol
type IDBTransactionSym = unique symbol
interface IDBTransaction {
[IDBTransactionSym]: never
objectStore(name: string): this extends Readonly<IDBTransaction> ? Readonly<IDBObjectStore> : IDBObjectStore
}
// An internal marker symbol
type IDBObjectStoreSym = unique symbol
interface IDBObjectStore {
[IDBObjectStoreSym]: never
add(this: Mutates<this, IDBObjectStoreSym>, value: Value, key: Key): IDBRequest
clear(this: Mutates<this, IDBObjectStoreSym>): IDBRequest
createIndex(this: Mutates<this, IDBObjectStoreSym>, name: string): IDBRequest
delete(this: Mutates<this, IDBObjectStoreSym>, key: Key | KeyRange): IDBRequest
deleteIndex(this: Mutates<this, IDBObjectStoreSym>, name: string): IDBRequest
put(this: Mutates<this, IDBObjectStoreSym>, value: Value, key: Key): IDBRequest
openCursor(): this extends Readonly<IDBObjectStore> ? IDBCursor : IDBCursorWithValue
openKeyCursor(): IDBCursor
}
interface IDBCursor {
// All the usual properties
advance(this: Mutates<this, "key" | "primaryKey">, count: number): void
continue(this: Mutates<this, "key" | "primaryKey">, key?: Key): void
continuePrimaryKey(this: Mutates<this, "key" | "primaryKey">, key: Key, primaryKey: Key): void
}
interface IDBCursorWithValue extends IDBCursor {
value: any
delete(this: Mutates<this, "key" | "primaryKey" | "value">): void
update(this: Mutates<this, "key" | "primaryKey" | "value">, value: any): void
} After looking at that, I feel I like that better, and maybe that could just be a built-in type with some things in the docs to note how this could be used. |
@weswigham Related comment...is that a bug? In either case, it's blocking this. |
Closing in favor of #29435 + a potential future effects suggestion. |
Search Terms
Suggestion
I would like to see a return type operator
T mutates this[A], U[B], ...
to indicate that zero or more properties on types, likethis
orU
above, are modified when the method is called. If eitherthis[A]
orU[B]
is readonly, the method should be visible but uncallable, and if all depended-upon properties are writable, it should be both visible and callable. From the callee's perspective, it'd only seeT
.Alternatively, you could implement it as a helper type
Mutates<T, [{O: this, K: A}, {O: U, K: B}]>
, but there are pitfalls I will explain here in a bit:The implementation above is complicated (I need the ability to intersect tuples' entries like I can union their entries via
T[number]
), but it's conceptually simple:foo(value: A): Mutates<B, this[number]>
is equivalent tofoo(value: A): this extends {readonly [P in number]: unknown} ? never : B
.The issue with this deriviation is that it doesn't prevent you from calling the method - it just prevents you from using the result. So if you were to add a theoretical
uncallable
primitive type to prevent even invoking the function, it'd work more like this:This would change the desugaring to this, which is what I really want:
foo(value: A): Mutates<B, this[number]>
is equivalent tofoo(value: A): this extends {readonly [P in number]: unknown} ? uncallable : B
. However, it's pretty plainly obvious that this is a terrible idea to implement as a primitive type, which is why I proposed it as a new return type operator.In terms of assignability,
(value: A) => T mutating U[K]
is assignable to(value: A) => T
and(value: A) => T mutating SubtypeOfU[K]
, but not(value: Readonly<A>) => T
,(value: Readonly<A>) => never
, or(value: A) => T mutating SupertypeOfU[K]
. Also,(value: A) => T
and(value: Readonly<A>) => T
are themselves assignable to(value: Readonly<A>) => T mutating U[K]
for allU
andK
.In addition to the above, I propose this should be generally inferred for TS functions, only required in type definitions.
Use Cases
This would enable you to patch the
Array<T>
type appropriately to allow the obvioustype ReadonlyArray<T> = Readonly<Array<T>>
. Conveniently, if you patch all the appropriate methods, you could even ensure it's covariant. But this wouldn't be the only area where it'd help, such as:transaction(storeName: string, mode: "readonly"): Readonly<IDBTransaction>
to enforce readonly-ness of that transaction at the type level.Examples
Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: