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.

    Install

    npm i frl-ts-utils

    DownloadsWeekly Downloads

    0

    Version

    2.2.9

    License

    MIT

    Unpacked Size

    493 kB

    Total Files

    309

    Last publish

    Collaborators

    • avatar