typesafely
v0.0.5
Published
TypeScript types designed to emulate the Rust Option and Result types.
Downloads
3
Readme
TypeSafely
TypeScript types (Option
, Result
and AsyncResult
) designed to emulate Rust types and patterns.
There are a lot of npm libraries which do this. Here's another.
type CalculationResult = Result<CalculationOk, CalculationError>;
// Return a Result from a calculation which may fail
const performCalculationSafely = (): CalculationResult => {
try {
const data = handleCalculation();
return Ok(data);
} catch (e) {
return Err(e.message);
}
};
// The data variable is now a Result type which must be either Ok or Err
const data: CalculationResult = performCalculationSafely();
// Now pass data to the matchResult function and explicitly handle each state:
matchResult(data, {
ok: (x) => x,
err: (e) => e,
});
Usage
This library provides three high level types:
Option
Result
AsyncResult
The Option
and Result
types are modeled after the same types in Rust. The AsyncResult
type is like a Result
but includes an additional state to represent "loading" and is intended to be used for data which is produced asynchronously.
In addition to these type primitives there are a few additional helper methods and functions:
- 'matching' functions,
matchOption
,matchResult
,matchAsyncResult
which operate like Rust match expressions. unwrap
andunwrapOr
methods. Like in Rust,unwrap
will "panic" if a type was not in an "Ok" state.if[Ok|Err|Loading]
andif[Some|None]
methods which allow you to conditionally run some logic if a type is of a particular variants. Non-matching variants will be ignored.
Option Type
// Create a Some Option variant and pass it to a match statement
const opt: Option<number> = Some(900);
// The some branch will run and x will be 900
matchOption(opt, {
some: (x) => console.log("[Some variant]:", x),
none: () => console.log("[None variant]"),
});
// Create a None Option variant and pass it to a match statement
const opt: Option<number> = None();
// The none branch will run:
matchOption(opt, {
some: (x) => console.log("[Some variant]:", x),
none: () => console.log("[None variant]"),
});
Result Type
// Create an Ok Result variant and pass it to a match statement
const result: Result<number, string> = Ok(100);
// The ok branch will run and x will be 100:
matchResult(result, {
ok: (x) => console.log("[Ok variant]:", x),
err: (e) => console.log("[Err variant]:", e),
});
// Create an Ok Result variant and pass it to a match statement
const err: Result<number, string> = Err("Error");
// The err branch will run and e will be "Error"
matchResult(result, {
ok: (x) => console.log("[Ok variant]:", x),
err: (e) => console.log("[Err variant]:", e),
});
AsyncResult Type
The AsyncResult
is similar to the Result
type but includes another variant to represent loading state. This is especially useful for modelling asynchronously fetched data and provides strong guarantees you are handling the appropriate state of the response. No need to independently set and update loading/error/response states, which is error prone. No need to write out fragile logic like !loading && !response
to check for error states.
const FetchDataComponent: React.FC = () => {
// Use an AsyncResult to model some asynchronously fetched data
const [data, setData] = React.useState<AsyncResult<number, string>>(
AsyncResultLoading(),
);
const fetchData = async () => {
try {
// Handle fetching data here...
setData(AsyncOk({ data: "ok!" }));
} catch (err) {
setData(AsyncErr("Failed to fetch data..."));
}
}
React.useEffect(() => {
fetchData();
});
return (
<>
{matchAsyncResult(data, {
ok: x => <p>Data: {JSON.stringify(x)}<p>,
err: e => <p>Error fetching data: {JSON.stringify(e)}</p>,
loading: () => <p>Loading...</p>,
})}
</>
);
};
Motivation
The main idea behind this approach is twofold and similar to the rationale for the similar design in Rust:
Result
types can be used to model values which may represent an error state and avoid throwing and catching errors (which is difficult to type-check correctly in TypeScript). AResult
makes it explicitly that a function may result in an error state, which calling code must handle.Option
types can be used to model values which may be in a present or absent state, which otherwise in JS/TS are usually modeled withnull
orundefined
. AnOption
makes this presence or absence more explicit and avoids issues like0 == false
"" == false"
etc.
For instance, imagine you have some value which is declared but not initialized yet.
const value: number = null;
// Somewhere else:
value = 50;
Later you want to check if the value is initialized and then run some other code:
if (!!value) {
// Run some other code which expects value to be defined
}
But what if actually, some other code had already set this value to be 0
? Then your !!value
check would result in false
and your code wouldn't run.
This is a simple example but a common pitfall and one which TypeScript can't easily protect against. Consider instead this:
const value: Option<number> = None();
matchOption(value, {
some: (x) => x, // Handle non-empty case
none: () => null, // Handle empty case
});
This code avoids the above issues completely.