# ts-lib-extended
> Additional types and tools for typescript

[![npm version](https://badge.fury.io/js/ts-lib-extended.svg)](https://badge.fury.io/js/ts-lib-extended)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![npm downloads](https://badgen.net/npm/dw/ts-lib-extended)](https://badge.fury.io/js/ts-lib-extended)

## Features
- Enum type (key and value extraction - ignores reveres mapping)
- Dictionary types (safe, readonly, key + value types)
- Constructor types (abstract, standard, parameter + instance types)
- Core class for disposable instances
- Events (handler, args, cancellation, external subscription + internal invocation)
- Array types (minimal length array, item type)
- Enforce Empty Object type (only allows the assignment of empty objects)
- Scoped instances (create tree structures)
- Type mappings

## Installation
```bash
npm i ts-lib-extended
```

## Events
With events it is possible to subscribe to a specific action or change on an instance.
This is inspired by C# and should work in a similar way.

```ts
import { Event, EventArgs, Disposable, EventHandler } from 'ts-lib-extended':

export class Example<T> extends Disposable {
  private _valueChangedHandler: EventHandler<this, EventArgs<T>>;

  constructor(
    private _value: T
  ) {
    super();
    // create handler that provides the subscribable event
    this._valueChangedHandler = new EventHandler();
  }

  public get value(): T { return this._value; }
  public set value(value_: T) {
    this._value = value_;
    this._valueChangedHandler.invoke(this, new EventArgs(value_));
  }

  public get valueChanged(): Event<this, EventArgs<T>> { return this._valueChangedHandler.event; }

  protected disposingInstance(): void {
    super.disposingInstance();
    // resolve references to listeners
    this._valueChangedHandler.dispose();
  }
}

// simulates a function that is somewhere in the code and manipulates the instance/caller
function changeValue(incoming_: Example<number>) {
  incoming_.value = 42 + incoming_.value;
}

// create instance/caller
const example = new Example(42);
// unique subscription id
const identifier = 'id for my subscription';

example.valueChanged.subscribe(
  identifier,
  (sender_, args_) => {
    // --> sender/invoker: instance of example class
    console.log(sender_);
    // --> submitted value --> 84
    console.log(args_.value);
    // will unsubscribe from the event (just an example use case)
    sender_.valueChanged.unsubscribe(identifier);
  }
);

// execute code that modifies the subscribed instance
changeValue(example);
```

### Cancelable Events

Actions can be canceled via events.

```ts
import { Event, CancelEventArgs, Disposable, EventHandler } from 'ts-lib-extended':

export class Example extends Disposable {
  private _loggingHandler: EventHandler<this, CancelEventArgs<string>>;

  constructor() {
    super();
    this._loggingHandler = new EventHandler();
  }

  public log(message_: string): void {
    const cancelationArgs = new CancelEventArgs<string>(message_);
    this._loggingHandler.invoke(this, cancelationArgs);

    // will not set value if canceled by event
    if (cancelationArgs.cancel) {
      return;
    }

    console.log(message_);
  }

  public get logging(): Event<this, CancelEventArgs<string>> { return this._loggingHandler.event; }

  protected disposingInstance(): void {
    super.disposingInstance();
    this._loggingHandler.dispose();
  }
}

function changeValue(incoming_: Example) {
  incoming_.log('my log message');
}

const example = new Example();
example.logging.subscribe('39fgfhuf85j', (_sender_, args_) => {
  // cancel if message is specific value
  args_.cancel = args_.value === 'my log message';
});

changeValue(example);
```

### Disposable

A disposable instance can be cleaned so that references to other instances can be released. After disposing the instance is partly "dead", some parts are vanished and not longer usable.

```ts
import { Disposable } from 'ts-lib-extended';

class Example extends Disposable {
  constructor(
    private _parentReference: any[]
  ) {
    super();
    this._disposers.push(() => this.releaseReferences());
  }

  private releaseReferences() {
    this._parentReference.splice(0);
  }
}

const instances = [{}];
const example = new Example(instances);
console.log(example.isDisposed) // false
example.dispose();
console.log(example.isDisposed) // true
```

This example uses the `disposers` feature for disposing internal stuff. You can also use `overrides` (disposingInstance, disposedInstance) to get this job done.

## Safe dictionary

```ts
import { Dictionary } from 'ts-lib-extended';

const dictionary: Dictionary<number> = {
  spaceballs: 42
};

const answer = dictionary.spaceballs;

if (answer) {
  console.log(answer);
}
```

## Minimal length array

The type `MinArray` can restrict array values to a minimum length

```ts
import { MinArray } from 'ts-lib-extended';

function calcSum(...array_: MinArray<number, 2>): number {
  let sum = 0;

  for (let i = 0; i < array_.length; i++) {
    sum += array_[i];
  }

  return sum;
}

console.log(calcSum(1)); // TS Error - at least 2 arguments are expected
console.log(calcSum(1,2,3,5,8,13)); // 32
```

## Enumerable

By default there is no basic (accessible) enum type that can be used to specify variable/param types. `Enumerable` solves the problem.

```ts
enum MyEnum {
  tony = 'iron man',
  steve = 'cap',
  peter = 'spider-man',
  bruce = 'hulk'
}

enum NumberEnum {
  tony,
  steve,
  peter,
  bruce
}

function doSomethingWithEnum(enum_: Enumerable): void {
  /** crazy code here */
}

doSomethingWithEnum(NumberEnum);
doSomethingWithEnum(MyEnum);
```

### Gain keys and values

Gaining keys and/or values from an enum is tricky. Object.keys(), Object.values() and Object.entries() do not correctly consider the numeric index reverse lookup entries for numeric enums. The `enumarableObject` will solve this issue.

```ts
import { enumarableObject } from 'ts-lib-extended';

enum NumberEnum {
  e1,
  e2
}

console.log(Object.keys(NumberEnum)) // ["0", "1", "e1", "e2"]
console.log(enumarableObject.keys(NumberEnum)) // ["e1", "e2"]

console.log(Object.values(NumberEnum)) // ["e1", "e2", 0, 1]
console.log(enumarableObject.values(NumberEnum)) // [0, 1]

console.log(Object.entries(NumberEnum)) // [["0", "e1"], ["1", "e2"], ["e1", 0], ["e2", 1]]
console.log(enumarableObject.entries(NumberEnum)) // [["e1", 0], ["e2", 1]]
```

## "Empty Object" type

Sometimes you need a `this object is empty`-type (e.g. as a default assignment for generics). Unfortunately, this cannot be achieved with `{}` ([detailed explanation](https://mercury.com/blog/creating-an-emptyobject-type-in-typescript)).

```ts
import type { EmptyObject } from 'ts-lib-extended';

type CustomParameters = Record<string, any>;

abstract class Special<T extends CustomParameters = EmptyObject> {
    public abstract doSomething(params_?: T): void;
}

type ValueParameters = { value: string };

class SpecialWithParams<T extends ValueParameters> extends Special<T> {
    public doSomething(params_?: T): void {
        /* do something */
    }
}

class SpecialWithoutParams extends Special {
    public doSomething(params_?: EmptyObject): void {
        /* without the generic type "params_" is unusable (and can be omitted) */
    }
}
```

## Scoping

Extend your classes from ScopedInstanceCore to get quick access to scopes. Scopes allow you to create a tree structure within your class instance. Variants can be used to create custom instances per scope.

```ts
import {
  InstanceScopeCore,
  ScopedInstanceCore,
  type InstanceScope
} from 'ts-lib-extended';

type MyScopeVariants = 'dark' | 'light';

class MyClass extends ScopedInstanceCore<MyClassScope> {
  constructor(public readonly user?: string) {
    super();
  }

  protected disposeScope(scope_: MyClassScope): void {
    scope_.dispose();
  }

  protected createScope(scopeId_: PropertyKey): MyClassScope {
    return new MyClassScope(scopeId_);
  }
}

class MyClassScope
  extends InstanceScopeCore<MyClass, MyScopeVariants>
  implements InstanceScope<MyClass, MyScopeVariants>
{
  public get dark(): MyClass {
    return this.getOrCreateInstance('dark');
  }

  public get light(): MyClass {
    return this.getOrCreateInstance('light');
  }

  protected createInstance(variant_: MyScopeVariants): MyClass {
    let user: string;

    console.log(this.scopeId, variant_);

    if (variant_ === 'dark') {
      if (this.scopeId === 'starwars') {
        user = 'Anakin Skywalker';
      } else {
        user = 'Riku';
      }
    } else {
      if (this.scopeId === 'starwars') {
        user = 'Luke Skywalker';
      } else {
        user = 'Sora';
      }
    }

    return new MyClass(user);
  }

  protected disposeInstance(instance_: MyClass): void {
    instance_.dispose();
  }
}

const mc = new MyClass();
const starwarsScope = mc.scope('starwars');
starwarsScope.dark.user; // => Anakin Skywalker
starwarsScope.light.user; // => Luke Skywalker

const kingdomheartsScope = mc.scope('kingdomhearts'); // or any other scope id
kingdomheartsScope.dark.user; // => Riku
kingdomheartsScope.light.user; // => Sora
```

## Type mapping

### Deep partial

Typescript's `Partial<T>` type, but recursive.

```ts
import { type DeepPartial } from 'ts-lib-extended';

type Something = {
  a: {
    b: string;
  };
  c: string;
  d: { e: string; }[];
}

type MaybeSomething = DeepPartial<Something>;
/*
{
  a?: {
    b?: string | undefined;
  } | undefined;
  c?: string | undefined;
  d?: ({
    e?: string | undefined;
  } | undefined)[] | undefined;
}
*/
```

### Deep required

Typescript's `Required<T>` type, but recursive.

```ts
import { type DeepRequired } from 'ts-lib-extended';

type MaybeSomething = {
  a?: {
    b?: string;
  };
  c: string;
  d: { e?: string; }[];
}

type Something = DeepRequired<MaybeSomething>;
/*
{
  a: {
    b: string;
  };
  c: string;
  d: {
    e: string;
  }[];
}
*/
```
