[заимствование — англ.]
Позволяет передавать значения в функцию и получать наиболее точный тип значения
далее по коду:
- либо измененный (Morphed)
{value: 'open'}
>>>{value: 'closed'}
- либо неподконтрольный (Leaved)
{value: 'closed'}
>>>undefined
Поставить ⭐GitHub | Поддержать автора
📢 V1 уже в пути!
Вы можете следить за ходом разработки на GitHub, а также опробовать* бета-версии:
npm install borrowing@next --save-exact
(*) - API экспериментальный и может измениться к моменту релиза.
English | Русский
[!NOTE] Примечание
Пример
import { Ownership } from 'borrowing'
import { replaceStr, sendMessage } from './lib'
const value = 'Привет, мир!' // тип 'Привет, мир!'
let ownership = new Ownership<string>().capture(value).give()
replaceStr(ownership, 'ИЗМЕН4И8ЫЙ МNР')
let morphedValue = ownership.take() // новый тип 'ИЗМЕН4И8ЫЙ МNР' | (*)
ownership // тип `Ownership<string, 'ИЗМЕН4И8ЫЙ МNР', ...>`
ownership = ownership.give()
sendMessage(ownership)
ownership // новый тип `undefined`
Реализация функций (assertion fuctions):
// 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 // тип `string`
fetch('https://web.site/api/log', { method: 'POST', body: value })
drop(ownership)
}
▶ Поэкспериментировать в TypeScript Playground
Содержимое
Полезные ссылки
- Про assertion функции в TypeScript Handbook [ англ. ]
- Can assertion functions provide a better experience for library users? [ англ. ] \ Пост на Reddit, демонстрирующий подходы к реализации механизмов заимствования на TypeScript. \ (некоторые размышления в процессе разработки данной библиотеки)
- BorrowScript (спецификация, на стадии проектирования) \ "TypeScript Синтаксис, Анализатор Заимствований Rust (borrow checker), Философия Go ... Без Сборщика Мусора"
[!tip] Совет
Используйте библиотеку в паре с правилами
no-unsafe-*
изtypescript-eslint
, такими какno-unsafe-call
.
Это позволяет предотвратить дальнейшее использование экземпляра Ownership, как после вызоваtake()
, так и после любого другого действия, приводящего кnever
.
Сниппеты для VS Code
Добавьте приведенные сниппеты в Global Snippets file (Ctrl+Shift+P > Snippets: Configure Snippets
). \
В файлах со сниппетами для одного языка свойство scope
можно убрать.
{
"Создать экземпляр `Ownership`": {
"scope": "typescript,typescriptreact",
"prefix": "ownership",
"body": ["new Ownership<${1:string}>().capture(${2:'hello'} as const).give();"],
},
"Повторно передать владение над `Ownership`": {
"scope": "typescript,typescriptreact",
"prefix": "give",
"body": [
"${0:ownership} = ${0:ownership}.give();",
// "$CLIPBOARD = $CLIPBOARD.give();"
],
},
"Создать `MorphAssertion` функцию": {
"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});",
"}",
],
},
"Создать `LeaveAssertion` функцию": {
"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
Ownership
Ownership(options?):
Ownership<General, Captured, State, ReleasePayload>
@summary
Конструктор примитивов, определяющих владение над значением определенного типа. \ Общий (General) тип значения указывается в списке параметров дженерика.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>({ throwOnWrongState: false }) // тип `Ownership<Status, unknown, ...>`
@description
Является исходным и целевым типом assertion функций.
Вместе с assertion функциями реализует механизмы заимствования через видоизменение собственного типа. \
Тип экземпляра Ownership
отражает как тип заимствованного в каждый момент времени значения,
так и состояние заимствования.
Ownership#options
@summary
Позволяет настраивать аспекты работы механизмов заимствования в рантайме. \
Доступны для чтения/записи прежде любого использования экземпляра Ownership
.
Настройка | Тип | Значение по умолчанию | Описание |
---|---|---|---|
throwOnWrongState | boolean |
true |
Включает выброс ошибок при неудавшейся смене состояния владения/заимствования через встроенные assertion функции. |
takenPlaceholder | any |
undefined |
Переопределяет "пустое" значение для Ownership . Полезно, если тип захваченного значения включает в себя undefined . |
ConsumerOwnership#captured
@summary
Содержит заимствованное (captured) значение до момента, пока экземпляр Ownership
не будет обработан assertion функцией. \
Доступен внутри assertion функции. Во внешнем коде извлекается через функцию или метод take()
.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>().capture('pending' as const)
const captured = ownership.captured // тип '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 // тип `Status`
}
Ownership#capture()
@summary
Устанавливает значение, над которым определено владение. \
Рекомендуется использование литеральной формы значения в паре с утверждением as const
.
@example
type Status = 'pending' | 'success' | 'error'
const ownership = new Ownership<Status>().capture('pending' as const) // тип `Ownership<Status, 'pending', ...>`
Ownership#expectPayload()
@summary
Определяет для экземпляра Ownership
тип значения,
которое может быть передано assertion функцией в ходе ее выполнения.
Передача осуществляется в теле assertion функции
при вызове release
(3-й аргумент) или drop
(2-й аргумент). \
Переданное значение извлекается во внешнем коде
через функции take
(2-й параметр колбэка) или drop
(1-й параметр колбэка).
@example
const acceptExitCode = ownership.expectPayload<0 | 1>().give()
_assert(acceptExitCode)
drop(acceptExitCode, (payload) => {
payload // 0
})
// эквивалентно `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) // эквивалентно `release(ownership, undefined, 0)`
}
Ownership#give()
@summary
Подготавливает экземпляр Ownership
к передаче внутрь assertion функции.
@example
const ownership = new Ownership<string>().capture('pending' as const)
const ownershipArg = ownership.give()
_assert(ownership)
ownership // тип `never`
_assert(ownershipArg)
ownershipArg // тип `ProviderOwnership<...>`
function _assert<T extends Ownership.GenericBounds<string>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, 'success'> {
// (...)
}
Ownership#take()
@summary
Извлекает заимствованное (captured) значение. \
После извлечения экземпляр Ownership
больше не содержит значения.
@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
Метод take
не инвалидирует экземпляр Ownership
. \
По этой причине рекомендуется использование функции take()
.
// небезопасно, т.к. `ownership` все еще доступен для использования (не приведен к `undefined` или `never`)
_morphedValue = ownership.take()
// безопасная альтернатива - приводит (asserts) `ownership` к `never`
take(ownership, (str) => void (_morphedValue = str))
Вспомогательные типы
Пространство имен Ownership |
Описание |
---|---|
Ownership.Options |
Настройки механизмов заимствования в рантайме. |
Ownership.inferTypes<T> └─ T extends Ownership |
Типы параметров экземпляра по отдельности, например inferTypes<typeof ownership>['Captured'] . |
Ownership.GenericBounds<G,RP> ├─ G - General └─ RP - ReleasePayload |
Для использования в списке параметров дженерик-типа assertion функции, чтобы произвести маппинг из типа фактически переданного экземпляра Ownership .На выходе получается структура, удобная для использования в *Assertion дженерик-типах. |
Ownership.ParamsBounds<T> └─ T extends GenericBounds |
Для использования в качестве типа параметра assertion функции, принимающего экземпляр Ownership .Внутрь передается дженерик параметр для успешного маппинга в GenericBounds . |
Ownership.MorphAssertion<T,R> ├─ T extends GenericBounds └─ R - Released |
Целевой тип assertion функции, возвращающей Ownership с потенциально видоизмененным типом заимствованного (captured) значения. |
Ownership.LeaveAssertion<T> └─ T extends GenericBounds |
Целевой тип assertion функции, поглощающей заимствованное значение и инвалидирующей тип Ownership . |
@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) тип Ownership
внутри assertion функции. \
Это позволяет получать доступ к заимствованному (captured) значению.
@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 // тип `number`
}
@description
При включенной настройке throwOnWrongState
(true
по умолчанию) \
вызов функции borrow
должен предшествовать вызову assertion функций release
и drop
.
Это связано с внутренним отслеживанием состояния владения/заимствования.
const ownership = new Ownership<number>().give()
_assert(ownership) // выбрасывает ошибку
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
release(ownership, value) // Ошибка: Unable to release (not borrowed), call `borrow` first
}
@throws
При включенной настройке throwOnWrongState
(true
по умолчанию) выбрасывает ошибку 'Unable to borrow ...' \
если перед этим не была вызвана функция give
.
Это связано с внутренним отслеживанием состояния владения/заимствования.
const ownership = new Ownership<number>()
_assert(ownership) // выбрасывает ошибку
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
borrow(ownership) // Ошибка: Unable to borrow (not given), call `give` first
}
release
release(
Ownership
, value, payload): asserts ownership isnever
@summary
Трансформирует заимствованное (captured) значение. \ Позволяет assertion функции возвращать результат (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
Функция release
приводит переданный Ownership
к never
в теле самой assertion функции.
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, any> {
borrow(ownership)
release(ownership)
ownership // тип `never`
}
@throws
При включенной настройке throwOnWrongState
(true
по умолчанию) выбрасывает ошибку \
'Unable to release ...' если перед этим не были поочередно вызваны функции give
и borrow
.
Это связано с внутренним отслеживанием состояния владения/заимствования.
const ownership = new Ownership<number>().capture(123 as const).give()
_assert(ownership) // выбрасывает ошибку
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.MorphAssertion<T, any> {
release(ownership, value) // Ошибка: Unable to release (not borrowed), call `borrow` first
}
drop
drop(
Ownership
, payload | receiver): asserts ownership isnever
@summary
Удаляет заимствованное (captured) значение. \ Позволяет assertion функции возвращать результат (payload), а внешнему коду - извлекать его.
@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
Может использоваться как в теле assertion функции (LeaveAssertion
)
вместо release
, так и во внешнем коде вместо take
. \
Избранное поведение определяется типом 2-го параметра -
извлечение значения, если это колбэк, и запись результата (payload) для остальных типов.
release(ownership, undefined, Result.Ok)
// эквивалентно
drop(_ownership, Result.Ok)
take(_ownership_, (_, _payload) => {})
// эквивалентно
drop(__ownership, (_payload) => {})
Функция drop
приводит переданный Ownership
к never
. \
Может использоваться внутри assertion функции, которая
инвалидирует Ownership
параметр, приводя его к undefined
.
function _assert<T extends Ownership.GenericBounds<number>>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is undefined {
borrow(ownership)
drop(ownership)
ownership // тип `never`
}
@throws
При включенной настройке throwOnWrongState
(true
по умолчанию) выбрасывает ошибку 'Unable to release ...' \
если перед этим не были поочередно вызваны функции give
и borrow
.
Это связано с внутренним отслеживанием состояния владения/заимствования.
const ownership = new Ownership<number>().capture(123 as const).give()
_assert(ownership) // выбрасывает ошибку
function _assert<T extends Ownership.GenericBounds>(
ownership: Ownership.ParamsBounds<T> | undefined,
): asserts ownership is Ownership.LeaveAssertion<T> {
// (...)
drop(ownership) // Ошибка: Unable to release (not borrowed), call `borrow` first
}
take
take(
Ownership
, receiver): asserts ownership isnever
@summary
Извлекает заимствованное значение из переданного Ownership
. \
После извлечения экземпляр Ownership
больше не содержит значения и имеет тип never
.
Также позволяет извлечь результат (payload) assertion функции, передавая его в колбэк вторым параметром.
@example
const ownership = new Ownership<number>().capture(123 as const)
let _dst: number
take(ownership, (value, _payload: unknown) => (_dst = value))
@description
Функция take
инвалидирует Ownership
параметр, приводя его к never
. \
По этой причине она рекомендуется к использованию вместо Ownership#take()
.
let _dst = ownership.take()
ownership // тип `Ownership<...>`
// безопасный вариант
take(ownership, (value) => (_dst = value))
ownership // тип `never`
@throws
При включенной настройке throwOnWrongState
(true
по умолчанию) выбрасывает ошибку 'Unable to take (not settled) ...' \
если владение над значением все еще не возвращено по результатам цепочки вызовов give
, borrow
и release/drop
.
Это связано с внутренним отслеживанием состояния владения/заимствования.
const ownership = new Ownership<number>().capture(123 as const).give()
take(ownership, (value) => (_dst = value)) // Ошибка: Unable to take (not settled), call `release` or `drop` first or remove `give` call
Также выбрасывает ошибку 'Unable to take (already taken)' при попытке повторного вызова на том же экземпляре Ownership
.
const ownership = new Ownership<number>().capture(123 as const)
take(ownership, (value) => (_dst = value))
take(ownership, (value) => (_dst = value)) // Ошибка: Unable to take (already taken)
Ограничения и советы
Всегда вызывайте функции
borrow
иrelease/drop
(в таком порядке) в теле assertion функций. \: asserts ownership is Ownership.MorphAssertion { borrow + release }
\: asserts ownership is Ownership.LeaveAssertion { borrow + drop }
\ В будущем наличие необходимых вызовов может проверяться линтером через плагин (запланирован).Не забывайте про вызов метода
give
перед передачей экземпляраOwnership
в assertion функцию. \ Однако даже при невалидном вызове методtake
позволяет получить заимствованное (captured) значение с последним валидным типом.
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 // тип 'closed'
update(ownership, 'open')
const v2 = ownership.take().value // тип 'closed' (не изменился)
type v2 = Ownership.inferTypes<typeof ownership>['Captured']['value'] // НЕПРАВИЛЬНЫЙ ТИП 'open' (то же и с `take` функцией)
ownership = ownership.give()
update(ownership, 'open')
const v3 = ownership.take().value // тип '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. К сожалению, функция take
и вспомогательный тип Ownership.inferTypes
все еще страдают от изменения типа. \
Планируется снизить риск нарушения этих правил путем переработки API
и реализации ранее упомянутого функционала для линтера.
Вышеприведенные требования проверяются в рантайме при включенной настройке throwOnWrongState
(true
по умолчанию). \
В этом случае их нарушение приведет к выбросу ошибки.
- Вызывайте
capture/give
тут же при создании экземпляраOwnership
и присваивании его переменной. \ Это позволит не иметь других ссылок на значение, которые могут стать невалидными после вызовов assertion функций.
interface Field {
value: string
}
declare function morph(/* ... */): void ... // некоторая `MorphAssertion` функция
// ❌ Неправильно
const field = { value: 'Hello' } as const
const fieldMutRef = new Ownership<Field>().capture(field).give()
morph(fieldMutRef)
drop(fieldMutRef)
fieldMutRef // тип `never`
field.value = 'Still accessible'
// ❌ Неправильно
const fieldRef = new Ownership<Field>().capture({ value: 'Hello' } as const)
const fieldMutRef = fieldRef.give()
morph(fieldRef)
drop(fieldMutRef)
fieldMutRef // тип `never`
fieldRef.take().value = 'Still accessible' // Ошибка (TypeError): Cannot read properties of undefined (reading 'value')
// ✅ Правильно
let fieldMutRef = new Ownership<Field>().capture({ value: 'Hello' } as const).give()
morph(fieldMutRef)
fieldMutRef = fieldMutRef.give() // `let` позволяет передавать владение несколько раз, используя ссылку, хранящуюся в единственной переменной
morph(fieldMutRef)
take(fieldMutRef, (field) => {
// (...)
})
fieldMutRef // тип `never`
// не остается других ссылок на значение