JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 9
  • Score
    100M100P100Q49175F
  • License MIT

A utility library that provides borrowing mechanisms via assertion functions

Package Exports

  • borrowing
  • borrowing/next

Readme

borrowing

npm

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)
}

See it in action


Table of Contents

Resources

[!TIP]

Use in combination with no-unsafe-*-rules from typescript-eslint, such as no-unsafe-call.
This prevents further use of the Ownership instance, either after calling take() or after any other assertion that results in never.

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);",
      "}",
    ],
  },
}

Scroll Up ↩

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>]) => void
Public

Variables

_WORK_IN_PROGRESS variable

Test

Signature:

_WORK_IN_PROGRESS: boolean
Beta

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

  1. Always call the borrow and release/drop functions (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).

  2. Don't forget to call the give method before passing the Ownership instance to the assertion function.
    However, even if the call is invalid, the take method 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.

  1. Call capture/give immediately when creating an Ownership instance 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

Scroll Up ↩

Previous versions