frl-ts-utils
TypeScript icon, indicating that this package has built-in type declarations

2.2.9 • Public • Published

FRL TypeScript utils

Build Status Coverage Status npm version Dependency status Dev Dependency Status License

This little project contains a few quality-of-life TypeScript utilities.

A. Installation

If you are using npm, then simply run the npm install frl-ts-utils CLI command to get the latest version.

If you are using yarn, then go with the yarn add frl-ts-utils command.

B. Types

C. Functions

  • makeRef<T> - creates a new Ref<T> object with the provided value.

  • toDeepReadonly<T> - casts an object of type T to type DeepReadonly<T>.

  • isDisposable - checks whether or not an object implements an IDisposable interface by having a method called dispose.

  • Assert - a namespace containing a few useful assertion functions. These assertion functions either pass and return the provided parameter (except for Assert.True and Assert.False functions) or throw an Error.

  • createIterable<T> - creates an Iterable<T> object from the provided iterator factory.

  • deepFreeze<T> - recursively deep freezes the provided object and its properties, and returns it as DeepReadonly<T>.

  • dynamicCast<T> - allows to safely cast an object to the specified type, otherwise returns null. Works for object and primitive types.

A few examples:

class Foo {}
class Bar extends Foo {}
 
const foo: Foo = new Bar();
// returns foo as a Bar object
const bar = dynamicCast(Bar, foo);
// returns null
const nullDate = dynamicCast(Date, foo);
// returns null as well
const nullStr = dynamicCast('string', foo);
 
const obj: any = 'string';
// returns obj as a string
const str = dynamicCast('string', obj);
// returns null
const nullNumber = dynamicCast('number', obj);
// returns null as well
const nullBar = dynamicCast(Bar, obj);

It works for generic types too, however the generic parameters won't be validated:

class Generic<T>
{
    public constructor(public value: T) {}
}
 
const foo = new Generic<number>(0);
 
// returns foo as a Generic<string> object
// note, how the result type has to specified explicitly
// otherwise, the resulting type would be Generic<unknown>
// which may, or may not be, something you want
const bar = dynamicCast<Generic<string>>(Generic, foo);
 
const str = 'foo';
// returns null
const nullStr = dynamicCast<Generic<Date>>(Generic, str);
  • isOfType<T> - allows to check, if an object is of specified type. Works for object and primitive types. This is very similar to the dynamicCast<T> function, however, instead of returning a cast object or null, it returns true or false instead, respectively.

  • extend<T> - creates a function extension.

  • instanceOfCast<T> - allows to safely cast an object to the specified type, otherwise returns null. Works for object types only.

An example:

class Foo {}
class Bar extends Foo {}
 
const foo: Foo = new Bar();
// returns foo as a Bar object
const bar = instanceOfCast(Bar, foo);
// returns null
const nullDate = instanceOfCast(Date, foo);

Similar to dynamicCast<T>, it works for generic types too:

class Generic<T>
{
    public constructor(public value: T) {}
}
 
const foo = new Generic<number>(0);
 
// returns foo as a Generic<string> object
// note, how the result type has to specified explicitly
// otherwise, the resulting type would be Generic<unknown>
// which may, or may not be, something you want
const bar = instanceOfCast<Generic<string>>(Generic, foo);
 
const date = new Date();
// returns null
const nullDate = instanceOfCast<Generic<boolean>>(Generic, date);
  • isInstanceOfType<T> - allows to check, if an object is of specified type. Works for object types only. This is very similar to the instanceOfCast<T> function, however, instead of returning a cast object or null, it returns true or false instead, respectively.

  • isDefined<T> - returns true, if an object is not null and not undefined, otherwise returns false.

  • isNull<T> - returns true, if an object is null, otherwise returns false.

  • isUndefined<T> - returns true, if an object is undefined, otherwise returns false.

  • primitiveCast<T> - allows to safely cast an object to the specified type, otherwise returns null. Works for primitive types only.

An example:

const obj: any = 'string';
// returns obj as a string
const str = primitiveCast('string', obj);
// returns null
const nullNumber = primitiveCast('number', obj);
  • isPrimitiveOfType<T> - allows to check, if an object is of specified type. Works for primitive types only. This is very similar to the primitiveCast<T> function, however, instead of returning a cast object or null, it returns true or false instead, respectively.

  • readonlyCast<T> - allows to force cast a Readonly<T> object to T type.
    Be careful while using this function, since Readonly objects are probably marked as readonly for a reason (or are frozen) and are not supposed to be mutated within the scope.

  • deepReadonlyCast<T> - allows to force cast a DeepReadonly<T> object to T type.
    Be careful while using this function, since DeepReadonly objects are probably marked as deep readonly for a reason (or are deep-frozen) and are not supposed to be mutated within the scope.

  • reinterpretCast<T> - allows to force cast an object to the specified type.
    Be very careful while using this function, because it allows you to cast any object to any type, which may cause the compilation process not to catch an obvious error.

An example:

class Bar {}
 
const bar = new Bar();
// this is a valid usage, however unfortunate it may be
const result = reinterpretCast<string>(bar);
result.trim(); // runtime error, instead of a compilation error
  • using<T> - performs an action on an IDisposable object, and disposes it right after.

  • usingAsync<T> - performs an asynchronous action on an IDisposable object, and disposes it right after.

  • wait - creates a promise that resolves after the specified amount of time (in milliseconds).

D. Classes & Interfaces

  • DeferredAction<TArgs> - represents an action that should be invoked after a specified amount of time has passed. This class allows e.g. to create a simple debouncing mechanism, since every new invocation request resets the timer.

An example:

// creates an action that executes after ~100ms
const deferred = new DeferredAction<string>({
    timeoutMs: 100,
    action: args => console.log('action invoked', args)
});
 
// invokes the action and starts the timeout
// after ~100ms, 'action invoked' 'foo' will be logged to the console...
deferred.invoke('foo');
 
// ... unless, the stop method is called before the timeout finishes, or...
deferred.stop();
 
// ... another invocation is called
deferred.invoke('bar');
  • IDisposable - represents a disposable object.

  • Flag<T> - represents a simple flag or switch object, whose value can be changed.

An example:

// creates a boolean flag with initial value set to false
const flag = new Flag<boolean>(false);
 
// update method allows to change the flag's value
flag.update(true);
 
// exchange also changes the flag's value, however, it also returns the old value
// here, the old variable will be equal to true
const old = flag.exchange(false); 
 
// current will be equal to false
const current = flag.value;
  • Lazy<T> - represents a lazily initialized object.

An example:

// creates a new lazy object
const lazy = new Lazy(() => 'foo');
 
// isCreated will be equal to false, since value hasn't been initialized yet
const isCreated = lazy.isValueCreated;
 
// calling value property's getter will invoke the provider and return 'foo'
// subsequent calls to value's getter will no longer call the provider,
// since the lazy object will cache its result
const value = lazy.value;
  • Mixin<T> - represents a mixin object that can be merged together with other objects.

  • RepeatedAction<TArgs> - represents a stoppable action that is continuously invoked at a specified interval. This class allows e.g. to create a simple polling mechanism, that stops after a desired result has been achieved.

An example:

let i = 0;
 
// creates an action that executes every ~100ms, while i < 100
const repeated = new RepeatedAction<string>({
    intervalMs: 100,
    action: args =>
    {
        console.log('action invoked', i++, args);
        // returning RepeatedActionResult.Done causes the action to stop
        // returning RepeatedActionResult.Continue causes the action to continue invoking its action on an interval
        return i >= 100 ? RepeatedActionResult.Done : RepeatedActionResult.Continue;
    }
});
 
// invokes the action and starts the interval
// every ~100ms, 'action invoked' i 'foo' will be logged to the console...
repeated.invoke('foo');
 
// ... unless, the stop method is called before the action invocation returns RepeatedActionResult.Done, or...
repeated.stop();
 
// ... another invocation is called
repeated.invoke('bar');
  • Rng - represents a pseudorandom number generator.

  • Semaphore - represents a semaphore variable, that limits concurrent access to an asynchronous block of code. There also exists a Mutex class, that acts as a simple lock.

  • SkippableAction<TArgs> - represents an asynchronous action that skips all intermediate invocations.

An example:

import { wait } from 'frl-ts-utils';
 
// creates a skippable action, that resolves after ~100ms
const skippable = new SkippableAction<string>(
    args => wait(100).then(() => console.log(args)));
 
// invokes the action and starts resolving it
skippable.invoke('foo');
 
// calling another invoke before the first invocation resolves causes the last invocation to be queued
// it will be invoked immediately after the first one resolves
skippable.invoke('bar');
 
// this call will cause the invoke('bar') call to be skipped
//  once the invoke('foo') finishes, the invoke('baz') will start resolving next
skippable.invoke('baz');
 
// current allows to fetch the promise, that is currently being resolved
// in this case, it will return the promise, that is a result of the invoke('foo') call
const promise = skippable.current();
  • StopWatch - represents a simple stopwatch object that allows to measure the passage of time. Its accurracy is somewhat limited, so unless you are ok with measurement errors of up to ~50ms, then this is not a tool for you.

E. Events

Contains event publishing and event subscription functionality.

Examples:

// creates a new event handler with no subscriptions
const handler = new EventHandler<string>();
 
// creates a new event subscription
handler.listen((sender, args) => console.log(sender, args));
 
// publishes an event with null sender and 'foo' argument
handler.publish(null, 'foo');
 
// disposing an event handler will automatically dispose all subscriptions
handler.dispose();

Operators

Event listeners are decorable with operators. By calling the decorate method, you can specify how to modify the listener's behavior.

Built-in operators are:

Example of operator application:

const handler = new EventHandler<string>();
 
// listen method returns a newly created event listener instance
const listener = handler.listen((sender, args) => console.log(sender, args));
 
// decorates the event listener with operators
// this particular decoration will cause the listener
// to skip first 3 event publications, and then
// only events with args being equal to either 'foo' or 'bar'
// will be sent further to the listener's delegate
listener.decorate(
    skip(3),
    filter((_, args) => ['foo', 'bar'].some(x => x === args));
 
// ignored by the listener, first skip
handler.publish(null, '1');
 
// ignored by the listener, second skip
handler.publish(null, '2');
 
// ignored by the listener, third skip
handler.publish(null, '3');
 
// caught by the listener
handler.publish(null, 'foo');
 
// ignored by the listener due to filtering operator
handler.publish(null, 'foobar');
 
// caught by the listener
handler.publish(null, 'bar');

It's also possible to define custom operators. The operator must be a function with the following signature:

function yourOperatorName<TArgs>(/* your operator params */): EventListenerOperator<TArgs>;

EventListenerOperator<TArgs> is a type representing a delegate, that returns an event delegate. It accepts two parameters:

  • next - a delegate, that calls the next operator, or the listener's delegate, if no other operators have been queued up.

  • listener - a reference to the event's listener. Can be used e.g. to automatically dispose the listener from inside the operator, based on some condition.

Let's define a custom operator, that simply logs the published event's sender and args to the console, along with the provided title via the operator's parameter and the amount of operator invocations:

function logToConsole<TArgs>(title: string): EventListenerOperator<TArgs>
{
    // listener parameter (the second one) is ignored in this case
    return next =>
    {
        // any operator state can be defined inside here
        // in this case, we will store the operator's invocation count
        let invocationCount = 0;
 
        // the operator's delegate definiton
        return (sender, args, event) =>
        {
            console.log(title);
            console.log('invocation count: ', ++invocationCount);
            console.log('sender: ', sender);
            console.log('args: ', args);
 
            // after performing our operator's actions (logging to the console)
            // we call the next delegate in the chain, with the same parameters
            next(sender, args, event);
        }
    };
}

And that's our operator! Let's apply it now to an event listener:

handler.listen((sender, args) => console.log(sender, args))
    .decorate(logToConsole('hello event!'));
  • MessageBroker - a generic collection of event handlers registered under user-defined names.

F. Logging

Contains logging functionality.

  • LogMessage - represents a logger message.

  • LogType - represents a logger message type.

  • ILogger - an interface representing a subscribable logger.

  • ILoggerListener - an interface representing a logger subscription.

  • Logger - an implementation of the ILogger interface.

Examples:

// creates a new logger with no listeners
const logger = new Logger();
 
// creates a new logger listener
logger.listen((message, timestamp) =>
    console.log(`[${message.type}${timestamp}]: ${message}`));
 
// logs a message
// there are also a few more specialized methods, that log a message
logger.log(LogMessage.Information('foo'));
 
// it's also possible to set the logger's level
logger.logLevel = LogType.Warning;
 
// this message won't be logged due to the current logger's log level
// being set to Warning or above
logger.logInformation('bar');
 
// disposing a logger will automatically dispose all listeners
logger.dispose();

G. Mapping

Contains a simple object mapping functionality.

  • IMapper - an interface representing an object mapper.

  • Mapper - an implementation of the IMapper interface. Additionall,y allows to add new mapping definitions.

Examples:

class Foo
{
    public constructor(public value: number) {}
}
 
class Bar
{
    public constructor(public value: string) {}
}
 
// creates a new mapper without any mapping definitions
const mapper = new Mapper();
 
// registers mapping from number to string
mapper.add('number', 'string', x => x.toString());
 
// registers mapping from string to number
mapper.add('string', 'number', x =>
{
    const result = Number(x);
    return isNaN(result) ? 0 : result;
});
 
// registers mapping from Foo to Bar
mapper.add(Foo, Bar, (x, m) =>
{
    // m is a reference to the mapper instance
    // it can be used to recursively map other objects
    // from within the mapping definition function
    const value = m.map('string', x.value);
    return new Bar(value);
});
 
// returns '15'
const numberToStringResult = mapper.map('string', 15);
 
// returns 8
const stringToNumberResult = mapper.map('number', '8');
 
// returns new Bar instance with value equal to '11'
const fooToBarResult = mapper.map(Bar, new Foo(11));
 
// throws an error, since mapping from Bar to Foo is undefined
const barToFooResult = mapper.map(Foo, new Bar('1'));
 
// it's also possible to define mappings between primitive types and class types
// registers mapping from number to Foo
mapper.add('number', Foo, x => new Foo(x));
 
// registers mapping from Foo to number
mapper.add(Foo, 'number', x => x.value);
 
// returns new Foo instance with value equal to 7
const numberToFooResult = mapper.map(Foo, 7);
 
// returns 6
const fooToNumberResult = mapper.map('number', new Foo(6));

In addition to the map method, the IMapper contains some other helpful mapping methods: mapNullable, mapUndefinable, mapOptional and mapRange. The first 3 perform mapping conditionally, only when the source object is not null/undefined (depending on the used method). mapRange allows to map a collection of objects to another collection.

mapRange examples:

// let's use the mapper from the previous example
 
const barCollection: Bar[] = [
    new Bar('1'),
    new Bar('2'),
    new Bar('3')
];
 
// returns an array with 3 new Foo instances
// first Foo instance value is equal to 1
// second Foo instance value is equal to 2
// third Foo instance value is equal to 3
const barToFooRangeResult = mapper.mapRange(Foo, barCollection);
 
// the source collection doesn't have to contain objects of the same type
// as long as all of its elements are mappable to the destination type
// if at least one element is not mappable, then the mapRange method will throw
const mixedCollection = [
    new Bar('4'),
    5,
    new Bar('6')
];
 
// returns an array with 3 new Foo instances
// first Foo instance value is equal to 4
// second Foo instance value is equal to 5
// third Foo instance value is equal to 6
const mixedToFooRangeResult = mapper.mapRange(Foo, mixedCollection);

H. Tasks

Contains an asychronous, cancellable task functionality.

Examples:

// creates a new promise-based task instance
// in Created state
const task = new Task<string>(() => Promise.resolve('foo'));
 
// alternatively:
// task = Task.FromResult('foo');
 
// executes the task, which changes its state to Running
// returns the task's result of type TaskResult<string>
const result = await task.execute();
 
// since the task executed without any errors,
// it will be in the Completed state
 
// value will be equal to 'foo'
const value = result.value;
 
// create a task, that throws an error during its execution
const errorTask = new Task<string>(() => Promise.reject(new Error()));
 
// alternatively:
// task = Task.FromError<string>(new Error());
 
// since the task throws an error,
// its state will be changed to Faulted
const result = await task.execute();
 
// error will be equal to the Error instance provided
// to the Promise.reject function
const error = result.error;

There exists a static instance of a completed task, which can be useful in certain situations:

const completedTask = Task.COMPLETED;

Tasks can also be cancelled by dedicated cancellation tokens, like so:

// creates a new cancellation token, that isn't cancelled yet
const cancellationToken = new TaskCancellationToken();
 
const task = new Task<string>(async () =>
{
    // simulate a long-running task
    for (let i = 0; i < 100; ++i)
    {
        // checks if the cancellation token has been cancelled
        // and, in that case, throws an error
        cancellationToken.throwIfCancellationRequested();
        await wait(100);
    }
    return 'foo';
});
 
// cancels the token with an optional reason
cancellationToken.cancel('cancellation reason');
 
// since the task is cancelled via a token,
// its state will be changed to Cancelled
const result = await task.execute();
 
// error will be of type TaskCancellationError
const error = result.error;
 
// it's also possible to cancel the token after a specified amount of time (in ms)
cancellationToken.cancelAfter(1000);

Another important functionality of ITask<T> is the possibility to continue it with another task. This can be achieved by calling the ITask.then method, like so:

// first task
const task = Task.FromResult('foo');
 
// continuation task
// the result parameter represents the first task's result
// which can be used to create the follow-up task
const continuationTask = task.then(result =>
    Task.FromResult([result.value, 'bar']));
 
const fullResult = await continuationTask.execute();
 
// value will be an array of strings, containing two elements: 'foo' and 'bar'
const value = fullResult.value;

ITask.then method has an optional second parameter of type TaskContinuationStrategy. It specifies, in which scenarios to continue the first task, based on its state. By default, all task states are continued.

If a continuation task is not invoked due to the continuation strategy, then its state will be changed to Discontinued.

Another useful ITask methods are:

  • join - runs mutliple tasks concurrently and returns a new task, that resolves after all tasks have been resolved (such a joined task can also be created by calling the Task.All function).
  • race - runs multiple tasks concurrently and returns a new task, that resolves after any task has been resolved (such a race task can also be created by calling the Task.Any function).
  • map - allows to map task's result to another type. It will only be executed for task's, that complete successfuly.

I. Collections

Contains a few useful collections and data structures, as well as some collection manipulation algorithms.

Dependents (3)

Package Sidebar

Install

npm i frl-ts-utils

Weekly Downloads

1

Version

2.2.9

License

MIT

Unpacked Size

493 kB

Total Files

309

Last publish

Collaborators

  • calionvarduk