I've been trying all day to make the infering work when using Error Classes. I have been developing the Result Pattern so that I could track the possible exceptions that a function / method can return, this is the result I have so far. It surely is a very good result, it works perfectly when not using the Error Classes, I don't even need to explicitly type the return of the function / method. But I am trying to get the same result with the Error Classes, this is what I got so far:
In the end of the file there are 2 example, one of them is a working example that uses object literals and the other one is a not working example that uses Error Classes
Link to the TS Playground
export type Result<V, E> = Ok<V, E> | Fail<V, E>;
interface IResult<V, E> {
readonly isOk: boolean;
readonly isFail: boolean;
readonly value: V | E;
map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E>;
flatMap<NextV>(mapFn: (value: V) => Result<NextV, E>): Result<NextV, E>;
mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE>;
flip(): Result<E, V>;
}
export class Ok<V = never, E = never> implements IResult<V, E> {
public readonly isOk = true as const;
public readonly isFail = false as const;
public constructor(public readonly value: V) {}
public map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E> {
return new Ok(mapFn(this.value));
}
public flatMap<NextV>(
mapFn: (value: V) => Result<NextV, E>,
): Result<NextV, E> {
return mapFn(this.value);
}
public mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE> {
return new Ok(this.value);
}
public flip(): Result<E, V> {
return new Fail(this.value);
}
}
export class Fail<V = never, E = never> implements IResult<V, E> {
public readonly isOk = false as const;
public readonly isFail = true as const;
public constructor(public readonly value: E) {}
public map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E> {
return new Fail(this.value);
}
public flatMap<NextV>(
mapFn: (value: V) => Result<NextV, E>,
): Result<NextV, E> {
return new Fail(this.value);
}
public mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE> {
return new Fail(mapFn(this.value));
}
public flip(): Result<E, V> {
return new Ok(this.value);
}
}
export namespace ResultUtils {
type OkValues<T extends Result<unknown, unknown>[]> = {
[K in keyof T]: T[K] extends Result<infer V, unknown> ? V : never;
};
type FailValues<T extends Result<unknown, unknown>[]> = Array<
T[number] extends Result<unknown, infer E> ? E : never
>;
export function combine<T extends Result<unknown, unknown>[]>(
...results: T
): Result<OkValues<T>, FailValues<T>> {
const fails = results.filter(
(r): r is Fail<unknown, FailValues<T>[number]> => r.isFail,
);
if (fails.length > 0) {
const failValues: FailValues<T> = fails.map((f) => f.value);
return new Fail(failValues);
}
const okValues: OkValues<T> = results.map(
(r) => r.value,
) as OkValues<T>;
return new Ok(okValues);
}
export function ok<V = never>(value: V) {
return new Ok(value);
}
export function fail<E = never>(value: E) {
return new Fail(value);
}
export function voidOk(): Ok<void, never> {
return new Ok<void>(undefined);
}
}
// Example 1 - Using object literals as errors: WORKING NORMALLY
function ope1() {
if (Math.random() === 0) {
const a = new Fail({fail1: "fail1"});
// ^? const a: Fail<never, {fail1: string}>
return a;
}
if (Math.random()) {
const a = new Fail({fail2: "fail2"});
// ^? const a: Fail<never, {fail2, string}>
return a;
}
const a = new Ok({ok: "ok"});
// ^? const a: Ok<{ok: string}, never>
return a;
}
const res1 = ope1();
// ^? const res1: Fail<never, {fail1: string}> | Fail<never, {fail2, string}> | Ok<{ok: string}, never>
// Example 2 - Using Error classes: NOT WORKING AS EXPECTED
class GenericError extends Error {}
class DomainError extends GenericError {}
class InfrastructureError extends GenericError {}
class TestError extends DomainError {}
function ope2() {
if (Math.random() === 0) {
const a = new Fail(new TestError());
// ^? const a: Fail<never, TestError>
return a;
}
if (Math.random() === 0) {
const a = new Fail(new InfrastructureError());
// ^? const a: Fail<never, InfrastructureError>
return a
}
const a = new Ok({ok: "ok"});
// ^?const a: Ok<{ok: string}, never>
return a;
}
const res2 = ope2();
// ^? const res2: Fail<never, TestError> | Ok<{ok: string}, never>
This is my tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"noFallthroughCasesInSwitch": false,
"paths": {
"@src/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Do you guys have any idea of how to fix this?
I've been trying all day to make the infering work when using Error Classes. I have been developing the Result Pattern so that I could track the possible exceptions that a function / method can return, this is the result I have so far. It surely is a very good result, it works perfectly when not using the Error Classes, I don't even need to explicitly type the return of the function / method. But I am trying to get the same result with the Error Classes, this is what I got so far:
In the end of the file there are 2 example, one of them is a working example that uses object literals and the other one is a not working example that uses Error Classes
Link to the TS Playground
export type Result<V, E> = Ok<V, E> | Fail<V, E>;
interface IResult<V, E> {
readonly isOk: boolean;
readonly isFail: boolean;
readonly value: V | E;
map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E>;
flatMap<NextV>(mapFn: (value: V) => Result<NextV, E>): Result<NextV, E>;
mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE>;
flip(): Result<E, V>;
}
export class Ok<V = never, E = never> implements IResult<V, E> {
public readonly isOk = true as const;
public readonly isFail = false as const;
public constructor(public readonly value: V) {}
public map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E> {
return new Ok(mapFn(this.value));
}
public flatMap<NextV>(
mapFn: (value: V) => Result<NextV, E>,
): Result<NextV, E> {
return mapFn(this.value);
}
public mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE> {
return new Ok(this.value);
}
public flip(): Result<E, V> {
return new Fail(this.value);
}
}
export class Fail<V = never, E = never> implements IResult<V, E> {
public readonly isOk = false as const;
public readonly isFail = true as const;
public constructor(public readonly value: E) {}
public map<NextV>(mapFn: (value: V) => NextV): Result<NextV, E> {
return new Fail(this.value);
}
public flatMap<NextV>(
mapFn: (value: V) => Result<NextV, E>,
): Result<NextV, E> {
return new Fail(this.value);
}
public mapFails<NextE>(mapFn: (value: E) => NextE): Result<V, NextE> {
return new Fail(mapFn(this.value));
}
public flip(): Result<E, V> {
return new Ok(this.value);
}
}
export namespace ResultUtils {
type OkValues<T extends Result<unknown, unknown>[]> = {
[K in keyof T]: T[K] extends Result<infer V, unknown> ? V : never;
};
type FailValues<T extends Result<unknown, unknown>[]> = Array<
T[number] extends Result<unknown, infer E> ? E : never
>;
export function combine<T extends Result<unknown, unknown>[]>(
...results: T
): Result<OkValues<T>, FailValues<T>> {
const fails = results.filter(
(r): r is Fail<unknown, FailValues<T>[number]> => r.isFail,
);
if (fails.length > 0) {
const failValues: FailValues<T> = fails.map((f) => f.value);
return new Fail(failValues);
}
const okValues: OkValues<T> = results.map(
(r) => r.value,
) as OkValues<T>;
return new Ok(okValues);
}
export function ok<V = never>(value: V) {
return new Ok(value);
}
export function fail<E = never>(value: E) {
return new Fail(value);
}
export function voidOk(): Ok<void, never> {
return new Ok<void>(undefined);
}
}
// Example 1 - Using object literals as errors: WORKING NORMALLY
function ope1() {
if (Math.random() === 0) {
const a = new Fail({fail1: "fail1"});
// ^? const a: Fail<never, {fail1: string}>
return a;
}
if (Math.random()) {
const a = new Fail({fail2: "fail2"});
// ^? const a: Fail<never, {fail2, string}>
return a;
}
const a = new Ok({ok: "ok"});
// ^? const a: Ok<{ok: string}, never>
return a;
}
const res1 = ope1();
// ^? const res1: Fail<never, {fail1: string}> | Fail<never, {fail2, string}> | Ok<{ok: string}, never>
// Example 2 - Using Error classes: NOT WORKING AS EXPECTED
class GenericError extends Error {}
class DomainError extends GenericError {}
class InfrastructureError extends GenericError {}
class TestError extends DomainError {}
function ope2() {
if (Math.random() === 0) {
const a = new Fail(new TestError());
// ^? const a: Fail<never, TestError>
return a;
}
if (Math.random() === 0) {
const a = new Fail(new InfrastructureError());
// ^? const a: Fail<never, InfrastructureError>
return a
}
const a = new Ok({ok: "ok"});
// ^?const a: Ok<{ok: string}, never>
return a;
}
const res2 = ope2();
// ^? const res2: Fail<never, TestError> | Ok<{ok: string}, never>
This is my tsconfig.json
:
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"noFallthroughCasesInSwitch": false,
"paths": {
"@src/*": ["./src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Do you guys have any idea of how to fix this?
Share Improve this question asked Mar 15 at 5:28 CristoferCristofer 654 bronze badges1 Answer
Reset to default 0The problem is TS compares types structurally and doesn't care about class names, in your case TestError and InfrastructureError are identical for TS since having the same number of properties. To fix that you need to break that equality and introduce some difference like:
Playground
class InfrastructureError extends GenericError {
__isInfrastructureError?: never;
}
class TestError extends DomainError {
__isTestError?: never;
}