Package Exports
- borrowing
- borrowing/next
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
Warning 🚧
This version of borrowing is under development.
Documentation may not be up to date, API may (will) have breaking changes.
You can try out the fresh beta version by installing it as follows:
npm install borrowing@next --save-exact
New functionality is available at the borrowing/next entrypoint.
***
The documentation for the current (latest) version is located here.
English | Русский
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)
}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
Functions
borrow() function
Signature:
declare function borrow<T, C extends T = T>(
cell: RefCell<T, C>,
): asserts cell is Borrow<T, C>borrowMut() function
Signature:
declare function borrowMut<T>(
cell: RefCellMut<T, T>,
): asserts cell is BorrowMut<T, T>mut() function
Signature:
mut: <T>(v: T) => Mut<T>Public
scope() function
Signature:
scope: <Move extends RefCellBase[]>(...args: [...Move, ScopeBlock<Move>]) => voidPublic
Variables
_WORK_IN_PROGRESS variable
Test
Signature:
_WORK_IN_PROGRESS: booleanBeta
Type Aliases
Borrow type
Signature:
type Borrow<T = unknown, C extends T = T> = Ref<T, C> & {
take(): RefCell<T, C>
}BorrowMut type
Signature:
type BorrowMut<T = unknown, C extends T = T> = RefMut<T, C> & {
take(): RefCellMut<T, C>
}Ref type
Signature:
type Ref<T = unknown, C extends T = T> = RefCell<T, C> & {
deref(): C
}RefCell type
Signature:
type RefCell<T = any, C extends T = T> = RefCellBase<T, C> &
UnionToIntersection<Traits<T, C, DerivedTraits<C>>>RefCellMut type
Signature:
type RefCellMut<T = any, C extends T = T> = RefCellMutBase<T, C> &
UnionToIntersection<Traits<T, C, DerivedTraits<C>>>RefMut type
Signature:
type RefMut<T = unknown, C extends T = T> = RefCellMut<T, C> & {
deref(): C
}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