Giter Site home page Giter Site logo

true-myth / true-myth Goto Github PK

View Code? Open in Web Editor NEW
862.0 8.0 28.0 6.71 MB

A library for safer and smarter error- and "nothing"-handling in TypeScript.

Home Page: https://true-myth.js.org

License: MIT License

TypeScript 98.61% Shell 0.38% JavaScript 1.01%
typescript typescript-library typescript-definitions functional-programming javascript monads functors applicatives

true-myth's Introduction

A library for safe, idiomatic null and error handling in TypeScript, with Maybe and Result types, supporting both a functional style and a more traditional method-call style.

Test coverage: 100% npm supported Node versions supported TypeScript versions Nightly TypeScript Run Stability: Active DNS by JS.org docs built with Typedoc

READMEAPI docsSourceIntro blog post

Overview

NOTE: this documentation is for version 6.x and 7.x, which require using TypeScript's more recent moduleResolution modes: "node16", "nodenext", or "bundler". (See TypeScript's docs on moduleResolution for more details!) If you cannot use that yet, please use version 5.x.

True Myth provides standard, type-safe wrappers and helper functions to help help you with two extremely common cases in programming:

  • not having a value
  • having a result where you need to deal with either success or failure

You could implement all of these yourself – it's not hard! – but it's much easier to just have one extremely well-tested library you can use everywhere to solve this problem once and for all.

Contents

Requirements

  • Node 18+
  • TS 4.7+
  • tsconfig.json:
    • moduleResolution: "Node16"
  • package.json
    • type: "module" (or else use import() to import True Myth into a commonJS build)

For details on using a pure ES modules package in TypeScript, see the TypeScript handbook's guide.

Setup

Add True Myth to your dependencies:

  • with Yarn:

    yarn add true-myth
  • with npm:

    npm install true-myth

This package ships ES6-modules so you can import the modules directly, or import the re-exports from the root module:

// this works:
import Maybe from 'true-myth/maybe';
import Result from 'true-myth/result';

// this also works:
import { Maybe, Result } from 'true-myth';

Basic bundle size info

Size of the ESM build without tree-shaking (yes, these are in bytes: this is a pretty small library!):

file size (B) terser1 (B) terser and brotli2 (B)
index.js 561 216 93
maybe.js 19646 3464 871
result.js 12744 3162 787
toolbelt.js 3598 881 270
unit.js 653 58 57
utils.js 888 321 166
total3 38090 8102 2244

Notes:

  • The unmodified size includes comments.
  • Thus, running through Terser gets us a much more realistic size: about 7.9KB to parse.
  • The total size across the wire of the whole library will be ~2.2KB.

Compatibility

This project follows the current draft of the Semantic Versioning for TypeScript Types specification.

  • Currently supported TypeScript versions: 4.7, 4.8, 4.9, 5.0, 5.1, 5.2, and 5.3
  • Compiler support policy: simple majors
  • Public API: all published types not in a -private module are public

Just the API, please

If you're unsure of why you would want to use the library, you might jump down to Why do I need this?.

These examples don't cover every corner of the API; it's just here to show you what a few of the functions are like. Full API documentation is available! You can also view the source if you prefer.

Result with a functional style

import Result, { err, map, ok, toString } from 'true-myth/result';

function fallibleCheck(isValid: boolean): Result<string, { reason: string }> {
  return isValid ? ok('all fine here') : err({ reason: 'was not valid' });
}

const describe = (s) => 'The outcome was: ' + s;

const wentFine = fallibleCheck(true);
const mappedFine = map(describe, wentFine);
console.log(toString(mappedFine)); // "Ok(The outcome was: all fine here)"

const notGreat = fallibleCheck(false);
const mappedBad = map(describe, notGreat);
console.log(toString(mappedBad)); // "Err({ reason: 'was not valid' })"

Maybe with the method style

import Maybe, { just, nothing } from 'true-myth/maybe';

function safeLength(mightBeAString: Maybe<string>): Maybe<number> {
  return mightBeAString.map((s) => s.length);
}

const justAString = just('a string');
const nothingHere = nothing<string>();
console.log(safeLength(justAString).toString()); // Just(8)
console.log(safeLength(nothingHere).toString()); // Nothing

Constructing Maybe

You can use Maybe.of to construct a Maybe from any value. It will return a Nothing if the passed type is null or undefined, or a Just otherwise.

import Maybe from 'true-myth/maybe';

function acceptsANullOhNo(value: number | null): Maybe<string> {
  const maybeNumber = Maybe.of(value);
  return mapOr('0', (n) => n.toString(), maybeNumber);
}

Safely getting at values

The library provides smart type narrowing tools to allow you to get at the values wrapped in the type:

import { ok } from 'true-myth/result';

const theAnswer = ok(42);
const theAnswerValue = theAnswer.isOk ? theAnswer.value : 0;

However, ternaries like this can be annoying at times, and don't necessarily fit into functional composition pipelines when the expressions become more complicated. For situations like those, you can use one of the safe unwrap methods:

import { ok, unwrapOr } from 'true-myth/result';

const theAnswer = ok(42);
const theAnswerValue = unwrapOr(0, theAnswer);

You can also use TypeScript's "type narrowing" capabilities: if you check which variant you are accessing, TypeScript will "narrow" the type to that variant and allow you to access the value directly if it is available.

import Maybe from 'true-myth/maybe';

// Maybe<string>
const couldBeSomething = Maybe.of('Hello!');

// type error, because `value` does not exist on `Nothing`:
// couldBeSomething.value;

if (couldBeSomething.isJust) {
  // valid, because `couldBeSomething` is "narrowed" to `Just` here:
  console.log(couldBeSomething.value);
}

This can also be convenient in functional style pipelines:

import { filter, map, pipe, prop } from 'ramda';
import Result from 'true-myth/result';

function getErrorMessages(results: Array<Result<string, Error>>) {
  return results
    .filter(Result.isErr)
    .map(Err.unwrapErr) // would not type-checkout with previous line
    .map((error) => error.message);
}

Curried variants

All static functions which take two or more parameters are automatically partially applied/curried so you can supply only some of the arguments as makes sense. For example, if you were using lodash, you might have something like this:

import * as _ from 'lodash/fp';
import { just, nothing, map } from 'true-myth/maybe';

const length = (s: string) => s.length;
const even = (n: number) => n % 2 === 0;
const timesThree = (n: number) => n * 3;

const transform = _.flow(
  // transform strings to their length: Just(3), Nothing, etc.
  _.map(map(length)),
  // drop `Nothing` instances
  _.filter(_.prop('isJust')),
  // get value now that it's safe to do so (TS will not allow it earlier)
  _.map(_.prop('value')),
  // only keep the even numbers ('fish' => 4)
  _.filter(even),
  // multiply by three
  _.map(timesThree),
  // add them up!
  _.sum
);

const result = transform([
  just('yay'),
  nothing(),
  nothing(),
  just('waffles'),
  just('fish'),
  just('oh'),
]);

console.log(result); // 18

This makes for a much nicer API than needing to include the parameters for every function. If we didn't have the curried functions, we'd have a much, much noisier input:

import * as _ from 'lodash';
import { map } from 'true-myth/maybe';

const length = (s: string) => s.length;
const even = (n: number) => n % 2 === 0;
const timesThree = (n: number) => n * 3;

const result = transform([
  Maybe.of('yay'),
  Maybe.nothing(),
  Maybe.nothing(),
  Maybe.of('waffles'),
  Maybe.of('fish'),
  Maybe.of('oh'),
]);

const transform = _.flow(
  // transform strings to their length: Just(3), Nothing, etc.
  (maybeStrings) => _.map(maybeStrings, (maybeString) => map(length, maybeString)),
  // drop `Nothing` instances
  (maybeLengths) => _.filter(maybeLengths, (maybe) => maybe.isJust),
  // get value now that it's safe to do so (TS will not allow it earlier)
  (justLengths) => _.map(justLengths, (maybe) => maybe.value),
  // only keep the even numbers ('fish' => 4)
  (lengths) => _.filter(lengths, even),
  // multiply by three
  (evenLengths) => _.map(evenLengths, timesThree),
  // add them up!
  _.sum
);

console.log(result); // 18

This "point-free" style isn't always better, but it's available for the times when it is better. (Use it judiciously!)

Why do I need this?

There are two motivating problems for True Myth (and other libraries like it): dealing with nothingness and dealing with operations which can fail.

1. Nothingness: null and undefined

How do you represent the concept of not having anything, programmatically? As a language, JavaScript uses null to represent this concept; if you have a variable myNumber to store numbers, you might assign the value null when you don't have any number at all. If you have a variable myString, you might set myString = null; when you don't have a string.

Some JavaScript programmers use undefined in place of null or in addition to null, so rather than setting a value to null they might just set let myString; or even let myString = undefined;.

Every language needs a way to express the concept of nothing, but null and undefined are a curse. Their presence in JavaScript (and in many other languages) introduce a host of problems, because they are not a particularly safe way to represent the concept. Say, for a moment, that you have a function that takes an integer as a parameter:

let myNumber = undefined;

function myFuncThatTakesAnInteger(anInteger) {
  return anInteger.toString();
}

myFuncThatTakesAnInteger(myNumber); // TypeError: anInteger is undefined

this is fine

When the function tries to convert the integer to a string, the function blows up because it was written with the assumption that the parameter being passed in (a) is defined and (b) has a toString method. Neither of these assumptions are true when anInteger is null or undefined. This leads JavaScript programmers to program defensively, with if (!anInteger) return; style guard blocks at the top of their functions. This leads to harder-to-read code, and what's more, it doesn't actually solve the root problem.

You could imagine this situation playing itself out in a million different ways: arguments to functions go missing. Values on objects turn out not to exist. Arrays are absent instead of merely empty. The result is a steady stream not merely of programming frustrations, but of errors. The program does not function as the programmer intends. That means stuff doesn't work correctly for the user of the software.

You can program around null and undefined. But defensive programming is gross. You write a lot of things like this:

function isNil(thingToCheck) {
  return thingToCheck === undefined || thingToCheck === null;
}

function doAThing(withAString) {
  if (isNil(withAString)) {
    withAString = 'some default value';
  }

  console.log(withAString.length);
}

If you forget that check, or simply assume, "Look, I'll never call this without including the argument," eventually you or someone else will get it wrong. Usually somewhere far away from the actual invocation of doAThing, so that it's not obvious why that value ended up being null there.

TypeScript takes us a big step in that direction, so long as our type annotations are good enough. (Use of any will leave us sad, though.) We can specify that type may be present, using the optional annotation. This at least helps keep us honest. But we still end up writing a ton of repeated boilerplate to deal with this problem. Rather than just handling it once and being done with it, we play a never-ending game of whack-a-mole. We must be constantly vigilant and proactive so that our users don't get into broken error states.

2. Failure handling: callbacks and exceptions

Similarly, you often have functions whose return value represents an operation which might fail in some way. We also often have functions which have to deal with the result of operations which might fail.

Many patterns exist to work around the fact that you can't very easily return two things together in JavaScript. Node has a callback pattern with an error as the first argument to every callback, set to null if there was no error. Client-side JavaScript usually just doesn't have a single pattern for handling this.

In both cases, you might use exceptions – but often an exception feels like the wrong thing because the possibility of failure is built into the kind of thing you're doing – querying an API, or checking the validity of some date, and so on.

In Node.js, the callback pattern encourages a style where literally every function starts with the exact same code:

const doSomething = (err, data) => {
  if (err) {
    return handleErr(err);
  }

  // do whatever the *actual* point of the function is
};

There are two major problems with this:

  1. It's incredibly repetitive – the very opposite of "Don't Repeat Yourself". We wouldn't do this with anything else in our codebase!

  2. It puts the error-handling right up front and not in a good way. While we want to have a failure case in mind when designing the behavior of our functions, it's not usually the point of most functions – things like handleErr in the above example being the exception and not the rule. The actual meat of the function is always after the error handling.

Meanwhile, in client-side code, if we're not using some similar kind of callback pattern, we usually resort to exceptions. But exceptions are unpredictable: you can't know whether a given function invocation is going to throw an exception until runtime as someone calling the function. No big deal if it's a small application and one person wrote all the code, but with even a few thousand lines of code or two developers, it's very easy to miss that. And then this happens:

// in one part of the codebase
function getMeAValue(url) {
  if (isMalformed(url)) {
    throw new Error(`The url `${url}` is malformed!`);
  }

  // do something else to load data from the URL
}

// somewhere else in the codebase
const value = getMeAValue('http:/www.google.com');  // missing slash

Notice: there's no way for the caller to know that the function will throw. Perhaps you're very disciplined and write good docstrings for every function – and moreover, perhaps everyone's editor shows it to them and they pay attention to that briefly-available popover. More likely, though, this exception throws at runtime and probably as a result of user-entered data – and then you're chasing down the problem through error logs.

More, if you do want to account for the reality that any function anywhere in JavaScript might actually throw, you're going to write something like this:

try {
  getMeAValue('http:/www.google.com'); // missing slash
} catch (e) {
  handleErr(e);
}

This is like the Node example but even worse for repetition!

Nor can TypeScript help you here! It doesn't have type signatures to say "This throws an exception!" (TypeScript's never might come to mind, but it might mean lots of things, not just exception-throwing.)

Neither callbacks nor exceptions are good solutions here.

Solutions: Maybe and Result

Maybe and Result are our escape hatch from all this madness.

We reach for libraries precisely so we can solve real business problems while letting lower-level concerns live in the "solved problems" category. True Myth, borrowing ideas from many other languages and libraries, aims to put code written to defend against null/undefined problems in that "solved problems" category.

Maybe and Result solve this problem once, and in a principled way, instead of in an ad-hoc way throughout your codebase, by putting the value into a container which is guaranteed to be safe to act upon, regardless of whether there's something inside it or not.

These containers let us write functions with actually safe assumptions about parameter values by extracting the question, "Does this variable contain a valid value?" to API boundaries, rather than needing to ask that question at the head of every. single. function.

What is this sorcery?

How it works: Maybe

It turns out you probably already have a good idea of how this works, if you've spent much time writing JavaScript, because this is exactly how arrays work.

Imagine, for a moment, that you have a variable myArray and you want to map over it and print out every value to the console. You instantiate it as an empty array and then forget to load it up with values before mapping over it:

let myArray = [];
// oops, I meant to load up the variable with an array, but I forgot!
myArray.forEach((n) => console.log(n)); // <nothing prints to the screen>

Even though this doesn't print anything to the screen, it doesn't unexpectedly blow up, either. In other words, it represents the concept of having nothing "inside the box" in a safe manner. By contrast, an integer has no such safe box around it. What if you could multiply an integer by two, and if your variable was "empty" for one reason or another, it wouldn't blow up?

let myInteger = undefined;

myInteger * 3; // 😢

Let's try that again, but this time let's put the actual value in a container and give ourselves safe access methods:

import Maybe from 'true-myth/maybe';

const myInteger = Maybe.of(undefined);
myInteger.map((x) => x * 3); // Nothing

mind blown

We received Nothing back as our value, which isn't particularly useful, but it also didn't halt our program in its tracks!

Best of all, when you use these with libraries like TypeScript, you can lean on their type systems to check aggressively for null and undefined, and actually eliminate those from your codebase by replacing anywhere you would have used them with Maybe.

How it works: Result

Result is similar to Maybe, except it packages up the result of an operation (like a network request) whether it's a success (an Ok) or a failure (an Err) and lets us unwrap the package at our leisure. Whether you get back a 200 or a 401 for your HTTP request, you can pass the box around the same either way; the methods and properties the container has are not dependent upon whether there is shiny new data or a big red error inside.

import { ok, err } from 'true-myth/result';

const myNumber = ok<number, string>(12);
const myNumberErr = err<number, string>('oh no');

console.log(myNumber.map((n) => n * 2)); // Ok(24)
console.log(myNumberErr.map((n) => n * 2)); // Err(oh no)

Thus, you can replace functions which take polymorphic arguments or have polymorphic return values to try to handle scenarios where something may be a success or an error with functions using Result.

Any place you try to treat either a Maybe or a Result as just the underlying value rather than the container, the type systems will complain, of course. And you'll also get help from smart editors with suggestions about what kinds of values (including functions) you need to interact with any given helper or method, since the type definitions are supplied.

By leaning on TypeScript to handle the checking, we also get all these benefits with no runtime overhead other than the cost of constructing the actual container objects (which is to say: very low!).

Design philosophy

The design aims for True Myth are:

  • to be as idiomatic as possible in JavaScript
  • to support a natural functional programming style
  • to have zero runtime cost beyond simple object construction and function invocation
  • to lean heavily on TypeScript to enable all of the above

In practice, that means:

  • You can construct the variant types in the traditional JavaScript way or with a pure function:

    import Maybe, { just, nothing } from 'true-myth/maybe';
    
    const classicalJust = new Maybe('value');
    const classicalNothing = new Maybe<string>();
    
    const functionalJust = just('value');
    const functionalNothing = nothing();
  • Similarly, you can use methods or pure functions:

    import { ok, map } from 'true-myth/result';
    
    const numberResult = ok(42);
    const ok84 = numberResult.map((x) => x * 2);
    const ok21 = map((x) => x / 2, numberResult);

    As this second example suggests, the aim has been to support the most idiomatic approach for each style. This means that yes, you might find it a bit confusing if you're actively switching between the two of them. (Why would you do that?!?)

  • Using the library with TypeScript will just work and will provide you with considerable safety out of the box. Using it with JavaScript will work just fine, but there is no runtime checking, and you're responsible to make sure you don't unwrap() a Maybe without checking that it's safe to do so.

  • Since this is a TypeScript-first library, we intentionally leave out any runtime type checking. As such, you should make use of the type systems if you want the benefits of the system. Many of the functions simply assume that the types are checked, and will error if you pass in items of the wrong type.

    For example, if you pass a non-Maybe instance to many functions, they will simply fail – even the basic helpers like isJust and isNothing. These assumptions have been made precisely because this is a TypeScript-first library. (See the discussion below comparing True Myth to Folktale and Sanctuary if you aren't using TypeScript and need runtime checking.)

The overarching themes are flexibility and approachability.

The hope is that a team just picking up these ideas for the first time can use them without adapting their whole style to a "traditional" functional programming approach, but a team comfortable with functional idioms will find themselves at home with the style of data-last pure functions. (For a brief discussion of why you want the data last in a functional style, see this blog post.)

A note on reference types: no deep copies here!

One important note: True Myth does not attempt to deeply-clone the wrapped values when performing operations on them. Instead, the library assumes that you will not mutate those objects in place. (Doing more than this would require taking on a dependency on e.g. lodash). If you violate that constraint, you can and will see surprising outcomes. Accordingly, you should take care not to mutate reference types, or to use deep cloning yourself when e.g. mapping over reference types.

import { just, map } from 'true-myth/maybe';

const anObjectToWrap = {
  desc: ['this', ' ', 'is a string'],
  val: 42,
};

const wrapped = just(anObjectToWrap);
const updated = map((obj) => ({ ...obj, val: 92 }), wrapped);

console.log((anObjectToWrap as Just<number>).val); // 42
console.log((updated as Just<number>).val); // 92
console.log((anObjectToWrap as Just<string[]>).desc); // ["this", " ", "is a string"]
console.log((updated as Just<string[]>).desc); // ["this", " ", "is a string"]

// Now mutate the original
anObjectToWrap.desc.push('.');

// And… 😱 we've mutated the new one, too:
console.log((anObjectToWrap as Just<string[]>).desc); // ["this", " ", "is a string", "."]
console.log((updated as Just<string[]>).desc); // ["this", " ", "is a string", "."]

In other words: you must use other tools along with True Myth if you're going to mutate objects you're wrapping in Maybe or Result.

True Myth will work quite nicely with lodash, Ramda, Immutable-JS, etc., so you can use whatever tools you like to handle this problem.

The type names

Maybe

The existing options in this space include Option, Optional, and Maybe. You could also point to "nullable," but that actually means the opposite of what we're doing here – these represent types which can not be nullable!

Option implies a choice between several different options; in this case that's not really what's going on. It's also not really a great word for the type in the sense that it's weird to read aloud: "an Option string" doesn't make any sense in English.

Optional is much better than Option. The semantics are much more accurate, in that it captures that the thing is allowed to be absent. It's also the nicest grammatically: "an Optional string". On the other hand, it's also the longest.

Maybe seems to be the best type name semantically: we're modeling something which may be there – or may not be there! Grammatically, it's comparable to "optional": "a Maybe string" isn't great – but "maybe a string" is the most natural accurate way to answer the question, "What's in this field?" It's also the shortest!

Optional or Maybe are both good names; Maybe just seemed slightly better.

The Maybe variants: Just and Nothing

Similar consideration was given to the names of the type variants. Options for the "present" type in other libraries are Some and Just. Options for the "absent" type are None or Nothing.

Why Just?

Both Just and Some are reasonable choices for this, and both have things to recommend them semantically:

  • When talking about the type of given item, "some" makes a lot of sense: "What's in this field? Some number." You can get the same idea across with "just" but it's a bit less clear: "What's in this field? Just a number."
  • On the other hand, when talking about or constructing a given value, "just" makes more sense: "What is this? It's just 12." When you try to use "some" there, it reads oddly: "What is this? It's some 12."

Given that "just a number" works (even if it's strictly a little less nice than "some number") and that "just 12" works but "some 12" doesn't, Just seems to be a slightly better option.

Why Nothing?

Given the choice between None and Nothing, the consideration just came down to the most natural language choice. "What's here? Nothing!" makes sense, while "What's here? None" does not. None also implies that there might be more than one of the items. It's entirely unnatural to say "There is none of a number here"; you'd normally say "there is no number here" or "there is nothing here" instead. So Nothing it is!

Result

In some languages and libraries, a more general type named Either is used instead of the more specific Result name. The two are equivalent in functionality – both provide two variants, each of which wraps a value. In the Either implementations, those are usually named Left and Right. In the Result implementations (both here and in other libraries and languages), they are named Ok and Err.

The main difference between Either and Result is precisely that question of generality. Either can meaningfully capture any scenario where there are two possible values resulting from a given function application, or applicable as arguments to a function. Result only captures the idea of something succeeding or failing. In that sense, Either might seem to be better: it can capture what Result captures (traditionally with Left being the error case and Right being the success, or right, case), and many more besides.

However, in practice, the idea of a result is far and away the most common case for using an Either, and it's also the easiest to explain. (An Either implementation would also be valuable, though, and it might be a later addition to the library.)

The Result variants: Ok and Err

Given a "result" type, we need to be able to express the idea of "success" and "failure." The most obvious names here would be Success and Failure. Those are actually really good names with a single problem: they're long. Needing to write success(12) or failure({ oh: 'no' }) is a lot to write over and over again. Especially when there some options which also work well: Ok and Err.

Both Ok and Err could be written out long-form: Okay and Error. But in this case, the longer names don't add any particular clarity; they require more typing; and the Error case also overloads the existing name of the base exception type in JavaScript. So: Ok and Err it is.

Inspiration

The design of True Myth draws heavily on prior art; essentially nothing of this is original – perhaps excepting the choice to make Maybe.of handle null and undefined in constructing the types. In particular, however, True Myth draws particular inspiration from:

Why not...

There are other great functional programming libraries out there... so why not just use one of them?

Note that much of the content between these sections is the same; it's presented as is so you can simply read the section appropriate to the library you're comparing it with.

Folktale?

Folktale has an API a lot like this one, as you'll see when perusing the docs. However, there are two main reasons you might prefer True Myth to Folktale:

  1. True Myth is TypeScript-first, which means that it assumes you are using TypeScript if you're aiming for rigorous type safety.

    By contrast, Folktale is a JavaScript-first library, with runtime checking built in for its types. Folktale's TypeScript support is in-progress, but will remain secondary until a TypeScript rewrite of the whole Folktale library lands... eventually.

    There's value in both of these approaches, so True Myth aims to take advantage of the compilers and play in a no-runtime-cost space.

    If you want a JS-focused (rather than TS-focused) library which will help you be safer without a compiler, you should definitely pick Folktale over True Myth. If you've already using TS, True Myth is a bit nicer of an experience.

  2. True Myth aims to keep functional programming jargon to a minimum and to use TypeScript type notation throughout its docs as well as in its implementation.

    Folktale is aimed squarely at people who are already pretty comfortable with the world of strongly-typed functional programming languages. This is particularly evident in the way its type signatures are written out (using the same basic notation you might see in e.g. Haskell), but it's also there in its heavy use of functional programming terminology throughout its docs.

    Haskell-style types are quite nice, and functional programming jargon is very useful. However, they're also another hump to get over. Again: a tradeoff.

    By opting for type notation that TS developers are already familiar with, and by focusing on what various functions do rather than the usual FP names for them, True Myth aims at people just coming up to speed on these ideas.

    The big win for Folktale over True Myth is Fantasy Land compatibility.

  3. True Myth's API aims to be more idiomatic as JavaScript/TypeScript, with a couple differences in particular worth calling out:

    • function naming convention: True Myth uses PascalCase for types and camelCase for functions – so, new Just(5) and just(5), whereas FolkTale uses the capitals as function names for type constructors, i.e. Just(5), and does not support new.

    • ease of construction from nullable types: True Myth allows you to construct Maybe types from nullable types with Maybe.of, because JS is full of null and undefined, and allowing Maybe.of to handle them makes it easier to be sure you're always doing the right thing.

      Folktale's Maybe.of only allows the use of non-nullable types, and requires you to use Maybe.fromNullable instead. This isn't unreasonable, but it dramatically decreases the convenience of integration with existing JS codebases or interfacing with untyped JS libraries.

  4. Folktale also aims to provide a larger suite of types and functions to use – though much smaller than lodash – including a number of general functions, concurrency, general union types, and more. True Myth intentionally punts on those concerns, assuming that most consumers are already using a library like Lodash or Ramda, and are comfortable with or prefer using e.g. Promises for concurrency, and aiming to be easy to integrate with those instead.

Sanctuary?

Sanctuary has many of the same goals as True Myth, but is much more focused on the expectations and patterns you'd see in Haskell or PureScript or similar languages. Its API and True Myth's are much less similar than Folktale and True Myth's are, as a result – the underlying details are often similar, but the names are nearly all different. A few of the major contrasts:

  1. True Myth is TypeScript-first, which means that it assumes you are using TypeScript if you're aiming for rigorous type safety.

    By contrast, Sanctuary is a JavaScript-first library, with runtime checking built in for its types. Sanctuary's TypeScript support is in progress, but will for the foreseeable future remain add-on rather than first-class. (Sanctuary does allow you to create a version of the module without the runtime checking, but it requires you to do this yourself.)

    There's value in both of these approaches, so True Myth aims to take advantage of the compilers and play in a no-runtime-cost space.

    If you want a JS-focused (rather than TS-focused) library which will help you be safer without a compiler, you should definitely pick Sanctuary over True Myth. If you've already using TS, True Myth is a bit nicer of an experience.

  2. True Myth aims to keep functional programming jargon to a minimum and to use TypeScript type notation throughout its docs as well as in its implementation.

    Sanctuary is aimed squarely at people who are already extremely comfortable the world of strongly-typed, pure functional programming languages. This is particularly evident in the way its type signatures are written out (using the same notation you would see in Haskell or PureScript), but it's also present in Sanctuary's heavy use of functional programming terminology throughout its docs.

    Haskell- and Purescript-style types are quite nice, and the functional programming jargon is very useful. However, they're also another hump to get over. Again: a tradeoff.

    By opting for type notation that TS developers are already familiar with, and by focusing on what various functions do rather than the usual FP names for them True Myth aims at people just coming up to speed on these ideas.

    The big win for Sanctuary over True Myth is Fantasy Land compatibility, or familiarity if coming from a language like Haskell or PureScript.

  3. True Myth's API aims to be more idiomatic as JavaScript/TypeScript, with a one difference in particular worth calling out: the function naming convention. True Myth uses PascalCase for types and camelCase for functions – so, new Just(5) and just(5), whereas Sanctuary uses the capitals as function names for type constructors, i.e. S.Just(5), and does not support new.

  4. Sanctuary also aims to provide a much larger suite of functions, more like Ramda, but with Haskell- or PureScript-inspired type safety and sophistication. True Myth intentionally punts on those concerns, assuming that most consumers are already using a library like Lodash or Ramda and aiming to be easy to integrate with those instead.

What's with the name?

For slightly quirky historical reasons, libraries which borrow ideas from typed functional programming in JavaScript often use names related to the phrase "fantasy land" – especially Fantasy Land itself and Folktale.

"True Myth" leans on that history (and serves, hopefully, as a respectful nod to Folktale in particular, as both Folktale and Sanctuary are huge inspirations for this library), and borrows an idea from J.R.R. Tolkien and C.S. Lewis: what if all myths appeal to us because they point ultimately at something true – and what if some story with the structure of a myth were true in history? It's a beautiful idea, and the name of this library was picked as an homage to it.

Footnotes

  1. Using terser 5.10.0 with --compress --mangle --mangle-props.

  2. Generated by running gzip -kq11 on the result of the terser invocation.

  3. This is just the sum of the previous lines. Real-world bundle size is a function of what you actually use, how your bundler handles tree-shaking, and how the results of bundling compresses. Notice that sufficiently small files can end up larger after compression; this stops being an issue once part of a bundle.

true-myth's People

Contributors

alantrick avatar bmakuh avatar buschtoens avatar chriskrycho avatar csantero avatar davidevmod avatar dependabot-preview[bot] avatar dependabot[bot] avatar jmartinezmaes avatar jrr avatar marcnq avatar marcuscemes avatar netaisllc avatar ombr avatar pixelhandler avatar royiro10 avatar screendriver avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

true-myth's Issues

Update supported Node versions

We currently support 10.* || >= 12.*; we should update this to support current LTSes. Also, document supported Node versions in the README.

Wrong item type when using Maybe.map

Hi, nice project. I'm using it to unwrap data from a graphql API however when I try to use the map method on the data the type is the array itself instead of the item in the array.

Here is an example of the data I get from the API.

const data = {
    camera: [
      {
        id: 1,
        name: 'camera1',
        location: { id: 1, name: 'location1' },
      },
      {
        id: 2,
        name: 'camera2',
        location: { id: 2, name: 'location2' },
      }
    ]
  }

This is the error I get using Maybe.map

 const locations = Maybe.of(data)
    .get('camera')
    .map(camera => camera.location); // Property 'location' does not exist on type 'CameraFragment[]'

Using Array.prototype.map

 const locations = Maybe.of(data)
    .get('camera')
    .unwrapOr([])
    .map(camera => camera.location); // type is correctly outputting the item CameraFragment

Right now I'm unwrapping it first then using Array.prototype.map to map it and it's working but I'm wondering why it's giving me the wrong type when I use Maybe.map. The types are generated using graphql-codegen so maybe that's the problem? Array.prototype.map works fine with them though. Am I missing something?

Serializing monads

Hi guys,

I have an unorthodox but really easy request. I hope you will find it reasonable enough.

It would be great if you could provide constants to access the data internals of Maybe and Result types (.value and .error properties).

Alternatively, you could add to the specs those data internals, so that if they change in the future, you consider it a breaking change and a SEMVER major release.

The reason is we need to deserialize monads and to do so we need to be coupled to these internals.

If you want more detail:

Context

We have a client with Redux that stores the app state in local storage, thus serializing all state. We want to store monads in the state. We know it is a bad practice to store classes in Redux, but Maybes and Results are data types that hold state, and they make much sense to be stored.

The Maybe and Result types serialize correctly to the following examples:

{variant: 'Just', value: 4}
{variant: 'Nothing'}
{variant: 'Ok', value: 4}
{variant: 'Err', error: 'hi!'}

We make our reducers detect the de-serialization action, and re-create the monads from the objects using Maybe.just(...), Maybe.nothing(), and so on.

The Problem

In order to de-serialize the monads, we need to be coupled to the object's internals: .variant, .value, and .error.

The Variant property is well specified in your docs, and in fact we are using the lib's constants Variant.Just, Variant.Nothing, etc. But the other internals are not specified (because they are internals), and as such are subject to possible change without notice.

If this change causes a major release, it allows this coupling because it is then our responsibility to keep our code consistent with the update.
If these internals are exposed through a constant, then you can change them without provoking a breaking change.

Why not a deserialize method

Deserializing involves type refinement from input source, it is a whole other field in itself, and it can devolve into libraries as complex as io-ts, so in my humble opinion, it doesn't make sense as a true-myth responsibility, but belonging to userland.

Thank you for your attention!

Idea: Result.try

Right now, to construct a Maybe you can use Maybe.of or Maybe.fromNullable which are convenience methods that allow you to either construct a Nothing if the value is null or undefined, or a Just if it's something else.

For Results, you must manually construct the Ok or the Err "manually." There is no convenience constructor akin to of. I propose a Result.try constructor that, as the name suggests, takes in a callback function to execute and if it executes successfully then you would construct an Ok or else if an exception is raised then an Err is constructed. Think of it as an expression-oriented version of try/catch.

Example:

import { Result } from 'true-myth'

const willItThrow = Result.try('Oh noes!!1', async () => {
  await fetchMeASammich()
})

What happens? If fetchMeASammich() fails (Maybe a network error? Maybe the server throws said sandwich across the room instead of hand-delivering it?), try will wrap that callback in a try/catch, catching the failure and constructing Result.Err('Oh noes!!1'). If fetchMeASammich() succeeds, try will return the return value of the callback wrapped in Result.Ok.

Motivation

Beyond the simple motivation of a single convenience constructor, the True Myth docs use a try/catch block as an example of tedious error handling code. Some codebases use try/catch blocks extensively, and so a streamlined, expression-oriented constructor like this would actually prove quite handy to simplify existing code and smooth the adoption curve for this lib.

Thoughts? Feedback welcome!

Idea: `Task`

Spitballing here a bit: I've been thinking about a type named Future or Task as a possibly-useful thing to add to the library. This is interesting to me in that it's really useful to have some tools in the space that Promise lives in—namely, asynchronous operations and computations—but with different tradeoffs than Promise made.

For those who might not have thought about what Promise does differently from, say, Result or Maybe before, there are a bunch of differences, some necessary consequences of how JS works and some less so. The ones that immediately come to mind for me:

  • Promise combines what True Myth's types call map and andThen into a single function: then. In terms of type signatures, the way to think about it is that map takes a function whose signature is (t: T) -> U and gives you back a Maybe<U>, and andThen takes a function whose signature is (t: T) -> Maybe<U>, and gives you back a Maybe<U>. The same thing goes for Result. Promise#then, however, can take either one. That is, if you give then a function with the signature (t: T) -> U, you'll get back a Promise<U>, and if you give then a function with the signature (t: T) -> Promise<U> you'll also get back a Promise<U>.

    This is a choice made for convenience and a sort of accident of history rolled into one, and I'm not at all interested in relitigating it. It is what it is in JavaScript today. More interesting to me is whether we can provide a better interface on top of it.

  • Promise to have two types in play—the return type and the type of the error that will show up in the catch arm—but unfortunately, the catch argument is (correctly) typed as any in TypeScript. (Properly, given TS 3.0, we’d probably do unknown, but TS sensibly tries to avoid backwards-incompatible changes in the standard library types.) This is because Promise#catch has to capture any thrown exception along with any rejection created with Promise.reject.

    As far as I can see this is a fundamental problem facing any kind of Task.fromPromise function, but in principle it’s solveable. You just need a type that takes the any (or better, unknown) type handed to the catch function and converts it to the desired error E type: (error: unknown) => E.

One other note: like Promise itself, and unlike Maybe and Result, a Task captures a kind of computation you can’t (synchronously) exit from! The whole point is to model asynchronous behaviors.

Move docs out of repo

Instead of having them at docs, set up an auto-publish mechanism, probably using gh-pages and a GitHub Actions Workflow for auto-publishing when changes land.

Question: safely convert data

I have a question of how to safely get or convert some data to another. I know the following code fails, but I was thinking something along these lines:

Maybe.of({a: "a"}).map(x => x.b)

Another question. Is the above code supposed to throw an error? Error: Tried to construct 'Just' with 'null' or 'undefined'

Issue #46 is related but not entirely the same, that is a feature request.

Why isn't curry1 a main export?

I tried to import curry like so ....

import {curry1} from "true-myth";

but found it isn't exported there, it is exported from utils though, like so,

import { curry1 } from "true-myth\src\utils";

Is there a reason curry isn't a main export?

Relying on typescript

First and foremost: Amazing project you got here! Keep up the good work!

I have a question regarding your info, that this library doesnt rely on runtime checks. What I dont understand is: say i write code with this library in typescript and someday 3 years in the future decide to stop using typescript, in which areas will i miss out with true-myth then? Which areas are only covered by ts? Example: if a maybe is nothing or result, thats checked in the runtime right, (because I cant think of a static way 😅)? So true-myth is not just a typescript "typing" layer without any runtime checks? I could use it with just js and still would profit? 🤔

I am referring to:

to have zero runtime cost beyond simple object construction and function invocation
to lean heavily on TypeScript to enable all of the above

Importing true-myth/result not possible

I’ve followed the README.md and tried the following import statement import Result, { err, map, ok, toString } from 'true-myth/result'; which fails with a runtime exception: Error: Cannot find module 'true-myth/result'.

The TypeScript compiler doesn’t complain because the respective .d.ts file exists, but there is no result.js in node_modules/true-myth nor any kind of lib mapping in the package.json.

Safely get values from a Map?

For Arrays you already have three great functions: head, last and find. But what about Map?

const myMap = new Map<string, string>();
myMap.get('foo'); // string | undefined

The only thing that I found was get but it seems that get() is just working for object literals.

forEach metod for Result

I've tried monet.js, which has forEach method for applying side-effects to wrapped value, but it breaks chaining.

With true-myth i am only able to do this only in imperative way:

const a = Result.ok("FOO");

if(a.isOk()) {
  console.log(a.value);
}

a.map((value) => value.toLowerCase()) // Result.ok("foo");

Or by returning same value in map:

Result.ok("FOO")
  .map((value) => {
    console.log(value);
    return value;
  })
  .map((value) => value.toLowerCase()) // Result.ok("foo")

I'd like to perform side-effects like this:

Result.ok("FOO")
  .forEach(console.log)
  .map((value) => value.toLowerCase()) // Result.ok("foo")

And the same thing for err:

Result.err("Error")
  .forEachErr(console.log)

Does not work with Embroider

An Ember app with true-myth does not build with Embroider v0.22.0.

There was a recent call to try out Embroider, so we did just that. One surprising result is that importing true-myth prevented our build from completing. We had to make a local copy of the library and changing all our imports.

Generate Dash docset

It'd be great if we could generate a Dash docset for this – I make heavy use of Dash myself, and would love this. (I've taken a stab at it once a long time ago, but got stuck and gave up as other things were more pressing.)

Handling async operations

Hi there, I'm new with functional programming so maybe this question may looks silly.
The thing is I recently started using this library, it seems great but I'm not sure how to deal with async operations.
Well actually after some time reading the docs and other fp stuff I'm realizing that there's no way to handle that with this library and I should use another library, e.g Fluture.

Is that so? I mean, for strict functional programming I have to use Ramda, a monad library like this one and another library for async operations?
That's a lot of API to learn there 😆

Or maybe I'm missing something and there's a way to do something like this:

type AsyncResult = Result<string, { readonly reason: string }>;

const asyncA = (): Promise<AsyncResult> => Promise.resolve(Result.ok('A'));
const asyncB = (value: string): Promise<AsyncResult> => Promise.resolve(Result.ok(value + 'B'));
const asyncC = (value: string): Promise<AsyncResult> => Promise.resolve(Result.ok(value + 'C'));

(await asyncA()).flatMap(asyncB).flatMap(asyncC); // Ok('ABC')

Thanks in advance!

UPDATE: After some attempts I solved it like this:

const resultA = await asyncA();
const resultB = resultA.isOk() ? await asyncB(resultA.value) : resultA;
const resultC = resultB.isOk() ? await asyncC(resultB.value) : resultB;
const result = resultC.unwrapOr('');
console.log(result); // 'ABC'

But I think there must be a better way...

Feature request: ability to get a null/undefined

Use case example - the ORM I'm using (Objection.js) wants a null or undefined to not update a field. In a situation where the field is optional (e.g. for an update), it would be nice to be able to do something like:

Person.patch({
  firstName: Maybe.of(request.firstName).unwrapOrNull()
})

In this case unsafelyUnwrap() would yield an undefined, which might work in this scenario, but it throws instead.

Thanks for taking a look and for the library (and New Rustacean)!

Add `Result.all`

I didn't add it when we originally added Maybe.all because it wasn't clear what the operation should be for the Err case, but it would be straightforward to add now, and worth doing so that you can have a structure-preserving transformation there. We should decide how to treat that scenario: we don't have a type class like Monoid available, so we can't do something clever like have a specialization over monoids that concatenates them but otherwise just pick the first one. We could special-case that way for something like Array, and/or we could simply allow the user to provide an accumulator-style callback.

Emulating the "do" notation for Maybe type (with functions).

I came up with an idea to implement a loose equivalent of haskell's do notation with functions.

Introduction

A "do" notation allows for much cleaner code, with all the values "unwrapped" from their monadic context. Under the hood, it's always a sequence of flatMap (also known as bind, chain or >>=) calls finished with a map call.

In JavaScript/TypeScript, an async/await is based on the same concept (there's even an excellent article about that - async/await is just the do-notation of the Promise monad. However, it is restricted to promises (or thenables) only. Scala calls it "for comprehensions".

Examples

In an ideal world, it could look like this (similar to haskell/scala):

const maybeAnAnswer = do {
  const t <- Maybe.Just(5);
  const u <- Maybe.Just(t + 11);
  yield (t + u) * 2;
}
expect(maybeAnAnswer).toEqual(Maybe.just(42));

Notice how t and u (unwrapped with <- operator) are available in every following line - they are "in scope" of the entire "do" block. Also, the last line has access to these values. Although it seems to be yielding a numeric value, it actually resolves to Maybe<number> - in that case the value returned is Just(42).

Now let's look at what happens if Nothing creeps in:

const maybeAnAnswer = do {
  const t <- Maybe.Just(5);
  const u <- Maybe.nothing(); // whoopsie!
  yield (t + u) * 2;
}
expect(maybeAnAnswer).toEqual(Maybe.nothing());

In this example we have a Nothing in the middle of our computations. But we're still safe! The last line (yield ...) was actually not executed at all and we ended up with a safe Maybe value, this time receiving Nothing.

Idea

Because this syntax will probably never make it to ECMAScript specification (or will it?), I decided to try to implement a similar feature using functions. Here's the first "do" block translated into a function:

const maybeAnAnswer = Maybe.do2(
  Maybe.just(5),
  (t: number) => Maybe.of(t + 11),
  (t: number, u: number) => (t + u) * 2
);
expect(maybeAnAnswer).toEqual(Maybe.just(42));

The value unwrapped from the first Maybe is available in every subsequent lambda. This means that the value of t parameter will be 5 in both following functions. The value unwrapped from the first lambda (t + 11) will become available under u parameter of the last function, which is an equivalent of the yield expression from previous examples.

Similarly:

const maybeAnAnswer = Maybe.do2(
  Maybe.just(5),
  (t: number) => Maybe.nothing(), // whoopsie!
  (t: number, u: number) => (t + u) * 2
);
expect(maybeAnAnswer).toEqual(Maybe.nothing());

Because one of the values to unwrap was Nothing, the result of the whole function call is Nothing as well.

That's cool, but why did you call it .do2?

Well, that's simply because of the limitations of the language.

In both haskell and scala one can use as many unwrapping expressions as she wants - these languages' compilers will figure out what's going on (that's probably also the case for JavaScript's async/await - nothing stops you from awaiting for as many promises as you could only imagine in a single block of code).

In this case, I have to explicitly say how many Maybe monads I'd like to unwrap - so do2 stands for two unwrapping "steps" (resulting in t and u values). I plan to add do3, do4 and do5, too (I think that five unwrapping steps should be more than enough for 99% of cases). Oh, and did I mention that it's also type-safe? 😉

When?

I'm working on it. My quick proof of concept proved to be working (I have already created do2 and do3 and I even have some unit tests for them), but it's going to take time to write some documentation.

Please, let me know what do you think about this idea!

Support TS 4.1+

A number of features (esp. around inference for all and tuple) break somewhere between TS 3.7.3 (current), likely because of the work done on the compiler to support inferring tuple types. It may be possible to reimplement both all and tuple in terms of those new TS features, but based on previous experiments it is non-trivial and the implementation may continue to be unsafe.

TS 4.1 and even 4.2 nightly work as desired out of the box; I was misremembering. The key was that they didn't allow us to solve some of the internal implementation issues I would have liked for them to solve, esp. around being able to infer the types correctly for Maybe.all and Maybe.tuple.

ES6 modules + Typescript in Node

Stemming from the convo in microsoft/TypeScript#8305

I don't think one can consume the submodules using TS in node (because they're in dist/ not in the root of the project). If this is the case, maybe we should clarify the documentation because it's a bit ambiguous as to what a ES6-module-friendly consumer is.

For example, doing this
index.ts

import Maybe from 'true-myth/maybe';

console.log(Maybe.of(0));

tsconfig.json

{
  "compileOptions": {
    "target": "es2015",    
    "moduleResolution": "node"
  },
  "files": ["index.ts"]
}

and then tsc && node index.js
yields: Error: Cannot find module 'true-myth/maybe'

So maybe adding another line to https://github.com/chriskrycho/true-myth#setup
like so:

const { Maybe, Result } = require('true-myth');
// or in Typescript in Node.js
import { Maybe, Result } from 'true-myth';

Another option:
Polluting the root of the directory with the js files 🤢

sourcemap path is wrong

(Emitted value instead of an instance of Error) Cannot find source file './/utils.ts': Error: Can't resolve './/utils.ts' in '/home/jenkins/workspace/build/node_modules/true-myth/dist/es'```
You can probably fix by simply setting baseUrl to an empty string in tsconfig.json.

errors in doc rendering

I was looking at your source code and the jsdoc comments and noticed that there were quite a few issues in the html output.

For instance: https://true-myth.js.org/modules/_maybe_.html#andthen

image

  1. The link for flatMap contains the text flatMap] or and links to the bind function on MDN.
  2. bind is not a link, but should link to the bind function on MDN.

This is according to the docs: https://github.com/true-myth/true-myth/blob/master/src/maybe.ts#L829-L835

image

  1. @typeparam tag definitions are not being rendered correctly (docs seem to pull from <T, U> vs the @typeparam tags

Another @typeparam example from fromResult

image

  1. The definition for E is no rendered under the Type parameters header, but is rendered above it with a typeparam tag.

I'm not sure if this is an issue with typedoc or with the syntax you are using.

5.1.1 regression: type inference for Result.ok

After updating from 5.1.0 to 5.1.1 we noticed a regression with Typescript not correctly inferring the types when we had Maybe.nothing() inside a Result.ok(). I've put together a minimal example to illustrate this:

5.1.1:

import { Maybe, Result } from 'true-myth';

function test(): Result<Maybe<string>, Error> {
  // The following doesn't typecheck
  return Result.ok(Maybe.nothing());
}
test.ts:7:3 - error TS2322: Type 'Result<Maybe<unknown>, Error>' is not assignable to type 'Result<Maybe<string>, Error>'.
  Type 'Ok<Maybe<unknown>, Error>' is not assignable to type 'Result<Maybe<string>, Error>'.
    Type 'Ok<Maybe<unknown>, Error>' is not assignable to type 'Ok<Maybe<string>, Error>'.
      Type 'Maybe<unknown>' is not assignable to type 'Maybe<string>'.
        Type 'Just<unknown>' is not assignable to type 'Maybe<string>'.
          Type 'Just<unknown>' is not assignable to type 'Just<string>'.
            Type 'unknown' is not assignable to type 'string'.

7   return Result.ok(Maybe.nothing());
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The workaround is to specify the type explicitly:

return Result.ok(Maybe.nothing<string>());

5.1.0 typechecks perfectly fine:

image

arrayTranspose does not exist

When you import a Maybe like this

import { Maybe } from 'true-myth';

let valid = [Maybe.just(2), Maybe.just('three')];
let allJust = Maybe.arrayTranspose(valid);

I get the error

Property 'arrayTranspose' does not exist on type 'typeof import("/Users/me/something/node_modules/true-myth/maybe")'

Maybe.get nested property on object

This is a feature request. Right now it is possible to get a first level property of a Maybe, but it would be really useful to access deeply nested properties.

E.g. Right now it is possible to do: maybeObj.get('title'), but I would like to be able to do: maybeObj.get('deep.property.a')

This would be really useful when receiving data from an IO operation where we don't know if it conforms to the data we expect.

[5.0.0] No unsafelyUnwrap anymore?

I can't find it anywhere in the docs anymore (except in one sentence in match) and it is also not available on Just instances and I can't import it anymore with import { unsafelyUnwrap } from 'true-myth/maybe';. It is also not mentioned in the release notes.

Was it removed or is it accidentally not exported anymore?

Broken source maps

A user was trying to use true-myth with SvelteKit. SvelteKit is powered by Vite. If there's an error in the user's application, Vite will crash upon encountering a broken source map and it appears that true-myth may be triggering this behavior.

E.g. I see this in the index.d.ts file:

{"version":3,"file":"index.d.ts","sourceRoot":"./","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,cAAc,MAAM,SAAS,CAAC;AAC1C,oBAAY,KAAK,CAAC,CAAC,IAAI,OAAO,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAClD,eAAO,MAAM,KAAK,uBAAiB,CAAC;AAEpC,OAAO,KAAK,eAAe,MAAM,UAAU,CAAC;AAC5C,oBAAY,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,UAAU,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3D,eAAO,MAAM,MAAM,wBAAkB,CAAC;AAEtC,OAAO,KAAK,aAAa,MAAM,QAAQ,CAAC;AACxC,oBAAY,IAAI,GAAG,OAAO,QAAQ,EAAE,IAAI,CAAC;AACzC,eAAO,MAAM,IAAI,sBAAgB,CAAC"}

I think that's an invalid path in for sources. There's no index.ts in the package root. I see one in src/index.ts though

Typescript 3.6 "type is not assignable" error

Hi, I'm having an issue using mapOr and mapOrElse in TS 3.6. I'm using the latest version of true-myth. Consider the following program:

import { Maybe } from 'true-myth';

const len = (s: string) => s.length;

const val = Maybe.of("42");
const out = Maybe.mapOr(0, len, val);

console.log(val);
console.log(out);

This works with TS up to 3.5.3 but fails in 3.6 with the following error:

main.ts:6:28 - error TS2345: Argument of type '(s: string) => number' is not assignable to parameter of type '(t: string) => 0'.
  Type 'number' is not assignable to type '0'.

6 const out = Maybe.mapOr(0, len, val);
                             ~~~


Found 1 error.

One way to overcome this is to specify types explicitly, but I'd like to avoid it if possible:

const out = Maybe.mapOr<string, number>(0, len, val);

[Discussion] Task, Either, TaskEither

Hi @chriskrycho, huge admirer of your (and the other contributors) work.

I've taken a load of your project structure, which is great and copied forked your Maybe class but added three others: Task, Either and TaskEither.

I've literally just finished this second so it's still pretty early days and I don't plan to invest much in making the package usable by others, it's just got some stuff I wanted. They're much more lightweight than your two as well, as I've only focused on what I considered essential.

However, due to how similar these are in coding style and implementation I was wondering if you'd be interested in me drafting a PR around one or more of the additions? It wouldn't take too much work on my part. Got 100% test coverage, just need to add some doc-friendly comments and would have to potentially adapt task to use Result instead as that might make more sense. Plus whatever else you felt was needed.

Anywhoo, you can have a look here if you want to check it out. If you think there's something useful there let me know :)

Cheers

ap() and currying

I've tried to follow the doc examples of ap() and lodash's curry(), but run into type problems:

import { curry } from 'lodash';
import { Result } from 'true-myth';

const add = (a: number, b: number): number => a + b;
const curriedAdd = curry(add);

console.log(
  Result.of(curriedAdd)
    .ap(Result.of(1)).ap(Result.of(5))
    .unsafelyUnwrap()
);

const merge3Strs = (a: string, b: string, c: string): string => `${a} ${b} ${c}`;
const curriedMerge = curry(merge3Strs);

console.log(
  Result.of<typeof curriedMerge, string>(curriedMerge)
    .ap(Result.of('a')).ap(Result.of('b')).ap(Result.of('c'))
    .unsafelyUnwrap()
);

The first usage has:

The 'this' context of type 'Result<CurriedFunction2<number, number, number>, unknown>' is not assignable to method's 'this' of type 'Result<(a: number) => number, unknown>'.

and the second:

The 'this' context of type 'Result<CurriedFunction3<string, string, string, string>, string>' is not assignable to method's 'this' of type 'Result<(a: string) => string, string>'.

but the code works correctly in both places.

Has lodash's typing changed so that it's now incompatible, or is there something wrong here?

Module '"./utils"' has no exported member '_Brand'

When I upgraded our application from 4.0.0 to the 4.1.0 version of true-myth, my Typescript (4.1.3) project fails to build with tsc.

$ tsc --build
../../node_modules/true-myth/unit.d.ts:1:10 - error TS2305: Module '"./utils"' has no exported member '_Brand'.

1 import { _Brand } from './utils';
           ~~~~~~

In my node_modules/true-myth/unit.d.ts file:

import { _Brand } from './utils';
/**
  The `Unit` type exists for the cases where you want a type-safe equivalent of
  `undefined` or `null`. It's a concrete instance, which won't blow up on you,
  and you can safely use it with e.g. [`Result`](../modules/_result_.html)
  without being concerned that you'll accidentally introduce `null` or
  `undefined` back into your application.
 */
export declare const Unit: _Brand<"unit">;
export declare type Unit = typeof Unit;
export default Unit;
//# sourceMappingURL=unit.d.ts.map

In node_modules/true-myth/utils.d.ts:

/** @internal */
export {};
//# sourceMappingURL=utils.d.ts.map

When I look at the 4.0.0 version, unit.d.ts is the same, but utils.d.ts contains:

/**
 * Check if the value here is an all-consuming monstrosity which will consume
 * everything in its transdimensional rage. A.k.a. `null` or `undefined`.
 */
export declare const isVoid: (value: any) => value is null | undefined;
export declare function curry1<T, U>(op: (t: T) => U, item?: T): U | ((t: T) => U);
export declare type AndThenAliases = 'andThen' | 'chain' | 'flatMap';
export declare class _Brand<Tag extends string> {
    private _brand;
    constructor(t: Tag);
}
//# sourceMappingURL=utils.d.ts.map

React native Issues

Im seeing a few errors when trying to use this in a react native project that I wanted to report.

warn Package true-myth has been ignored because it contains invalid configuration. Reason: Package subpath './package.json' is not defined by "exports" in /node_modules/true-myth/package.json`.
 This seems to be fixed just by adding  `"./package.json": "./package.json",` in the exports section of the package.json in node_modules. 
LogBox.js:173 Require cycle: node_modules/true-myth/dist/result.js -> node_modules/true-myth/dist/maybe.js -> node_modules/true-myth/dist/result.js

Require cycles are allowed, but can result in uninitialized values. Consider refactoring to remove the need for a cycle.

It seems to be working otherwise though.
Node: 16.13.0
NPM: 8.1.0

TODO: move to org

Last week, I created a parent org for True Myth implementations, since some colleagues are building out a C♯ translation. Let's migrate this there!

  • update package.json
  • update any references throughout docs
  • move the repo

maybefy, a function that wraps an async function to make it return a Result

Curious what you think of this pattern. This function takes an async function and returns another function that resolves the Promise and wraps the resolved output inside a Result. Type information for params and returned values is preserved.

export function maybefy<F extends (...args: any[]) => Promise<any>>(fn: F) {
    type PromisedGeneric<P> = P extends Promise<infer G> ? G : never;
    type Promised = PromisedGeneric<ReturnType<F>>;

    return async (...args: Parameters<F>): Promise<Result<Promised, Error>> => {
        try {
            const result = await fn(...args);
            return result === null || result === undefined
                ? Result.err(Error("N/A"))
                : Result.ok(result);
        } catch (e) {
            return Result.err(e);
        }
    };
}

Use case: write code that interacts with APIs in a classical way instead of having it directly return Result.ok or Result.err.

Applicative interface is wrong

Hello, why your interface differ of Folktale maybe in applicative.
https://github.com/folktale/data.maybe/blob/master/lib/maybe.js#L206

Your suggestion:

const state: any = {
  b: {}
}
const a = Maybe.of(state).get('b').map(() => (num: number) => num * num)
const b = Maybe.of(234)
console.log(a.ap(b))

But I can't use that as chain.
I could do such:

const indentity = <T>(a: T): T => a
const state: any = {
  b: {}
}
const a = Maybe.of(state).get('b').map(() => (num: number) => num * num).unwrapOr(indentity)
const b = Maybe.of(234)
console.log(b.map(a))

But it's a crutch.
How it must be actually:

const state: any = {
  b: {}
}
const a = Maybe.of(state).get('b').map(() => (num: number) => num * num)
const c = Maybe.of(state).get('b').get('c').map(() => (num: number) => num * num)
const b = Maybe.of(234)
console.log(b.ap(a).ap(c))

Result.ok() gone in 5.x

We're upgrading from 4.1.1 to 5.1.0 and one large change we needed to do in our codebase was to replace Result.ok() with Result.ok(Unit) or else we were getting:

Error: Tried to construct `Ok` with `null`. Maybe you want `Maybe.Nothing`?

Two things I'd like to ask:

  1. Why is this a runtime check instead of a compile time check? The .d.ts files contain export declare function ok<T, E>(): Result<Unit, E>; so all our code typechecked, but it blows up in runtime. If this is not a function call users of the library should not make then I think ideally it shouldn't typecheck.
  2. Can we get this convenience method back? One of the nice things about true-myth is that Result, Maybe, and Unit types are nicely connected to each other and this was one of the nicenesses. On using Maybe instead of Result: I think Maybe<ErrorType> is not as clear as Result<Unit, ErrorType> in terms of what the code is doing.

Thanks!

Idea: `Lift`

Hi and thanks for this library!

It would be nice to have a lift function in the library that ensures a value T is a Maybe, without creating a Maybe<Maybe>:

lift<T>(a: T | Maybe<T>): Maybe<T> {
    return Maybe.isInstance( value )
        ? value
        : Maybe.of( value )
}

Input wanted: TS version support

The latest PR I merged (#21) depends on the recently-released TS 3.1. In other TS projects I help maintain, we have an official "at least the current and previous release of TS." Right now, for example, that would entail supporting 3.1 and 3.0, but not (necessarily) 2.9 or earlier. In those projects, we do not usually intentionally break consumers of earlier versions; it's more that we don't adopt new type system features etc. until we're at least one release further along.

(As to why only a couple recent versions rather than semver… TS itself ignores semver entirely and introduces breaking changes with most point releases. While this policy itself drives me slightly crazy, it's what we have to work with.)

As usage is starting to grow a bit, I'd really like to hear if that policy will work for people using the library, or if longer support is important for some reason.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.