-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Discussion: Improving typings #3413
Description
Constraints
Here is the set of constraints following proposal attemts to satisfy (please call
out if you find something is missing or doesn't belong)
- Provide a way to ensure that
ipfs-core
andipfs-http-client
implement
same base API.- They could intentionally diverge a bit (e.g. http-client takes extra HTTPOptions)
and so couldipfs-core
but there should be common subset between two.- Compatibility should be ensured type checker as opposed to discipline or
peer review
- Compatibility should be ensured type checker as opposed to discipline or
- They could intentionally diverge a bit (e.g. http-client takes extra HTTPOptions)
- Should require no type annotation repetitions.
- Types should be simple and stright forward, meaning avoid
magic types
that are hard to understand. - Cross package type dependecies should not depend on internal file structure
(as much as possible)
Proposal
I propose to satisify all of the listed constraints by doing following
-
Create a designated directory for reusable type definitions (e.g.
src/interface
or whatever wins bikeshed contest and have component specific
interface definitions foripfs.pin
,ipfs.files
,ipfs.dag
, etc..Note: This still creates file structure but it is agnostic of implementation
details. Alternative could be to use TS namespaces,
but they are left over from pre-esm days in TS and seems like non futureproof
approach, which is why using naming convetion seems like a better option
ipfs.pin
->interface/pin
,ipfs.files
->interface/files
, etc...Below is illustration of what
interface/pin.ts
could look like:import CID from "cids" import { AbortOptions } from "./utils" export interface API { addAll (source: ToPins, options?: AddOptions & AbortOptions): AsyncIterable<CID> add(source: ToPin, options?: AddOptions & AbortOptions): Promise<CID> rmAll(source: ToPins, options?: RemoveOptions & AbortOptions): AsyncIterable<CID> rm(source: ToPin, options?: RemoveOptions & AbortOptions): Promise<CID> ls(options?: ListOptions & AbortOptions): AsyncIterable<PinEntry> } export type ToPin = | CID | string | ToPinWithPath | ToPinWithCID export type ToPinWithPath = { path: string recursive?: boolean metadata?: any cid?: undefined } export type ToPinWithCID = { cid?: CID recursive?: boolean metadata?: any path?: string } export type ToPins = | ToPin | Iterable<ToPin> | AsyncIterable<ToPin> export type Pin = { path: string | CID recursive: boolean metadata?: any } export type PinEntry = { cid: CID type: PinType } export type PinType = | 'direct' | 'recursive' | 'indirect' export type RemoveOptions = { /** * Recursively unpin the object linked */ recursive?: boolean } export type AddOptions = { lock?: boolean } type ListOptions = { /** * CIDs or IPFS paths to search for in the pinset */ paths?: Array<string|CID> /** * Filter by this type of pin ("recursive", "direct" or "indirect") */ type?: PinType | 'all' }
-
Each IPFS component that can provide
@implements {API}
annotation that type
checker will ensure.Note: That does imply that implementer implements that exact API or it's
superset which enables deriviation acrossipfs-core
and
ipfs-http-client
like added options in the later.Below is illustration of how
ipfs-core
can use this on the samepin
component:/** * @implements {API} */ class PinAPI { /** * @param {Object} config * @param {GCLock} config.gcLock * @param {DagReader} config.dagReader * @param {PinManager} config.pinManager */ constructor ({ gcLock, dagReader, pinManager }) { // ... } } /** * @typedef {import('../../interface/pin').API} API */
This will work regardless if we choose to do Incidental Complexity: Dependency injection is getting in the way #3391 or a slight variation on
keep the current style used everywhere in chore: make IPFS API static (remove api-manager) #3365. If we keep
the current style above will look as:const createAddAll = require('./add-all') /** * @implements {API} */ class PinAPI { /** * @param {Object} config * @param {GCLock} config.gcLock * @param {DagReader} config.dagReader * @param {PinManager} config.pinManager */ constructor ({ gcLock, dagReader, pinManager }) { this.addAll = createAddAll({ gcLock, dagReader, pinManager }) // ... } } /** * @typedef {import('../../interface/pin').API} API */
Where
./add-all.js
looks like:/** * @param {Object} config * @param {GCLock} config.gcLock * @param {DagReader} config.dagReader * @param {PinManager} config.pinManager */ module.exports = ({ pinManager, gcLock, dagReader }) => { /** * Adds multiple IPFS objects to the pinset and also stores it to the IPFS * repo. pinset is the set of hashes currently pinned (not gc'able) * * @param {ToPins} source - One or more CIDs or IPFS Paths to pin in your repo * @param {AddOptions & AbortOptions} [options] * @returns {AsyncIterable<CID>} - CIDs that were pinned. */ async function * addAll (source, options = {}) { // ... } } /** * @typedef {import('../../interface/pin').API} API * @typedef {import('../../interface/pin').ToPins} ToPins * @typedef {import('../../interface/pin').AddOptions} AddOptions * @typedef {import('../../interface/util').AbortOptions} AbortOptions * ... */
Note that all the types are imported from
interface/*
.And if we choose to reduce to remove dependency incejection (as per Incidental Complexity: Dependency injection is getting in the way #3391) things work just as well (better actually)
/** * @implements {API} */ class PinAPI { /** * @param {Object} config * @param {GCLock} config.gcLock * @param {DagReader} config.dagReader * @param {PinManager} config.pinManager */ constructor ({ gcLock, dagReader, pinManager }) { // ... } /** * Adds multiple IPFS objects to the pinset and also stores it to the IPFS * repo. pinset is the set of hashes currently pinned (not gc'able) * * @param {ToPins} source - One or more CIDs or IPFS Paths to pin in your repo * @param {AddOptions & AbortOptions} [options] * @returns {AsyncIterable<CID>} - CIDs that were pinned. */ addAll(source, options) { return addAll(this.state, source, options) } // ... }
-
ipfs-http-client
will do more or less the same, with the
difference thatconfig
is whatever it is inipfs-http-client
:/** * @implements {API} */ class PinAPI { /** * @param {Object} config * .... */ constructor (config) { // ... } // ... } /** * @typedef {import('ipfs-core/src/interface/pin').API} API */
Here as well we can continue with our current approach of
dependency injections:const createAddAll = require('./add-all') /** * @implements {API} */ class PinAPI { /** * @param {Object} config * .... */ constructor (config) { this.addAll = createAddAll(config) // ... } // ... } /** * @typedef {import('ipfs-core/src/interface/pin').API} API */
Or move to dependency injection free approach:
const addAll = require('./add-all') /** * @implements {API} */ class PinAPI { /** * @param {Object} config * .... */ constructor (config) { // ... } /** * @param {ToPins} source * @param {AddOptions & ClientOptions} [options] */ addAll (source, options) { return addAll(this.state, source, options) } // ... } /** * @typedef {import('ipfs-core/src/interface/pin').API} API * @typedef {import(ipfs-core/src/interface/pin').ToPins} ToPins * @typedef {import('ipfs-core/src/interface/pin').AddOptions} AddOptions * @typedef {import('../../util').HTTOptions} ClientOptions */
Note: That implementation uses
ClientOptions
in place ofAbortOptions
(which it extends) while the class itself also implements pinAPI
that type checker will ensure.
Additional notes
- I believe @hugomrdias mentioned that in the future release of TS
@typedef {import('ipfs-core/src/interface/pin')} Pin
would gain namespace like behavior which will reduce amount of imports in the examples. - I expect that future improvements of TS to allow following patterns as well:
Which will basically provide namespace like behavior so we could just have a single import.
import * from "./pin" export pin // ...