gcanti/io-ts

[QUESTION] Best way to validate non-enumerable properties

albertodiazdorado opened this issue ยท 1 comments

๐Ÿš€ Feature request

Current Behavior

Validating non-enumerable properties is cumbersome, since they are not taken into account by io-ts codecs (to my knowledge):

import * as t from "io-ts";
import { isLeft } from "fp-ts/Either";

const e = new Error("ENOENT");
const validation = t.type({ message: t.string }).decode(e);
console.log(isLeft(validation)); // true
console.log(e.message); // ENOENT

Desired Behavior

Validating non-enumerable properties is easy.

Suggested Solution

I am not sure whether the solution is...

  1. To have io-ts codecs also check non-enumerable properties by default
  2. To have an additional wrapper that explicitely checks non-enumerable properties, a la
    const errorCodec = t.nonEnumerable(t.type({ message: t.string }));
  3. To install a third party library to turn non-enumerable properties into enumerable properties or to code said function yourself:
    const getOwnProperties = (e: unknown) =>
      Object.getOwnPropertyNames(e).reduce(
        (error, property) => ({ ..error, [property]: Object.getOwnPropertyDescriptor(e, property)?.value }),
       {}
     );
  4. To include said function in fp-ts

Who does this impact? Who is this for?

Advance TS users who do not want to use any in catch blocks

Describe alternatives you've considered

Alternative 1: Disable TypeScript

declare function throws(): never;
declare function handleEnoent(): void;
declare function handleGenericError(): void;

try {
  throws();
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
  if (error.code === "ENOENT") {
    handleEnoent();
  } else {
    handleGenericError();
  }
}

Alternative 2: Manual validation

declare function throws(): never;
declare function handleEnoent(): void;
declare function handleGenericError(): void;

class ErrorWithCode extends Error {
  constructor(msg: string, readonly code: unknown) {
    super(msg);
  }
}

const isErrorWithCode = (e: unknown): e is ErrorWithCode =>
  typeof e === "object" && e !== null && "code" in e;

try {
  throws();
} catch (error: unknown) {
  if (isErrorWithCode(error) && error.code === "ENOENT") {
    handleEnoent();
  } else {
    handleGenericError();
  }
}

Alternative 3: io-ts cumbersome validation

import * as t from "io-ts";
declare function throws(): never;
declare function handleEnoent(): void;
declare function handleGenericError(): void;

const getOwnProperties = (e: unknown) =>
  Object.getOwnPropertyNames(e).reduce(
    (error, property) => ({
      ...error,
      [property]: Object.getOwnPropertyDescriptor(e, property)?.value
    }),
    {}
  );

const errorCodec = t.type({ message: t.string, code: t.string });

try {
  throws();
} catch (error: unknown) {
  const validation = errorCodec.decode(getOwnProperties(error));

  if (validation._tag === "Right" && validation.right.code === "ENOENT") {
    handleEnoent();
  } else {
    handleGenericError();
  }
}

Additional context

Hey @gcanti , huge fan here! I love fp-ts :D
I would be willing to implement this feature if you would like to have it in io-ts or fp-ts

Your environment

As of October 28th 2022:

Software Version(s)
io-ts latest
fp-ts latest
TypeScript latest

Also a problem I have come across with dynamically defined properties that happen to be part of some interfaces