
Research
/Security News
jscrambler npm Package Compromised in Supply Chain Attack
A compromised jscrambler npm release added a malicious preinstall hook that runs hidden native binaries on Linux, macOS, and Windows.
@autometa/overloaded
Advanced tools
Overloads as easy to make as they are to use.
export function myFunc(a: string): string;
export function myFunc(a: string, b: number): string;
export function myFunc(a: string, b: string): string;
export function myFunc(a: string, b: { a: string; b: number[] }): string;
export function myFunc(a: string, b: MyClassBuilder): MyClass;
export function myFunc(...args: unknown) {
return overloads(
// optional name 'a' // type of 'a' inferred as 'string'
def(string("a")).matches((a) => `hello ${a}`),
// simply declare each argument as you expect it. The first match will
// be executed
def(string("a"), number("b")).matches((a, b) => `hello ${a}`.repeat(b)),
// Verify complex (even nested) objects, arrays, tuples.
def(string("a"), shape({ a: string(), arr: array([number()]) })).matches(
(a, b) => b.arr.map((num) => `${num}: hello ${a}: ${b.a}`)
),
// Check for an instance of a class. Optionally provide a 'shape' as above
def(string("a"), instance("b", MyClassBuilder)).matches(
(name, myClassBuilder) => myClassBuilder.setName(name).build()
),
// validate actual values - useful for configurable, non overloaded functions
// or implementing the strategy pattern.
def(
string("a", { equals: "admin" }),
string("b", { in: ["buyer", "seller"] })
).matches((a) => `hello ${a}`),
// Throw an error if no matching overload, or define a fallback
fallback((...args: unknown[]) => {
// .. do some fallback stuff
})
).use(args);
}
npm add @autometa/overloaded
yarn add @autometa/overloaded
pnpm add @autometa/overloaded
Function and method overloads are an elegant way to expose multiple definition signatures for a function which help intellisense and display more clear intent about the purpose of your function for a given input.
Imagine a function with the following requirements
a and ba and b can be either a string or a number.a is a string then b must be a string.a is a number then b must be a number.a and b are string, return a tuple [a, b]a and b are numbers, return the sum of the numbers.This isn't a terribly useful function but that's okay.
The 'simplest' approach is to use discriminated unions (... aren't they all?).
function add(
a: string | number,
b: string | number
): [string, string] | number {
// ...
}
This is fine but unless the above requirements are documented somewhere the consumer will not be able to immediately understand how to use this function.
The solution is overloads. To make this function better for our consumer we can add two new signatures:
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(
a: string | number,
b: string | number
): [string, string] | number {
// ...
}
Now for our consumers this function makes a lot of sense. Once they provide a valid value for a, the
types b and return will automatically be inferred according to our requirements. We can even
document each overload separately.
/**
* blah blah blah
*/
function add(a: string, b: string): [string, string];
/**
* blah blah blah
*/
function add(a: number, b: number): number;
function add(
a: string | number,
b: string | number
): [string, string] | number {
// ...
}
While our function is nice to use, implementing it is a bit more grueling. We need to check our types and perform the correct behavior, also accounting for error states:
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(
a: string | number,
b: string | number
): [string, string] | number {
if (typeof a === "string") {
if (typeof b === "string") {
return [a, b];
} else {
throw new Error("a & b must both be strings");
}
}
if (typeof a === "number") {
if (typeof b === "number") {
return a + b;
} else {
throw new Error("a & b must both be numbers");
}
}
throw new Error("unknown types a & b must be string or number");
}
For just two variables with two primitive types, this is pretty rough.
We can imagine then how it might look if a and b can be completely different types from each other - including more complex types like objects and arrays which require even more validation.
At some point the the function parameter list becomes unmaintainable at the base level, which is easily resolved with a rest param.
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(a: [string, boolean], b: MyLibOpts): number;
function add(...args: unknown[]): [string, string] | number {}
Okay so how does Overloaded help?
Using overloaded to build your function and method overloads is simple.
Call the overloads function with your param overloads, then pass
the real arguments.
Overloaded functions are defined by the function pair def and it's child function match. Def accepts a rest param array of BaseArguments.
A BaseArgument can be created by it's corresponding function. So StringArgument has a string() factory function, while BooleanArgument
has the factory function boolean(). def returns an object
containing a match function. match accepts a function who's parameter
signature will be inferred from the preceding def.
I.E. for def(string(), string()), the match callback
will accept exactly 2 arguments which must be strings:
// ---------------------- V 'a' and 'b' both inferred as 'string'
def(string(), string()).match((a, b)=>}{})
// okay but redundant
def(string(), string()).match((a: string, b: string)=>}{});
// bad - ts error
def(string(), string()).match((a: string, b: number)=>}{});
Sticking with our original 2 overloads:
import { overloads, def, string, number } from "@autometa/overloaded";
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(
// 'unknown' instead of union is also fine here
...args: (string | number)[]
){
return overloads(
def(string(), string()).match((a, b) => [a, b])
def(number(), number()).match((a, b) => a + b)
// if using individual def pass an array [a, b] to `use`
).use(args);
}
And that's it. We now have an equivalent implementation as what we started with.
The return types for the base function definition will be inferred as a union
of the return types of each match. If no matching overload is found, an error will be thrown
highlighting each overload and provided context as to why it was not matched.
In the above example, the inferred return type is [string, string] | number
We saw Arguments in our above example. They are factory functions named approximately the same as their corresponding type.
Some like the above can be called with no additional arguments. Some require additional context. All argument factories can be named. It is suggested these names match the name in the corresponding overload. Rewriting the above example:
import { overloads, def, string, number } from "@autometa/overloaded";
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(
...args: (string | number)[]
// ...args: unknown[] // generalized alternative
){
return overloads(
def(string('a'), string('b')).match((a, b) => [a, b])
def(number('a'), number('b')).match((a, b) => a + b)
// if using individual def pass an array [a, b] to `use`
).use(args);
}
Note: It is not necessary for all overload parameters to share the same names. Likewise it is not necessary overloaded argument factories to share the same name across overloads.
The name is optional, and is used primarily for context when an error is thrown. If no name is provided, the arguments index in the parameter list will be used instead.
Assertions are an additional layer of overload matching which can be employed. Validations mean that Overloaded can have useful applications in non-overloaded functions also. Each type factory has its own set of validations which can be used. Using string here as an example:
import { admin, buyer, seller } from "./user-actions";
type UserTypes = "admin" | "buyer" | "seller";
function performUserAction(userType: UserTypes, data: unknown) {
return overloads(
def(string("userType", { equals: "admin" }), unknown("data")).match(
(_userType, data) => {
return admin.action(data);
}
),
def(string("userType", { equals: "buyer" }), unknown("data")).match(
(_userType, data) => {
return buyer.action(data);
}
),
def(string("userType", { equals: "seller" }), unknown("data")).match(
(_userType, data) => {
return seller.action(data);
}
)
).use([userType]);
}
Congratulations. You have implemented... an over-engineered switch statement!
However there are other checks like 'minLength', 'maxLength', 'startsWith' etc which can be composed together to simplify filtering logic for some domains. Each argument type has it's own assertions.
Assertions can of course also be used within an overloaded function or method to provide additional filtering.
Assertions are used for filtering, and do not through errors by themselves.
Note Some assertions are ignored if an invalid type is passed. For example, most string assertions will not be executed if a number or boolean or object is passed. In that case the assertion executed will be on the type itself.
If no overloads match the provided argument an Error will be thrown detailing why each overload failed to match.
Alternatively it is possible to provide a 'fallback' which
will be executed instead of an error being thrown. The fallback
will receive a rest param of arguments with types being unknown
A Fallback is defined with the fallback function.
import { overloads, def, string, number } from "@autometa/overloaded";
function add(a: string, b: string): [string, string];
function add(a: number, b: number): number;
function add(
...args: (string | number)[] // 'unknown' is a union is also fine here
) {
return overloads(
def(string(), string()).match((a, b) => [a, b]),
def(number(), number()).match((a, b) => a + b),
fallback((...args: unknown[]) => console.log(args))
).use(args);
}
declare class MyObject {
constructor(name: string | undefined, widgets: string[] | undefined);
}
function makeMyClass(name: string): MyObject;
function makeMyClass(widgets: string[]): MyObject;
function makeMyClass(name: string, widgets: string[]): MyObject;
function makeMyClass(...args: (string | string[])[]) {
return overloads(
// Create an instance with only a name
def(string("name")).matches((name) => new MyObject(name)),
// Create an instance with only its list of widgets
def(array("widgets", [string()])).matches(
(name, widgets) => new MyObject(undefined, widgets)
),
// Create an instance with both a name and its list of widgets
def(string("name"), array("widgets", [string()])).matches(
(name, widgets) => new MyObject(undefined, widgets)
),
// No match - Throw an error
fallback((...args) => {
throw new Error(
`A 'MyObject' instance requires either a name, a list of widgets or both. Received: ${args}`
);
})
).use(args);
}
Convert a plain javascript object to a DTO instance. Pretend
we have a plainToDto function which creates a new instance of a class
and assigns the values from a raw object to its instance properties
abstract class User {
name: string;
registered: Date;
}
class AdminUser extends User {
permissions: ("read" | "write" | "ban" | "unban" | "sticky")[];
}
class HobbyUser extends User {
interests: string[];
}
class PaidUser extends User {
interests: string[];
tier: number;
badges: number[];
}
// return type inferred as
// AdminUser | HobbyUser | PaidUser
// Throws an error if no match found.
function createUserDto(user: unknown) {
return overloads(
def(shape({ permissions: array([string()]) })).matches((user) => {
return plainToDto(AdminUser, user);
}),
def(shape({ interests: array([string()]) })).matches((user) => {
return plainToDto(HobbyUser, user);
}),
def(shape({ tier: number() })).matches((user) => {
return plainToDto(PaidUser, user);
})
).use([user]);
}
Overloads can take an optional description. Simply provide a string as the first parameter of an overload. This can be used to document the purpose of this particular overload, but won't get lost if things move around like a comment.
function myOverloadedFunction(...args: unknown[]) {
return overloads(
param(
"return a FooWidgetWrapper when the first argument is a FooWidget",
instance(FooWidget)
).matches((widget) => {
// ... do stuff
return return myNewFooWidgetWrapper;
})
).use(args);
}
String TemplatesPlease be aware the following is kind of cursed
It is possible to name an overload by providing a template string literal before the function call. This can be used in addition to or instead of the description.
Names let you produce overloads that vaguely resemble a function definition.
return overloads(
def`doThingA`(string(), number()).matches((a, b) => {
return a.repeat(b);
}),
def`doThingB`(string(), string()).matches((a, b) => {
return `${a} ${b}`;
})
);
// Using description
return overloads(
def`doThingA`(
"some additional context about doing thing A",
string(),
number()
).matches((a, b) => {
return a.repeat(b);
}),
def`doThingB`(
"some other context about doing thing B",
string(),
string()
).matches((a, b) => {
return `${a} ${b}`;
})
);
The "primitives", string, number and boolean are represented by factories
of matching name. Each accepts a name and assertion options.
Matches an argument in the same position which is a string.
def(string("a")).matches((a) => `Foo: ${a}`);
Assertions:
equals: Asserts that two strings are exactly equal
def(string("a", { equals: "user" })).matches((a) => UserFactory);
minLength: Asserts that any string passed will have at least n characters (inclusive), where n is the value passed to minLength
def(string("a", { minLength: 10 })).matches((a) => `Foo: ${a}`);
// name is optional
def(string({ minLength: 10 })).matches((a) => `Foo: ${a}`);
maxLength: Asserts that any string passed will have at most n characters (inclusive),
where n is the value passed to minLength
def(string("a", { maxLength: 10 })).matches((a) => `Foo: ${a}`);
includes: Asserts that any string passed includes some substring.
def(string("a", { includes: "users" })).matches((a) => usersFactory(a));
startsWith: Asserts that a string starts with a specific substring.
def(string("a", { startsWith: "users" })).matches((a) => mySettings[a]);
endsWith: Asserts that a string ends with a specific substring.
def(string("a", { endsWith: "users" })).matches((a) => mySettings[a]);
in: Asserts that a string is part of an array.
def(string("a", { in: ["group1", "group2"] })).matches((a) => this.getGroup(a));
Matches a parameter which of type number.
Assertions:
min: Asserts that a number has at least some minimum value (inclusive)
def(number("a", { min: 0 })).matches((a) => myArray[a]);
max: Asserts that a number has at most some maximum value (inclusive)
def(number("a", { max: 0 })).matches((a) => throw new Error(`'a' must be positive`));
in: Asserts that a number is part of an array.
def(string("a", { in: [101, 102] })).matches((a) =>
this.courses.enroll.math(a)
);
equals: Asserts that the provided number is exactly equal to the expected.
def(string("a", { equals: 101 })).matches((a) => DalmatianCoatFactory);
types: Asserts that the provided value is either an integer or a float value.
def(string("a", { type: "float" })).matches((a) => a * MY_CONST);
def(string("a", { type: "int" })).matches((a) => MY_ENUM[a]);
boolean assertions
Arrays and tuples are similar to each other, but arrays accept a list of possible type options with indeterminate length (unless asserted against), while a tuple is an array of fixed length with deterministic type options.
array
def(array([string(), number()])).match((a: (string | number)[]) => {
// ...
});
Assertions
tuple
def(tuple([string(), number()])).match((a: [string, number]) => {
// ...
});
Assertions
Shapes represent anonymous objects and class instances. It accepts
an object whose keys match the expected object and whose values
are Argument types. By default, only properties with keys defined in the shape
will be used to match.
If the real value passed to the overloaded function contains additional
keys, they will not be considered for validation, unless the exhaustive option is set to true.
def(shape({ a: string(), b: tuple([number(), boolean()]) })).match(
({ a, b }) => {
console.log(a);
console.log(b[0]);
console.log(b[1]);
}
);
Assertions
Functions have minimal validation, providing only a shape and an optional argument list length.
def(func<(a: string) => void>()).match((fn) => fn("hi"));
Assertions
Catch-all/wildcard argument with no typing. Will check if the value is defined, which can be overwritten.
def(string(), unknown(), number()).match((a: string, b: unknown, c: number) => {
// ...
});
Matches an instance of the Node Date class.
import { yesterdayDate } from "./my-date-utils";
def(date({ before: yesterdayDate() })).match((a: Date) => {
// ...
});
Assertions
Matches an argument which is an instance of a provided class (also referred to here as a blueprint). Optionally accepts a shape, which is used to validate the individual
properties of the instance.
def(instance(MyClass, shape({ name: string(), age: number() }))).match(
(a: MyClass) => {
// ...
}
);
Instance assertions can be defined in the shape argument.
FAQs
Function and method overloading made easy
The npm package @autometa/overloaded receives a total of 12 weekly downloads. As such, @autometa/overloaded popularity was classified as not popular.
We found that @autometa/overloaded demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A compromised jscrambler npm release added a malicious preinstall hook that runs hidden native binaries on Linux, macOS, and Windows.

Research
/Security News
A malicious .NET package is typosquatting the Braintree SDK to steal live payment card data, merchant API keys, and host secrets from production apps.

Security News
/Research
Compromised Injective SDK npm version 1.20.21 exfiltrates wallet private keys and mnemonics through fake telemetry functionality.