In-House MathSwe-TS-and-Client Libraries | MathSwe Ops Services (2024/09/15)

In-House MathSwe-Ts-and-Client Libraries | MathSwe Ops Services (2024/09/15)

It provides two new in-house libraries in the MathSwe Ops Services application that will become MathSwe standard libraries for any TypeScript and server projects. While MathSwe-TS leverages the FP-TS library to add extended support for FP, MathSwe-Client will be standard for server applications.


Add in-house libs mathswe-ts, mathswe-client

Sep 15: PR #2 merged into by tobiasbriones

The mathswe-ts/adt module defines the **general pattern matching for sum types **. You also must follow manual guides to correctly build a sum type in TypeScript, even with fp-ts and mathswe-ts.

describe("Lazy Rich SumType", () => {
    type Shape = { tag: "Point" } | { tag: "Circle", radius: number };

    const point: Shape = { tag: "Point" };
    const circle: Shape = { tag: "Circle", radius: 1 };

    it("computes the area via matching and destructure", () => {
        type Circle = { radius: number };

        const circleArea
            = ({ radius }: Circle) => Math.PI * Math.pow(radius, 2);

        const area = (shape: Shape): number => {
            const withShapeVariant = withMatchVariant<number>(shape);

            return pipe(
                shape,
                match({
                    Point: () => 0,
                    Circle: withShapeVariant(circleArea),
                }),
            );
        };

        expect(area(point)).toBe(0);
        expect(area(circle)).toBe(Math.PI);
    });
});
Sum Type Pattern Matching

Something the test doesn’t show is that you must create the data constructors manually to keep up with good practices. That is, to avoid the tag field in client code, which is supposed to be an implementation detail.

Notice that, when using pipe you give context to the match function, i.e. SumTypeMap<Shape, string>, so the pattern matching is exhaustive. If you call it separately, i.e. match({})(shape), it loses the sum type, i.e. SumTypeMap<SumType, string>, so the generic type weakens from Shape to SumType.

You must apply the withMatchVariant higher-order function to pass your branch function to it. Your branch function, for example, circleArea, will define the type of the branch. In that case, you tell your withShapeVariant partial function that the type of the branch argument is Circle since it performs a cast under the hood, so you must define this type correctly to match successfully the branch.

As you can see, the branch function circleArea defines the { radius }: Circle param type that the withShapeVariant partial function will use to convert the original sum type to the actual variant value.

Finally, the matching is lazy for correctness. If you pass an eager map to match, it will execute all the variants automatically. Further, it would fail at runtime because it’d pass the same argument to all the different branch functions without discrimination.

Conversely, the mathswe-ts/enum module allows you to match plain sum type branches eagerly.

describe("PlainEnum", () => {
    type Color = { tag: "Red" } | { tag: "Green" } | { tag: "Blue" };

    const red: Color = { tag: "Red" };
    const green: Color = { tag: "Green" };
    const blue: Color = { tag: "Blue" };

    it("should pipe match enum variants with type safe exhaustive map", () => {
        const label = (color: Color) => pipe(
            color,
            matchPlain({
                Red: "red-variant",
                Green: "green-variant",
                Blue: "blue-variant",
            }),
        );

        expect(label(red)).toBe("red-variant");
        expect(label(green)).toBe("green-variant");
        expect(label(blue)).toBe("blue-variant");
    });
});
Matching Plain Sum Type Branches

The matchPlain function of the enum module is eager, and you must not use matchPlain for non-trivial sum types.

Notice that, fp-ts doesn’t have a general pattern-matching solution, only ad-hoc matching functions for specific types, like Either. Therefore, I took the exciting challenge to solve it, as per the language limitations since it’s better to have some kind of pattern matching than none.

Finally, the mathswe-ts/string module defines some type-classes to start implementing them in TS programs as a standard practice (like the Rust way).

export interface ToString<T> {
    toString(value: T): string;
}

export interface FromString<T> {
    fromString(string: string): Either<string, T>;
}
Type Classes in the String Module

The module mathswe-ts/require provides a method requireRight to unsafe unwrap of Either values. It throws an Error if the given Either is Left, or else returns its Right value. It’s useful for testing when you’re sure a value is always Right, or must be Right to pass the test.

The current MathSwe-TS library supports sum-type general pattern matching, which is lazy, but also for eager matching for plain branches without fields. It further defines some type-classes for Strings and a method for unsafe unwrapping of Either.


The mathswe-client/domain/domain module defines the ToDomainName type class to convert any domain to a string domain name. It also defines the Allowed sum type to grant access to those domains.

export interface ToDomainName<T> {
    toDomainName(domain: T): string;
}

export type Allowed
    = { tag: "FullAccess" }
    | { tag: "PartialAccess", values: string[] }
Domain Module
export type MathSwe
    = "MathSweCom"
    | "MathSoftware"
    | "MathSoftwareEngineer";
MathSwe Module
export type ThirdParty = "GitHubCom";
ThirdParty Module

The mathswe-client/req/http module defines HTTP abstractions.

export type Hostname = {
    domainName: string,
    subdomain: string,
}

export type Path = string[];

export type SecureUrl = {
    hostname: Hostname,
    path: Path,
}
HTTP Module

The mathswe-client/req/origin path contains modules with rules defining if a given request origin has access to the server.

export type OriginDomain
    = { tag: "MathSweDomain", mathswe: MathSwe }
    | { tag: "ThirdPartyDomain", thirdParty: ThirdParty };

export type OriginPath = Path;

export type Origin = {
    domain: OriginDomain,
    path: OriginPath,
    url: SecureUrl,
}
Modules OriginDomain and Origin
describe("newOriginPathFromDomain", () => {
    it("should return path for FullAccess domain", () => {
        const domain: OriginDomain = pipe(
            "mathswe.com",
            originDomainFromString.fromString,
            requireRight,
        );

        const path = "/any-path";
        const expected = right(pipe(path, newPathFromString, requireRight));
        const result = pipe(path, newOriginPathFromDomain(domain));

        expect(result).toEqual(expected);
    });

    it("should return an error for a restricted domain path", () => {
        const domain: OriginDomain = pipe(
            "github.com",
            originDomainFromString.fromString,
            requireRight,
        );

        // github.com/restricted-path random user
        const path = "restricted-path";
        const expected = left(`Path ${ path } of domain ${ domain } is restricted.`);
        const result = pipe(path, newOriginPathFromDomain(domain));

        expect(result).toEqual(expected);
    });

    it("should accept partially allowed path", () => {
        const domain: OriginDomain = pipe(
            "github.com",
            originDomainFromString.fromString,
            requireRight,
        );

        // github.com/mathswe organization
        const path = "mathswe";
        const expected = right(pipe(path, newPathFromString, requireRight));
        const result = pipe(path, newOriginPathFromDomain(domain));

        expect(result).toEqual(expected);
    });
});

describe("newOriginFromUrl", () => {
    it(
        "should return a valid Origin when domain and path are allowed",
        () => {
            const expectedUrl: SecureUrl = pipe(
                "https://mathswe.com/valid-path",
                newUrlFromString,
                requireRight,
            );

            const result = newOriginFromUrl(expectedUrl);
            const { domain, path, url } = requireRight(result);
            const expectedDomain = mathSweDomain("MathSweCom");
            const expectedPath = pipe(
                "valid-path",
                newPathFromString,
                requireRight,
            );

            expect(domain).toEqual(expectedDomain);
            expect(path).toEqual(expectedPath);
            expect(url).toEqual(expectedUrl);
        },
    );

    it("should return an error if the domain is disallowed", () => {
        const result = newOriginFromString("example.com/some-path");

        expect(isLeft(result)).toBe(true);
    });
});
Ensuring Access to Allowed Origins Only

Finally, the mathswe-client/req/client/client-req module provides a function to get the Origin of a Request. Recall that the existence of an Origin value means the underlying request is allowed.

describe("getOrigin", () => {
    it(
        "should return Right(Origin) when a valid Origin header is provided",
        () => {
            const mockRequest = {
                headers: {
                    get: (key: string) =>
                        key === "Origin"
                        ? "https://mathswe.com"
                        : null,
                },
            } as Request;

            const result = getOrigin(mockRequest);
            const expectedOrigin = pipe(
                "https://mathswe.com",
                newOriginFromString,
                requireRight,
            );

            expect(result).toEqual(right(expectedOrigin));
        },
    );
});
Parsing HTTP Request to Allowed Origin

The current MathSwe-Client library defines known MathSwe domains with full access and third-party domains, like GitHub, which can only have partial access to some paths. It defines HTTP concepts, like SecureUrl, so that DSL ensures only allowed origins according to domain rules.


The new features of MathSwe-TS complement general and exhausting (when piping) pattern matching for sum types. While it’s not perfect, it works better than expected, complimenting FP-TS, which only has ad-hoc matching for monads like Option and Either.

Further, the new MathSwe-Client supports Origin rules to only accept HTTP requests from accepted domains and paths.

The purpose of these two libraries is to become MathSwe standards for TypeScript projects.