Formulaic Docs

Creating Custom Data Classes

You can use FP’s Literal class for successful data, but has many limitations.

To go beyond the default Literal class, you can define a custom class that extends DataFP (an abstract class from FP).

Example class created by extending DataFP
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { Equals, IsIn, IsJWT, IsString } from "class-validator";
import { DataFP } from "./DataFP";

export class LoginResponse extends DataFP<"LoginResponse", 200 | 201> {
  public static readonly kind = "LoginResponse"; (1)

  @ApiProperty() (2)
  @Equals("LoginResponse")
  @Expose()
  public override readonly kind: "LoginResponse"; (3)

  @ApiProperty()
  @IsIn([200, 201])
  @Expose()
  public override readonly status: 200 | 201; (3)

  @ApiProperty()
  @IsString()
  @Expose()
  public readonly id: string; (4)

  @ApiProperty()
  @IsJWT()
  @Expose()
  public readonly jwt: string; (4)

  public constructor(
    id: string,
    jwt: string,
    newRegistration: boolean = false,
  ) {
    super();
    this.kind = "LoginResponse";
    this.status = newRegistration ? 201 : 200;
    this.id = id;
    this.jwt = jwt;
  }
}
1 Defining a static kind property is used in some utilities, but is not required.
2 DataFP does not attach ApiProperty, Expose, or any validation decorators (unlike other FP classes) - if using OpenAPI or fp-interceptor you should declare ApiProperty and Expose.
3 You must define kind and status fields.
4 You may define any additional fields to represent your data, such as id and jwt.

Now the properties are exposed directly in the returned object, no need to go through data:

Example usage of custom class extending DataFP
const response = new LoginResponse("admin", "[jwt contents]");
expect(response.id).toBe("admin");

If an error might be returned, you can check that the value is a LoginResponse before accessing fields, or use a transformation like map or chain.

async function performLogin(
  username: string,
  password: string,
): Promise<LoginResponse | UnexpectedError<LoginResponse>> {
  return response;
}

const login = await performLogin("admin", "top-secret-password");
if(login.hasData) {
  console.log(`Success!  JWT: ${login.jwt}`);
} else {
  console.log("Failed login.");
}