Skip to content

Commit 22beaab

Browse files
committed
initial commit
0 parents  commit 22beaab

File tree

7 files changed

+307
-0
lines changed

7 files changed

+307
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
package-lock.json
4+
pnpm-lock.yaml
5+
yarn.lock

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 LockBlock-dev
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# PoC TypeScript Option and Result type
2+
3+
This project implements basic versions of the `Option` and `Result` type, inspired by their counterparts in the Rust programming language, to demonstrate their benefits in TypeScript.
4+
5+
## Motivation
6+
7+
In Rust, the [`Option`](https://doc.rust-lang.org/stable/core/option/index.html) type is used to represent the presence or absence of a value. The [`Result`](https://doc.rust-lang.org/stable/core/result/index.html) type, on the other hand, is designed for error handling, avoiding exceptions by encouraging explicit handling of both success and failure.
8+
9+
While TypeScript supports [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) to determine if a value is null or undefined, making the `Option` type less crucial, the `Result` type is particularly valuable. It allows developers to represent errors directly in the return type, promoting safer and more predictable error handling by leveraging TypeScript's type system to enforce explicit handling of both success and failure cases, rather than depending on unchecked exceptions.
10+
11+
## Overview
12+
13+
### [`Option<T>`](./lib/option.ts)
14+
15+
The `Option` type represent an optional value, either `Some` (a value is present) or `None` (no value). It removes ambiguity and is safer than relying on `null` or `undefined`.
16+
17+
**Example:**
18+
19+
```ts
20+
import { type Option, Some, None } from "./option";
21+
22+
interface User {
23+
id: number;
24+
name: string;
25+
address?: string;
26+
}
27+
28+
const users: User[] = [
29+
{ id: 1, name: "Alice", address: "123 Main St" },
30+
{ id: 2, name: "Bob" },
31+
{ id: 3, name: "Charlie", address: "456 Oak St" },
32+
];
33+
34+
function getUserAddress(userId: number): Option<string> {
35+
const user = users.find((u) => u.id === userId);
36+
return user && user.address ? Some(user.address) : None;
37+
}
38+
39+
const opt = getUserAddress(1);
40+
// const opt = getUserAddress(2);
41+
// const opt = getUserAddress(4);
42+
43+
if (opt.isSome()) {
44+
// infered string
45+
console.log(opt.unwrap());
46+
} else {
47+
// infered never
48+
console.log("No value");
49+
}
50+
```
51+
52+
### [`Result<T, E>`](./lib/result.ts)
53+
54+
The `Result` type represents the outcome of an operation that can succeed or fail, either `Ok` (success with a value) or `Err` (failure with an error). It avoids throwing exceptions and provides a functional approach to error handling.
55+
56+
**Example:**
57+
58+
```ts
59+
import { type Result, Ok, Err } from "./result";
60+
61+
function divide(dividend: number, divisor: number): Result<number, Error> {
62+
if (divisor === 0) {
63+
return Err(new Error("Cannot divide by zero"));
64+
} else {
65+
return Ok(dividend / divisor);
66+
}
67+
}
68+
69+
const res = divide(10, 2);
70+
// const res = divide(10, 0);
71+
72+
if (res.isOk()) {
73+
// infered number
74+
console.log(res.unwrap());
75+
} else if (res.isErr()) {
76+
// infered Error
77+
console.error(res.unwrapErr().message);
78+
}
79+
```
80+
81+
## License
82+
83+
See the [LICENSE](./LICENSE).

lib/option.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Represents an optional value.
3+
*/
4+
export interface Option<T> {
5+
/**
6+
* Returns the contained `Some` value.
7+
*/
8+
unwrap(): T;
9+
/**
10+
* Returns the contained `Some` value or a default.
11+
*/
12+
unwrapOr(defaultValue: T): T;
13+
/**
14+
* Returns `true` if the option is a `Some` value.
15+
*/
16+
isSome(): this is SomeOption<T>;
17+
/**
18+
* Returns `true` if the option is a `None` value.
19+
*/
20+
isNone(): this is NoneOption;
21+
}
22+
23+
type SomeOption<T> = Option<T>;
24+
type NoneOption = Option<never>;
25+
26+
class OptionImpl<T> implements Option<T> {
27+
private constructor(private value?: T) {}
28+
29+
public unwrap(): T {
30+
if (this.isSome()) return this.value as T;
31+
32+
throw new Error(`called \`unwrap()\` on a \`None\` value`);
33+
}
34+
35+
public unwrapOr(defaultValue: T): T {
36+
return this.isSome() ? (this.value as T) : defaultValue;
37+
}
38+
39+
public isSome(): this is SomeOption<T> {
40+
return this.value !== undefined;
41+
}
42+
43+
public isNone(): this is NoneOption {
44+
return this.value === undefined;
45+
}
46+
47+
/**
48+
* Creates a new `Some` option.
49+
*/
50+
public static some<T>(value: T): SomeOption<T> {
51+
return new OptionImpl(value);
52+
}
53+
54+
/**
55+
* Creates a new `None` option.
56+
*/
57+
public static none(): NoneOption {
58+
return new OptionImpl();
59+
}
60+
}
61+
62+
/**
63+
* Creates a new `Some` option.
64+
*/
65+
export const Some = OptionImpl.some;
66+
/**
67+
* Creates a new `None` option.
68+
*/
69+
export const None = OptionImpl.none();

lib/result.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Option, Some, None } from "./option";
2+
3+
/**
4+
* Represents a value that can be either a success or a failure.
5+
*/
6+
export interface Result<T, E = Error> {
7+
/**
8+
* Returns the contained `Ok` value.
9+
*/
10+
unwrap(): T;
11+
/**
12+
* Returns the contained `Ok` value or a default.
13+
*/
14+
unwrapOr(defaultValue: T): T;
15+
/**
16+
* Returns the contained `Err` value.
17+
*/
18+
unwrapErr(): E;
19+
/**
20+
* Returns `true` if the result is `Ok`.
21+
*/
22+
isOk(): this is OkResult<T>;
23+
/**
24+
* Returns `true` if the result is `Err`.
25+
*/
26+
isErr(): this is ErrResult<E>;
27+
/**
28+
* Converts from `Result<T, E>` to `Option<T>`.
29+
*/
30+
ok(): Option<T>;
31+
/**
32+
* Converts from `Result<T, E>` to `Option<E>`.
33+
*/
34+
err(): Option<E>;
35+
}
36+
37+
type OkResult<T> = Result<T, never>;
38+
type ErrResult<E = Error> = Result<never, E>;
39+
40+
class ResultImpl<T, E = Error> implements Result<T, E> {
41+
private constructor(private value: T | E, private isError: boolean) {}
42+
43+
public unwrap(): T {
44+
if (this.isOk()) return this.value as T;
45+
46+
throw new Error(
47+
`called \`unwrap()\` on an \`Err\` value: ${this.value}`,
48+
);
49+
}
50+
51+
public unwrapOr(defaultValue: T): T {
52+
return this.isOk() ? (this.value as T) : defaultValue;
53+
}
54+
55+
public unwrapErr(): E {
56+
if (this.isErr()) return this.value as E;
57+
58+
throw new Error(
59+
`called \`unwrapErr()\` on an \`Ok\` value: ${this.value}`,
60+
);
61+
}
62+
63+
public isOk(): this is OkResult<T> {
64+
return !this.isError;
65+
}
66+
67+
public isErr(): this is ErrResult<E> {
68+
return this.isError;
69+
}
70+
71+
public ok(): Option<T> {
72+
return this.isOk() ? Some(this.value as T) : None;
73+
}
74+
75+
public err(): Option<E> {
76+
return this.isErr() ? Some(this.value as E) : None;
77+
}
78+
79+
/**
80+
* Creates a new `Ok` result.
81+
*/
82+
public static ok<T>(value: T): OkResult<T> {
83+
return new ResultImpl<T, never>(value, false);
84+
}
85+
86+
/**
87+
* Creates a new `Err` result.
88+
*/
89+
public static err<E = Error>(error: E): ErrResult<E> {
90+
return new ResultImpl<never, E>(error, true);
91+
}
92+
}
93+
94+
/**
95+
* Creates a new `Ok` result.
96+
*/
97+
export const Ok = ResultImpl.ok;
98+
/**
99+
* Creates a new `Err` result.
100+
*/
101+
export const Err = ResultImpl.err;

package.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "poc-ts-option-and-result-type",
3+
"private": true,
4+
"version": "1.0.0",
5+
"author": "LockBlock-dev",
6+
"license": "MIT",
7+
"scripts": {
8+
"build": "tsc",
9+
"watch": "tsc -w"
10+
},
11+
"devDependencies": {
12+
"typescript": "^5.7.2"
13+
}
14+
}

tsconfig.json

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "CommonJS",
5+
"rootDir": "lib",
6+
"moduleResolution": "node",
7+
"outDir": "./dist",
8+
"esModuleInterop": true,
9+
"forceConsistentCasingInFileNames": true,
10+
11+
"strict": true,
12+
"skipLibCheck": true
13+
}
14+
}

0 commit comments

Comments
 (0)