Skip to content

Commit

Permalink
API enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
yishn committed Mar 22, 2023
1 parent 6125275 commit 08d6591
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 595 deletions.
196 changes: 48 additions & 148 deletions README.md
Expand Up @@ -12,14 +12,13 @@ For optimal type safety, use this library in TypeScript's strict mode.
- [Match Data](#match-data)
- [Generic Enum Types](#generic-enum-types)
- [Mutate Enum Variant](#mutate-enum-variant)
- [Enum Classes](#enum-classes)

### Installation

Using with Deno is as simple as adding an import to your code:

```ts
import {/* ... */} from "https://deno.land/x/algebraic_enum/src/mod.ts";
import /* ... */ "https://deno.land/x/algebraic_enum/src/mod.ts";
```
For Node.js, you can install with npm:
Expand All @@ -39,17 +38,17 @@ Note that variant names cannot be `_` as it is reserved.
```ts
import { Enum } from "https://deno.land/x/algebraic_enum/src/mod.ts";

const StatusVariants = {
Success: null,
Failure: null,
Pending: null,
};
class StatusVariants {
Success = null;
Failure = null;
Pending = null;
}

type Status = Enum<typeof StatusVariants>;
type Status = Enum<StatusVariants>;

let status: Status = { Success: null };
const status: Status = { Success: null };

let invalidStatus: Status = { Success: null, Failure: null };
const invalidStatus: Status = { Success: null, Failure: null };
// Compilation error, as `Enum` can only contain exactly one variant
```

Expand All @@ -68,50 +67,47 @@ support.

```ts
// Simplified version of Enum:
type Status =
| { Success: null }
| { Failure: null }
| { Pending: null };
type Status = { Success: null } | { Failure: null } | { Pending: null };
```

For easier enum value construction, you can use the `Enum.factory` function:

```ts
type Status = Enum<typeof StatusVariants>;
const Status = Enum.factory<Status>(StatusVariants);
type Status = Enum<StatusVariants>;
const Status = () => Enum.factory(StatusVariants);

let success = Status.Success(null);
let failure = Status.Failure(null);
let pending = Status.Pending(null);
const success = Status().Success();
const failure = Status().Failure();
const pending = Status().Pending();
```

### Different Variant Data Types

You can attach data of different data types to each variant of an enum by using
the `ofType` helper function. One restriction is that you cannot use `undefined`
or `void` as your variant data type.
the `Variant` function. One restriction is that you cannot use `undefined` or
`void` as your variant data type.

```ts
import { Enum, ofType } from "https://deno.land/x/algebraic_enum/src/mod.ts";
import { Enum, Variant } from "https://deno.land/x/algebraic_enum/src/mod.ts";

const StatusVariants = {
Success: ofType<string>(),
Failure: ofType<{
class StatusVariants {
Success = Variant<string>();
Failure = Variant<{
code: number;
message: string;
}>(),
Pending: null,
};
}>();
Pending = null;
}

type Status = Enum<typeof StatusVariants>;
const Status = Enum.factory<Status>(StatusVariants);
type Status = Enum<StatusVariants>;
const Status = () => Enum.factory(StatusVariants);

let success = Status.Success("Hello World!");
let failure = Status.Failure({
const success = Status().Success("Hello World!");
const failure = Status().Failure({
code: 404,
message: "Not Found",
});
let pending = Status.Pending(null);
const pending = Status().Pending();
```

### Match Data
Expand All @@ -120,7 +116,7 @@ You can use `Enum.match` to determine the correct variant and extract data from
your enum:

```ts
let message = Enum.match(status, {
const message = Enum.match(status, {
Success: (data) => data,
Failure: (data) => data.message,
Pending: (data) => "Pending...",
Expand All @@ -131,7 +127,7 @@ Note that matches need to be exhaustive. You need to exhaust every last
possibility in order for the code to be valid. The following code won't compile:

```ts
let code = Enum.match(status, {
const code = Enum.match(status, {
Failure: (data) => data.code,
// Won't compile because of missing variants
});
Expand All @@ -155,140 +151,44 @@ if (status.Failure !== undefined) {

### Generic Enum Types

It is possible to create generic enum types. Since TypeScript doesn't support
generic objects, you need to create your variants object without generics.
Instead, mark variants with generic types as `unknown`. When constructing the
enum type itself, you can finally override certain variant data types with the
correct generic type.
It is possible to create generic enum types:

```ts
import {
Enum,
memo,
ofType,
} from "https://deno.land/x/algebraic_enum/src/mod.ts";

const StatusVariants = {
Success: ofType<unknown>(),
Failure: ofType<{
import { Enum, Variant } from "https://deno.land/x/algebraic_enum/src/mod.ts";

class StatusVariants<T> {
Success = Variant<T>();
Failure = Variant<{
code: number;
message: string;
}>(),
Pending: null,
Pending = null;
};

// Mark `Success` variant data type as generic
type Status<T> = Enum<typeof StatusVariants & { Success: T }>;
```

Creating an enum factory is going to be more complicated, but possible. You can
wrap it in another function to specify the generic types. To avoid recreating
the enum factory over and over again, it is recommended to use the `memo` helper
function.
type Status<T> = Enum<StatusVariants<T>>;
const Status = <T>() => Enum.factory(StatusVariants<T>);

```ts
type Status<T> = Enum<typeof StatusVariants & { Success: T }>;
const Status = memo(<T>() => Enum.factory<Status<T>>(StatusVariants));

let success = Status<string>().Success("Hello World!");
let failure = Status<boolean>().Failure({
const success = Status<string>().Success("Hello World!");
const failure = Status<boolean>().Failure({
code: 404,
message: "Not Found",
});
let pending = Status<number>().Pending(null);
const pending = Status<number>().Pending();
```

### Mutate Enum Variant

By default, enum types are shallow read-only, meaning you can't change the
variant of an existing enum value or assigning different data to an existing
variant (This is prevented by TypeScript's type system which doesn't incur
variant (this is prevented by TypeScript's type system which doesn't incur
additional runtime performance penalty), but it's still possible to mutate the
underlying variant data itself.

With `Enum.mutate`, you can change the variant of an existing enum value itself,
provided the variable is marked as mutable:

```ts
import { Mut } from "https://deno.land/x/algebraic_enum/src/mod.ts";

// ...

let status = Status<string>().Success("Hello World!");

Enum.mutate(status, Status<string>().Pending(null));
// Compilation error, since `status` is not marked as mutable

let mutableStatus = Status<string>().Pending(null) as Mut<Status<string>>;

Enum.mutate(mutableStatus, Status<string>().Success("Mutated!"));
// `mutableStatus` is now a `Success` variant
```

### Enum Classes

If you want to define methods on your enum for easier readability, you can
create an enum class which behaves like a normal enum and also like a class
where you can have instance methods.

First, you need to define your enum methods separately, extending from the
abstract class `EnumImpl` along with your enum variants object. Your actual type
can be defined using the `EnumClass` helper type.

Make sure your method and property names on your `EnumImpl` class do not collide
with your variant names.
With `Enum.mutate`, you can change the variant of an existing enum value itself:

```ts
import {
ofType,
memo,
Enum,
EnumClass,
EnumImpl,
} from "https://deno.land/x/algebraic_enum/src/mod.ts";

const StatusVariants = {
Success: ofType<unknown>(),
Failure: ofType<{
code: number;
message: string;
}>(),
Pending: null;
}

class StatusImpl<T> extends EnumImpl<typeof StatusVariants & { Success: T }> {
// Make sure to have the correct enum class type as `this`, otherwise you
// won't be able to treat `this` as an `Enum`.
getMessage(this: Status<T>): string {
return Enum.match(this, {
Success: (data) => data,
Failure: (data) => data.message,
Pending: (data) => "Pending...",
});
}

// Declare `this` as mutable to enable enum mutation.
fail(this: Mut<Status<T>>, code: number, message: string): void {
Enum.mutate(this, Status<T>().Failure({ code, message }));
}
}

type Status<T> = EnumClass<StatusImpl<T>>;
```

It's also possible to create an enum factory for easy object construction by
additionally passing `StatusImpl` to `Enum.factory`.

```ts
const Status = memo(<T>() =>
Enum.factory<Status<T>>(StatusVariants, StatusImpl)
);

let status = Status<string>().Success("Hello!");
let message = status.getMessage();
message.fail(404, "Not found");
// Compilation error, since `message` is not marked as mutable
const status = Status<string>().Success("Hello World!");

let mutableStatus = Status<string>().Pending(null) as Mut<Status<string>>;
mutableStatus.fail(404, "Not found");
Enum.mutate(status, Status<string>().Pending());
// `status` is now of variant `Pending`
```

0 comments on commit 08d6591

Please sign in to comment.