Description
Search Terms
- mutating function
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, like this
or U
above, are modified when the method is called. If either this[A]
or U[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 see T
.
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:
type Mutates<T, Deps extends {O: unknown, K: keyof this["O"]}[]> = {
0: Deps extends [] ? T :
((d: Deps) => void) extends ((first: infer A, ...rest: infer B) => void) ?
{readonly [P in A["K"]]: unknown} extends A["O"] ? Mutates<T, B> :
never :
never
}[0]
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 to foo(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:
type Mutates<T, Deps extends {O: unknown, K: keyof this["O"]}[]> = {
0: Deps extends [] ? T :
((d: Deps) => void) extends ((first: infer A, ...rest: infer B) => void) ?
{readonly [P in A["K"]]: unknown} extends A["O"] ? Mutates<T, B> :
uncallable :
never
}[0]
This would change the desugaring to this, which is what I really want: foo(value: A): Mutates<B, this[number]>
is equivalent to foo(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 all U
and K
.
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 obvious type 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:
- Readonly transactions, where you could define
transaction(storeName: string, mode: "readonly"): Readonly<IDBTransaction>
to enforce readonly-ness of that transaction at the type level. - Enforcing immutability around maps, sets, and the like.
Examples
interface Array<T> {
splice(index: number, remove: number, ...replacements: T[]): T[] mutates this[number], this["length"]
push(value: T): number mutates this[number], this["length"]
pop(): T mutates this[number], this["length"]
}
// An internal marker symbol
type SetSym = unique symbol
interface Set<T> {
[SetSym]: never
add(value: T): this mutates this[SetSym], this["size"]
delete(value: T): boolean mutates this[SetSym], this["size"]
clear(): void mutates this[SetSym], this["size"]
}
// An internal marker symbol
type MapSym = unique symbol
interface Map<K, V> {
[MapSym]: never
set(key: K, value: V): this mutates this[MapSym], this["size"]
delete(key: K): boolean mutates this[MapSym], this["size"]
clear(): void mutates this[MapSym], this["size"]
}
// Little bit of setup code for the interesting stuff.
interface IDBTransaction {
objectStore(name: string): this extends Readonly<IDBTransaction> ? Readonly<IDBObjectStore> : IDBObjectStore
}
// An internal marker symbol
type IDBObjectStoreSym = unique symbol
interface IDBObjectStore {
[IDBObjectStoreSym]: never
add(value: Value, key: Key): IDBRequest mutates this[IDBObjectStoreSym]
clear(): IDBRequest mutates this[IDBObjectStoreSym]
createIndex(name: string): IDBRequest mutates this[IDBObjectStoreSym]
delete(key: Key | KeyRange): IDBRequest mutates this[IDBObjectStoreSym]
deleteIndex(name: string): IDBRequest mutates this[IDBObjectStoreSym]
put(value: Value, key: Key): IDBRequest mutates this[IDBObjectStoreSym]
openCursor(): this extends Readonly<IDBObjectStore> ? IDBCursor : IDBCursorWithValue
openKeyCursor(): IDBCursor
}
type IDBCursorSym = unique symbol
interface IDBCursor {
[IDBCursorSym]: never
// All the usual properties
advance(count: number): void mutates this[IDBCursorSym]
continue(key?: Key): void mutates this[IDBCursorSym]
continuePrimaryKey(key: Key, primaryKey: Key): void mutates this[IDBCursorSym]
}
interface IDBCursorWithValue extends IDBCursor {
delete(): void
update(): void
}
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.