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
}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
-
What do you want to use this for?
Documenting what types of errors a function may throw. -
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. -
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