Package Exports
- borrowing
Readme
Allows you to pass values to a function and get the most accurate value type
in the following code:
- either Morphed one
{value: 'open'}>>>{value: 'closed'} - or no longer under control (Leaved)
{value: 'closed'}>>>undefined
Become a π§Stargazer | Support the author
English | Π ΡΡΡΠΊΠΈΠΉ
[!NOTE]
V1 is on its way! You can follow the development on GitHub, and try out* the beta versions:
npm install borrowing@next --save-exact(*) - The API is experimental and may change in the release version.
Example
import { Ownership } from 'borrowing'
import { replaceStr, sendMessage } from './lib'
const value = 'Hello, world!' // type 'Hello, world!'
let ownership = new Ownership<string>().capture(value).give()
replaceStr(ownership, 'M0RPH3D W0R1D')
let morphedValue = ownership.take() // new type 'M0RPH3D W0R1D' | (*)
ownership // type `Ownership<string, 'M0RPH3D W0R1D', ...>`
ownership = ownership.give()
sendMessage(ownership)
ownership // new type `undefined`Implementation of assertions functions:
// lib.ts
import { borrow, drop, Ownership, release } from 'borrowing'
export function replaceStr<V extends string, T extends Ownership.GenericBounds<string>>(
ownership: Ownership.ParamsBounds<T> | undefined,
value: V,
): asserts ownership is Ownership.MorphAssertion<T, V> {
release(ownership, value)
}
export function sendMessage<T extends Ownership.GenericBounds<string>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is undefined {
borrow(ownership)
const value = ownership.captured // type `string`
fetch('https://web.site/api/log', { method: 'POST', body: value })
drop(ownership)
}βΆ See it in action
Table of Contents
Resources
- Assertion functions in TypeScript Handbook
- Can assertion functions provide a better experience for library users?
A Reddit post demonstrating approaches to implementing borrowing mechanisms in TypeScript.
(some thoughts on the development of this library) - BorrowScript (spec, design phase)
"TypeScript Syntax, Rust Borrow Checker, Go Philosophies ... No Garbage Collection"
[!tip]
Use in combination with
no-unsafe-*-rules fromtypescript-eslint, such asno-unsafe-call.
This prevents further use of the Ownership instance, either after callingtake()or after any other assertion that results innever.
VS Code Snippets
Put this in your Global Snippets file (Ctrl+Shift+P > Snippets: Configure Snippets).
You can remove the scope property in single-language snippet files.
{
"Create borrowing-ready `Ownership` instance": {
"scope": "typescript,typescriptreact",
"prefix": "ownership",
"body": ["new Ownership<${1:string}>().capture(${2:'hello'} as const).give();"],
},
"Give settled `Ownership` again": {
"scope": "typescript,typescriptreact",
"prefix": "give",
"body": [
"${0:ownership} = ${0:ownership}.give();",
// "$CLIPBOARD = $CLIPBOARD.give();"
],
},
"Create `MorphAssertion` function": {
"scope": "typescript,typescriptreact",
"prefix": "morph",
"body": [
"function ${1:assert}<T extends Ownership.GenericBounds<${2:string}>>(",
" ownership: Ownership.ParamsBounds<T> | undefined,",
"): asserts ownership is Ownership.MorphAssertion<T, ${3:T['Captured']}> {",
" borrow(ownership);",
" $0",
" release(ownership, ${4:ownership.captured});",
"}",
],
},
"Create `LeaveAssertion` function": {
"scope": "typescript,typescriptreact",
"prefix": "leave",
"body": [
"function ${1:assert}<T extends Ownership.GenericBounds<${2:string}>>(",
" ownership: Ownership.ParamsBounds<T> | undefined,",
"): asserts ownership is Ownership.LeaveAssertion<T> {",
" borrow(ownership);",
" $0",
" drop(ownership$3);",
"}",
],
},
}
API Reference
Ownership
Ownership(options?):
Ownership<General, Captured, State, ReleasePayload>
@summary
A constructor of primitives that define ownership over a value of a particular type.
The General type of the value is specified in the generic parameter list.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>({ throwOnWrongState: false }) // type `Ownership<Status, unknown, ...>`@description
It is the source and target type of assertion functions.
In combination with assertion functions, it implements borrowing mechanisms through modification of its own type.
The type of Ownership instance reflects both the type of the captured value and the borrowing state.
Ownership#options
@summary
Allows to customize aspects of how borrowing mechanisms work at runtime.
Are public before any use of the Ownership instance.
| Option | Type | Default | Description |
|---|---|---|---|
| throwOnWrongState | boolean |
true |
Throw error when ownership/borrowing state changes fail via built-in assertion functions. |
| takenPlaceholder | any |
undefined |
Override an "empty" value for Ownership. Useful if the captured value's type includes undefined. |
ConsumerOwnership#captured
@summary
Contains the captured value until the Ownership instance is processed by the assertion function.
Is public inside the assertion function. In external code, retrieved via the take() function or method.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>().capture('pending' as const)
const captured = ownership.captured // type 'pending'
function _assert<T extends Ownership.GenericBounds<Status>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership)
const captured = ownership.captured // type `Status`
}Ownership#capture()
@summary
Sets the value over which ownership is defined.
It is recommended to use the literal form of the value in combination with the as const assertion.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>().capture('pending' as const) // type `Ownership<Status, 'pending', ...>`Ownership#expectPayload()
@summary
Specifies for an Ownership instance the type of value
that can be passed from the assertion function during its execution.
The payload is set in the body of the assertion function
when calling release (3rd argument) or drop (2nd argument).
The passed value is retrieved in external code
via the take (2nd callback parameter) or drop (1st callback parameter) functions.
@example
const acceptExitCode = ownership.expectPayload<0 | 1>().give()
_assert(acceptExitCode)
drop(acceptExitCode, (payload) => {
payload // 0
})
// same as `take(acceptExitCode, (_, payload) => { ... })`
function _assert<T extends Ownership.GenericBounds<number, 0 | 1>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership)
drop(ownership, 0) // same as `release(ownership, undefined, 0)`
}Ownership#give()
@summary
Prepares an Ownership instance to be given into the assertion function.
@example
const ownership = new Ownership<string>().capture('pending' as const)
const ownershipArg = ownership.give()
_assert(ownership)
ownership // type `never`
_assert(ownershipArg)
ownershipArg // type `ProviderOwnership<...>`
function _assert<T extends Ownership.GenericBounds<string>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, 'success'> {
// (...)
}Ownership#take()
@summary
Retrieves the captured value.
After retrieval, the Ownership instance no longer contains a value.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>().capture('pending' as const)
let _value = ownership.take() // 'pending'
_value = ownership.take() // undefined@description
The take method does not invalidate the Ownership instance.
For this reason, it is recommended to use the take() function.
// unsafe because the ownership is still in use (not `undefined` or `never`)
_morphedValue = ownership.take()
// safer alternative - asserts `ownership is never`
take(ownership, (str) => void (_morphedValue = str))Utility Types
Ownership namespace |
Description |
|---|---|
Ownership.Options |
Runtime borrowing mechanism settings. |
Ownership.inferTypes<T>ββ T extends Ownership |
The instance parameter types individually, such as inferTypes<typeof ownership>['Captured']. |
Ownership.GenericBounds<G,RP>ββ G - Generalββ RP - ReleasePayload |
For use in the parameter list of a generic assertion function to perform a mapping from the type of the actual Ownership instance passed.The resulting type is a structure convenient for use in *Assertion utility types. |
Ownership.ParamsBounds<T>ββ T extends GenericBounds |
For use as the type of an assertion function parameter that takes an Ownership instance.A generic parameter extending GenericBounds is passed inside to ensure successful mapping. |
Ownership.MorphAssertion<T,R>ββ T extends GenericBoundsββ R - Released |
The target type of an assertion function that results in Ownership with a potentially morphed type of the captured value. |
Ownership.LeaveAssertion<T>ββ T extends GenericBounds |
The target type of an assertion function that consumes a borrowed value completely and invalidates the Ownership type. |
@example
const options: Ownership.Options = {
throwOnWrongState: false,
takenPlaceholder: undefined,
}
const _ownership = new Ownership<string>(options).capture('foo' as const)
type Captured = Ownership.inferTypes<typeof _ownership>['Captured'] // 'foo'
function _assert<T extends Ownership.GenericBounds<string>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, string> {
// (...)
release(ownership, 'bar')
}
function _throwAway<T extends Ownership.GenericBounds<string, 0 | 1>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership)
type Payload = Ownership.inferTypes<typeof ownership>['ReleasePayload'] // 0 | 1
drop(ownership, 0)
}borrow
borrow(
Ownership): asserts ownership isConsumerOwnership
@summary
Narrows the type of Ownership inside the assertion function.
This allows access to the captured value.
@example
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership)
const value = ownership.captured // type `number`
}@description
When the throwOnWrongState setting is enabled (true by default),
a call to the borrow function must precede a call to the release and drop assertion functions.
This is due to internal tracking of the ownership/borrowing status.
const ownership = new Ownership<number>().give()
_assert(ownership) // throws
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
release(ownership, value) // Error: Unable to release (not borrowed), call `borrow` first
}@throws
When throwOnWrongState setting is enabled (true by default), throws an 'Unable to borrow ...' error
if give has not been called before.
This is due to internal tracking of the ownership/borrowing status.
const ownership = new Ownership<number>()
_assert(ownership) // throws
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership) // Error: Unable to borrow (not given), call `give` first
}release
release(
Ownership, value, payload): asserts ownership isnever
@summary
Morphs a captured value.
Allows the assertion function to return a payload.
@example
type Status = 'open' | 'closed'
enum Result {
Ok,
Err,
}
function close<T extends Ownership.GenericBounds<Status, Result>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, 'closed'> {
borrow(ownership)
release(ownership, 'closed', Result.Ok)
}@description
The release function narrows the passed Ownership to never in the body of the assertion function itself.
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, any> {
borrow(ownership)
release(ownership)
ownership // type `never`
}@throws
When throwOnWrongState setting is enabled (true by default), throws an 'Unable to release ...' error
if the give and borrow functions were not called in sequence beforehand.
This is due to internal tracking of the ownership/borrowing status.
const ownership = new Ownership<number>().capture(123 as const).give()
_assert(ownership) // throws
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, any> {
release(ownership, value) // Error: Unable to release (not borrowed), call `borrow` first
}drop
drop(
Ownership, payload | receiver): asserts ownership isnever
@summary
Garbages a captured value.
Allows the assertion function to return a payload, and external code to retrieve it.
@example
enum Result {
Ok,
Err,
}
function _assert<T extends Ownership.GenericBounds<any, Result>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership)
drop(ownership, Result.Ok)
}
const ownership = new Ownership().expectPayload<Result>().give()
_assert(ownership)
drop(ownership, (payload) => {
payload // Result.Ok
})@description
Can be used both in the body of an assertion function (LeaveAssertion)
instead of release, and in external code instead of take.
The selected behavior is determined by the type of the 2nd parameter -
extracting the value if it is a callback, and writing the payload for other types.
release(ownership, undefined, Result.Ok)
// same as
drop(_ownership, Result.Ok)
take(_ownership_, (_, _payload) => {})
// same as
drop(__ownership, (_payload) => {})The drop function narrows the passed Ownership to never.
Can be used inside an assertion function that
invalidates the Ownership parameter by narrowing it to undefined.
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is undefined {
borrow(ownership)
drop(ownership)
ownership // type `never`
}@throws
When throwOnWrongState setting is enabled (true by default), throws an 'Unable to release ...' error
if the give and borrow functions were not called in sequence beforehand.
This is due to internal tracking of the ownership/borrowing status.
const ownership = new Ownership<number>().capture(123 as const).give()
_assert(ownership) // throws
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
// (...)
drop(ownership) // Error: Unable to release (not borrowed), call `borrow` first
}take
take(
Ownership, receiver): asserts ownership isnever
@summary
Retrieves the captured value from the passed Ownership.
After retrieval, the Ownership instance no longer contains a value and is of type never.
It also allows to extract the payload of the assertion function by passing it to the callback as the second parameter.
@example
const ownership = new Ownership<number>().capture(123 as const)
let _dst: number
take(ownership, (value, _payload: unknown) => (_dst = value))@description
The take function invalidates the Ownership parameter by narrowing it to never.
For this reason, it is recommended to use it instead of Ownership#take().
let _dst = ownership.take()
ownership // type `Ownership<...>`
// safe
take(ownership, (value) => (_dst = value))
ownership // type `never`@throws
When throwOnWrongState setting is enabled (true by default), throws an 'Unable to take (not settled) ...' error
if ownership of the value has not yet been returned as a result of sequential give, borrow, and release/drop calls.
This is due to internal tracking of the ownership/borrowing status.
const ownership = new Ownership<number>().capture(123 as const).give()
take(ownership, (value) => (_dst = value)) // Error: Unable to take (not settled), call `release` or `drop` first or remove `give` callAlso throws 'Unable to take (already taken)' error when trying to call again on the same Ownership instance.
const ownership = new Ownership<number>().capture(123 as const)
take(ownership, (value) => (_dst = value))
take(ownership, (value) => (_dst = value)) // Error: Unable to take (already taken)Limitations and Recommendations
Always call the
borrowandrelease/dropfunctions (in that order) in the body of assertion functions.: asserts ownership is Ownership.MorphAssertion { borrow + release }: asserts ownership is Ownership.LeaveAssertion { borrow + β drop β β }
In the future, the presence of the necessary calls may be checked by the linter via a plugin (planned).Don't forget to call the
givemethod before passing theOwnershipinstance to the assertion function.
However, even if the call is invalid, thetakemethod allows you to get the captured value with the last valid type.
interface State {
value: string
}
let ownership = new Ownership<State>({ throwOnWrongState: false }).capture({ value: 'open' } as const).give()
update(ownership, 'closed')
const v1 = ownership.take().value // type 'closed'
update(ownership, 'open')
const v2 = ownership.take().value // type 'closed' (has not changed)
type v2 = Ownership.inferTypes<typeof ownership>['Captured']['value'] // WRONG TYPE 'open' (same with `take` function)
ownership = ownership.give()
update(ownership, 'open')
const v3 = ownership.take().value // type 'open'
function update<T extends Ownership.GenericBounds<State>, V extends 'open' | 'closed'>(
ownership: Ownership.ParamsBounds<T> | undefined,
value: V,
): asserts ownership is Ownership.MorphAssertion<T, { value: V }> {
borrow(ownership)
release(ownership, { value })
}2.1. Unfortunately, the take function and the Ownership.inferTypes utility type still suffer from type changing.
It is planned to reduce the risk of violating these rules by reworking the API
and implementing the previously mentioned functionality for the linter.
The above requirements are checked at runtime when the throwOnWrongState setting is enabled (true by default).
In this case, their violation will result in an error being thrown.
- Call
capture/giveimmediately when creating anOwnershipinstance and assigning it to a variable.
This will prevent you from having other references to the value that may become invalid after assertion functions calls.
interface Field {
value: string
}
declare function morph(/* ... */): void ... // some `MorphAssertion` function
// β Incorrect
const field = { value: 'Hello' } as const
const fieldMutRef = new Ownership<Field>().capture(field).give()
morph(fieldMutRef)
drop(fieldMutRef)
fieldMutRef // type `never`
field.value = 'Still accessible'
// β Incorrect
const fieldRef = new Ownership<Field>().capture({ value: 'Hello' } as const)
const fieldMutRef = fieldRef.give()
morph(fieldRef)
drop(fieldMutRef)
fieldMutRef // type `never`
fieldRef.take().value = 'Still accessible' // TypeError: Cannot read properties of undefined (reading 'value')
// β
Correct
let fieldMutRef = new Ownership<Field>().capture({ value: 'Hello' } as const).give()
morph(fieldMutRef)
fieldMutRef = fieldMutRef.give() // `let` allows ownership to be given multiple times using a single reference
morph(fieldMutRef)
take(fieldMutRef, (field) => {
// (...)
})
fieldMutRef // type `never`
// there are no other references left