microsoft/TypeScript

Type metadata / shadow types

mckravchyk opened this issue ยท 1 comments

Suggestion

๐Ÿ” Search Terms

type metadata info secondary

โœ… Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

โญ Suggestion

Create a type that in a union, has no effect on the result of the union (same as never), but can be retrieved using a special type (i.e. GetMetadata<T>).

type x = number | Metadata<unknown>; // type is: number
type metaX = GetMetadata<x>; // type is unknown

๐Ÿ“ƒ Motivating Example

This can be used to document errors which a function may throw.

Currently, Typescript allows to do something like the below, but there's an issue in that the Throws<> type pollutes the function return type which has to be manually eliminated by type casting - not very practical.

This is a full example of what I'm aiming at, from my comment in the issue #13219 (comment)

type Throws<T> = { throws: T };

type GetThrow<T> = T extends Throws<infer R> ? R : never;
type GetResult<T> = T extends Throws<unknown> ? never : T;

type GetFunctionThrow<T extends (...args: any[]) => any> = GetThrow<ReturnType<T>>;
type GetFunctionResult<T extends (...args: any[]) => any> = GetResult<ReturnType<T>>;

type CalcError = {
    code: "calc_error"
    message: string
}

type DivisionError = {
    code: 'division_error'
    message: string
}

function divide(a: number, b: number): number | Throws<DivisionError> {
    if (b === 0) {
        throw { code: "division_error", message: "Cannot divide by 0" } as DivisionError; 
    }

    return a / b;
}

function calc(a: number, b: number, operation: 'divide'): number | Throws<CalcError | DivisionError> {
    if (typeof a !== 'number' || typeof b !== 'number') {
        throw { code: "calc_error", message: "Required 2 number arguments" } as CalcError;
    }

    if (operation !== 'divide') {
        throw { code: "calc_error", message: "Unsupported operation" } as CalcError;
    }

    return divide(a,b);
}

let x: number;

try {
    // This casting makes this method hardly practical. If we had a metadata type, it would be a number by default.
    x = calc(5 ,6, 'divide') as GetFunctionResult<typeof calc>;
} catch (e: unknown) { // Catch-clause annotation must be any or unknown
    const err = e as GetFunctionThrow<typeof calc>; // CalcError | DivisionError
}

Playground

With the new special Metadata<T> utility type it would possible to store such information and access it with a special type GetMetadata<T> while the type like number | string | Metadata<unknown> would be just number | string.

๐Ÿ’ป Use Cases

  1. What do you want to use this for?
    Documenting what types of errors a function may throw.

  2. What shortcomings exist with current approaches?
    It's possible to use a helper type to eliminate the metadata from the union, but it's too verbose to be practical. It could also be stored in a separate type, but with classes, it would be away from the method body and also not practical enough to consider.

  3. What workarounds are you using in the meantime?
    I'm not documenting the types of errors a function may throw at all or I use tsdoc throws (but it does not allow to store type info).

i almost publish my lib https://github.com/kraftrio/kraftr/tree/develop/core/errors you can check it how i handled to store metadata, i created a eslint plugin too to enforce the usages (could have auto fix)
edit: i published my lib
you can try it here