Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 classes and Advanced Typings #13

Open
wants to merge 63 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
c277c8b
new implementation and advanced typescript
pabadm Sep 22, 2023
e31439c
new implementation and advanced typescript
pabadm Sep 22, 2023
45e2900
exported types
pabadm Sep 22, 2023
c7e3ab8
added cjs and mjs support
pabadm Sep 25, 2023
bd47956
removed comments from build
pabadm Sep 25, 2023
704ec37
nojs.yml and support for both ES6 modules and CJS
pabadm Sep 28, 2023
8eb3da2
typescrip tests
pabadm Oct 25, 2023
56f3c40
typo override (meant overload) fix
pabadm Oct 25, 2023
8200e94
useless test fix
pabadm Oct 25, 2023
49fd9b1
removed CALL export
pabadm Oct 25, 2023
f735d41
removed CALL export from d.ts
pabadm Oct 25, 2023
e5d90dc
Update README.md
pabadm Oct 25, 2023
7856502
Update README.md
pabadm Oct 25, 2023
f6d3ccb
Update README.md
pabadm Oct 25, 2023
af1775f
Update README.md
pabadm Oct 25, 2023
bc246ca
added test for function with generic
pabadm Oct 25, 2023
d9c20eb
Update README.md
pabadm Oct 25, 2023
fe8d724
Update README.md
pabadm Oct 26, 2023
c96f690
Update README.md
pabadm Oct 26, 2023
e9996bc
Update README.md
pabadm Oct 26, 2023
a1cd346
Update README.md
pabadm Oct 26, 2023
da39743
Update README.md
pabadm Oct 26, 2023
50b89aa
Update README.md
pabadm Oct 26, 2023
8d05297
Update README.md
pabadm Oct 26, 2023
c7f86c5
Update README.md
pabadm Oct 26, 2023
26a5acd
Update README.md
pabadm Oct 26, 2023
354bb22
Update README.md
pabadm Oct 26, 2023
6b0475c
Update README.md
pabadm Oct 26, 2023
110f520
added new way to create callable object
pabadm Oct 27, 2023
f1df7d2
Merge branch 'ES6-Syntax' of https://github.com/pabadm/node-callable-…
pabadm Oct 27, 2023
584213d
Update README.md
pabadm Oct 26, 2023
91dea6c
Update README.md
pabadm Oct 27, 2023
fef2240
Update README.md
pabadm Oct 27, 2023
f404d78
Update README.md
pabadm Oct 27, 2023
34618bf
Update README.md
pabadm Oct 27, 2023
d6727b9
created aliases to bind,call,apply
pabadm Oct 27, 2023
07a5e1c
Update README.md
pabadm Oct 27, 2023
ffc1dc4
fixed double bind bug
pabadm Oct 27, 2023
3fa75e0
fixed call and apply bindings
pabadm Oct 27, 2023
95f1d95
callableBind now accepts multiple args like function.bind
pabadm Oct 27, 2023
a104b6d
callableApply, callableCall binding fix
pabadm Oct 27, 2023
afe85fa
apply, call, bind logic moved to prototype
pabadm Oct 28, 2023
7512984
symbols description edit + methods ...args
pabadm Oct 30, 2023
0360843
reimplementation due to CSP
pabadm Dec 3, 2023
d4deb25
properties map optimisation
pabadm Dec 3, 2023
c4c1624
code simplification (this.constructor => new.target)
pabadm Dec 6, 2023
b67c21f
edited package.json 'files'
pabadm Dec 6, 2023
fe245ef
Update README.md
pabadm Dec 6, 2023
30b3a3e
test fixes
pabadm Dec 6, 2023
8b47161
inlined some code
pabadm Dec 7, 2023
4cc3178
Merge branch 'ES6-Syntax' of https://github.com/pabadm/node-callable-…
pabadm Dec 7, 2023
c2bb931
TS: better function type detection
pabadm Dec 7, 2023
b7639de
inlined more
pabadm Dec 7, 2023
bada7a9
sideEffects: false
pabadm Dec 16, 2023
eb09e33
Update README.md
pabadm Feb 3, 2024
4741517
Callable.makeCallable => Callable.from + improved typings
pabadm Feb 3, 2024
7b63b84
deleted .idea
pabadm Feb 3, 2024
1951f38
test rename
pabadm Feb 3, 2024
1599dcd
readme code formatting
pabadm Feb 3, 2024
8e07330
improved typings
pabadm Feb 4, 2024
1ed72ef
Merge branch 'ES6-Syntax' of https://github.com/pabadm/node-callable-…
pabadm Feb 4, 2024
831fa32
Fake key in Callable type to distinguish Callable and Function type
pabadm Feb 4, 2024
ed14d23
removed prototype field from CloneFuncFromClass
pabadm Feb 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 19.x]
node-version: [16.x, 18.x, 19.x, 20.x, 21.x, 22.x, 23.x, 24.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ jspm_packages
# Optional REPL history
.node_repl_history

/dist
# dist
dist
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this was a meaningful change, either remove it or remove the useless comment.

170 changes: 141 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://img.shields.io/github/actions/workflow/status/CGamesPlay/node-callable-instance/node.js.yml?branch=master)](https://github.com/CGamesPlay/node-callable-instance/actions/workflows/node.js.yml) [![Download Size](https://img.shields.io/bundlephobia/min/callable-instance.svg?style=flat)](https://bundlephobia.com/package/callable-instance@latest) [![dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](https://www.npmjs.com/package/callable-instance?activeTab=dependencies) [![npm](https://img.shields.io/npm/v/callable-instance)](https://www.npmjs.com/package/callable-instance) [![npm](https://img.shields.io/npm/dw/callable-instance)](https://www.npmjs.com/package/callable-instance)

This module allows you to create an ES6 class that is callable as a function. The invocation is sent to one of the object's normal prototype methods.
This module allows you to create an ES6 `class` or regular JS `Object` that is callable as a function.

## Installation

Expand All @@ -15,65 +15,177 @@ npm install callable-instance
In the following example, we will create an `ExampleClass` class. The instances have all of the normal properties and methods, but are actually functions as well.

```javascript
import CallableInstance from "callable-instance";
import Callable from "callable-instance";
// If you aren't using ES modules, you can use require:
// var CallableInstance = require("callable-instance");
// var Callable = require("callable-instance");

class ExampleClass extends CallableInstance {
class ExampleClass extends Callable {
constructor() {
// CallableInstance accepts the name of the property to use as the callable
// method.
super("instanceMethod");
super();
}

instanceMethod() {
console.log("instanceMethod called!");
[Callable.CALL](arg) {
return arg
}
}

var test = new ExampleClass();
// Invoke the method normally
test.instanceMethod();
// Call the instance itself, redirects to instanceMethod
test[Callable.CALL]();
// Call the instance itself, redirects to [Callable.CALL]
test();
// The instance is actually a closure bound to itself and can be used like a
// normal function.
test.apply(null, [1, 2, 3]);
```
> **_NOTE:_** Usage of custom method name is also supported.

```javascript
class ExampleClassWithCustomMethodName extends Callable {
constructor(){
// in super provide object method`s name which will be used when calling object
super('myMethod')
}
myMethod(arg){
return arg
}
}
```

### Other Usage Variant
In the next example, we will create `callableObject` using Callable.makeCallable.

```javascript
import Callable from "callable-instance";
// makeCallable creates new callable object with all the properties from source object
const callableObject = Callable.makeCallable({
test: "test",
[Callable.CALL]() {
return this.test;
},
});

// cloning using spread operator + makeCallable. (spread looses call signature so it is important to call makeCallable again)
const cloned = Callable.makeCallable({...callableObject});

// cloning using Callable.clone (accepts only direct instance of Callable. e.g. made with makeCallable)
const cloned2 = Callable.clone(callableObject);
```
> **_NOTE:_** Usage of custom method name is also supported.
```javascript
import Callable from "callable-instance";
const callableObject = Callable.makeCallable({
test: "test",
getTest() {
return this.test;
},
}, "getTest"); // second parameter is optional method`s name which will be used when calling object

// but it is important to also provide method name when cloning using spread + makeCallable
const cloned = Callable.makeCallable({...callableObject}, "getTest");

// Callable.clone does not have this issue
const cloned2 = Callable.clone(callableObject);
```

## Typescript


`Callable` has full typescript support.

1. **Using interface**

```typescript
// Callable has 2 generics
// 1st is for class | interface | function (for extracting type of call signature)
// 2nd optional generic is for method name (string | symbol | number) which will be used as type of call signature from 1st generic (defaults to Callable.CALL)

interface IExampleClass {
// interface type will provide actual type of the function without limits
[Callable.CALL](arg: string): string
}
// implements is optional but advised https://www.typescriptlang.org/docs/handbook/interfaces.html
class ExampleClass extends Callable<IExampleClass> implements IExampleClass {
constructor(){
super()
}
[Callable.CALL](arg: string){
return arg
}
}
```

TypeScript is also supported. `CallableInstance` is generic, accepting a tuple of arguments and a return type.
2. **Using function type**
```typescript
class ExampleClass extends Callable<(arg: string) => string> {
constructor(){
super()
}
[Callable.CALL](arg: string){
return arg
}
}
```

3. **Using class type**
```typescript
import CallableInstance from "callable-instance";
// easiest way for typing Callable
class ExampleClass extends Callable<typeof ExampleClass> {
constructor(){
super()
}
[Callable.CALL](arg: string){
return arg
}
}
```
> **_NOTE:_** For function overload or generics use Interface or Function variant.

### **Override Call**

class ExampleClass extends CallableInstance<[number], string> {
Due to typescript limitations module also provides OverrideCall type.
It can be used to override call signature in child classes.

```typescript
// Override call has 3 generics but must be written only in one way
// class Child extends (Parent as OverrideCall<typeof Parent>)<Child, propertyName>
// 1st generic is Parent
// 2nd generic is Child. Can be interface | class | function
// 3rd optional generic is propertyName can be string | symbol | number. defaults to Callable.CALL

// call signature is (() => string)
class ExampleClass extends Callable<() => string> {
constructor() {
super("instanceMethod");
super();
}

instanceMethod(input: number): string {
return `${input}`;
[Callable.CALL]() {
return "test";
}
}
```

Note that the types specified may differ from the argument and return value types of the target method; this is an error due to a limitation of TypeScript.
// overriding call signature to (() => number)
class ExampleClassChild extends (ExampleClass as OverrideCall<typeof ExampleClass>)<() => number> {
constructor(){
super();
}

[Callable.CALL](){
return 100
}
}
```

### Inherited Properties
## Inherited Properties

All instances of CallableMethod are also an instances of [Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function), and have all of Function's properties.

Libraries that accept functions will expect that they behave as Function objects do. For example, if you alter the semantics of the `call` or `apply` methods, library code may fail to work with your callable instance. In these cases, you can simply bind the instance method to the callable instance and pass that instead (e.g. `test.instanceMethod.bind(test)`).
`Callable` behaves exactly like the method specified in constructor but they are not equal. `Callable` just creates an alias to the function. It's `bind`, `apply`, `call` methods behaves exactly like used function's methods. So it is safe to use `call`, `apply` or `bind` on `Callable` directly. `test.call(test2, ...args)` will return `test.instanceMethod.call(test2, ...args)`.

This can also cause problems if your derived class wants to have a `name` or `length` property, which are built-in properties and not configurable by default. You can have your class disable the built-in descriptors of these properties to make them available for your use.
Libraries that accept functions will expect that they behave as Function objects do. For example, if you alter the semantics of the call or apply methods, library code may fail to work with your callable instance. This can also cause problems if your derived class wants to have a `name` or `length` property, which are built-in properties and not configurable by default. You can have your class disable the built-in descriptors of these properties to make them available for your use.

```javascript
var test = new ExampleClass();
console.log(test.name); // Will print 'ExampleClass' (constructor.name is used by default)
test.name = "hello!";
console.log(test.name); // Will print 'instanceMethod'
console.log(test.name); // Will print 'ExampleClass'

class NameableClass extends CallableInstance {
class NameableClass extends Callable {
constructor() {
super("instanceMethod");
Object.defineProperty(this, "name", {
Expand Down
8 changes: 0 additions & 8 deletions index.d.ts

This file was deleted.

14 changes: 0 additions & 14 deletions index.js

This file was deleted.

78 changes: 78 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
declare module "callable-instance" {
const CALL: unique symbol;
export type SCALL = typeof CALL;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should remove this type alias. It's not saving much and it adds an extra concept for people to be confused by. In this file we can use typeof CALL directly, and any consumer can use typeof Callable.CALL.


type BaseProperty = symbol | string | number;
type CustomProperty = Exclude<BaseProperty, SCALL>;

type BaseFunc = (...args: any) => any;
type BaseClass = abstract new (...args: any) => any;
type BaseInterface = {
[k: BaseProperty]: any;
};

type ExtractFuncFromInterface<
I extends BaseInterface,
P extends BaseProperty
> = I[P] extends BaseFunc ? I[P] : never;

interface CloneFuncFromClass<C extends BaseClass, P extends BaseProperty> {
/**
* For TS generics and function overload support use interface or function type for Callable
*/
(...args: Parameters<InstanceType<C>[P]>): ReturnType<InstanceType<C>[P]>;
}
type ExtractFunc<
C extends BaseClass | BaseFunc | BaseInterface,
P extends BaseProperty
> = C extends BaseClass
? CloneFuncFromClass<C, P>
: C extends BaseFunc
? C
: C extends BaseInterface
? ExtractFuncFromInterface<C, P>
: never;

export interface CallableConstructor {
get CALL(): SCALL;

makeCallable<I extends BaseInterface, P extends CustomProperty>(
object: I,
property: P
): PickProperties<I> & ExtractFuncFromInterface<I, P>;
makeCallable<I extends BaseInterface>(
object: I
): PickProperties<I> & ExtractFuncFromInterface<I, SCALL>;

clone<C extends BaseInterface & BaseFunc>(callableObject: C): C;

new <
C extends BaseClass | BaseFunc | BaseInterface,
P extends CustomProperty
>(
property: P
): ExtractFunc<C, P>;

new <C extends BaseClass | BaseFunc | BaseInterface>(): ExtractFunc<
C,
SCALL
>;
}

type PickProperties<Obj extends Record<BaseProperty, unknown>> = {
[k in keyof Obj]: Obj[k];
};

export type OverrideCall<S extends BaseClass> = {
new <
C extends BaseClass | BaseFunc | BaseInterface,
P extends BaseProperty = SCALL
>(
...args: ConstructorParameters<S>
): Omit<PickProperties<InstanceType<S>>, P> & ExtractFunc<C, P>;
} & S;

const Callable: CallableConstructor;

export default Callable;
}
Loading
Loading