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, 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:
typeMutates<T,Depsextends{O: unknown,K: keyof this["O"]}[]>={0: Depsextends[] ? T :
((d: Deps)=>void)extends((first: infer A, ...rest: infer B)=>void) ?
{readonly[PinA["K"]]: unknown}extendsA["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:
typeMutates<T,Depsextends{O: unknown,K: keyof this["O"]}[]>={0: Depsextends[] ? T :
((d: Deps)=>void)extends((first: infer A, ...rest: infer B)=>void) ?
{readonly[PinA["K"]]: unknown}extendsA["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
interfaceArray<T>{splice(index: number,remove: number, ...replacements: T[]): T[]mutates this[number],this["length"]push(value: T): numbermutatesthis[number],this["length"]pop(): Tmutates this[number],this["length"]}// An internal marker symboltypeSetSym=unique symbolinterfaceSet<T>{[SetSym]: neveradd(value: T): thismutates this[SetSym],this["size"]delete(value: T): booleanmutatesthis[SetSym],this["size"]clear(): voidmutates this[SetSym],this["size"]}// An internal marker symboltypeMapSym=unique symbolinterfaceMap<K,V>{[MapSym]: neverset(key: K,value: V): thismutates this[MapSym],this["size"]delete(key: K): booleanmutatesthis[MapSym],this["size"]clear(): voidmutates this[MapSym],this["size"]}// Little bit of setup code for the interesting stuff.interfaceIDBTransaction{objectStore(name: string): this extendsReadonly<IDBTransaction> ? Readonly<IDBObjectStore> : IDBObjectStore}// An internal marker symboltypeIDBObjectStoreSym=unique symbolinterfaceIDBObjectStore{[IDBObjectStoreSym]: neveradd(value: Value,key: Key): IDBRequestmutatesthis[IDBObjectStoreSym]clear(): IDBRequestmutatesthis[IDBObjectStoreSym]createIndex(name: string): IDBRequestmutatesthis[IDBObjectStoreSym]delete(key: Key|KeyRange): IDBRequestmutatesthis[IDBObjectStoreSym]deleteIndex(name: string): IDBRequestmutatesthis[IDBObjectStoreSym]put(value: Value,key: Key): IDBRequestmutatesthis[IDBObjectStoreSym]openCursor(): this extendsReadonly<IDBObjectStore> ? IDBCursor : IDBCursorWithValueopenKeyCursor(): IDBCursor}typeIDBCursorSym=unique symbolinterfaceIDBCursor{[IDBCursorSym]: never// All the usual propertiesadvance(count: number): voidmutates this[IDBCursorSym]continue(key?: Key): voidmutates this[IDBCursorSym]continuePrimaryKey(key: Key,primaryKey: Key): voidmutates this[IDBCursorSym]}interfaceIDBCursorWithValueextendsIDBCursor{delete(): voidupdate(): void}Checklist
My suggestion meets these guidelines:
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, likethisorUabove, 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
uncallableprimitive 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) => Tand(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) => Tand(value: Readonly<A>) => Tare themselves assignable to(value: Readonly<A>) => T mutating U[K]for allUandK.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: