From 62648cca1c85f56e2b5b27d03955e8fe41fabf59 Mon Sep 17 00:00:00 2001 From: Piotr Hitori Bosak Date: Wed, 30 Jun 2021 13:04:35 +0200 Subject: [PATCH] https://github.com/woutervh-/typescript-is/issues/104 --- README.md | 68 ++++++++++++++++++++++++++++++++++++++ index.d.ts | 2 +- index.js | 13 ++++++-- test-fixtures/issue-104.ts | 21 ++++++++++++ test/issue-104.ts | 25 ++++++++++++++ tsconfig-test.json | 3 +- tsconfig.json | 3 +- 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 test-fixtures/issue-104.ts create mode 100644 test/issue-104.ts diff --git a/README.md b/README.md index 0631ff9..eb622f6 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,74 @@ new A().method(42) === 42; // true new A().method('42' as any); // will throw error ``` +### async and `Promise` returning methods +`AssertType` can also work correctly with `async` methods, returning promise rejected with `TypeGuardError` + +To enable this functionality, you need to emit decorators metadata for your TypeScript project. + +```json +{ + "compilerOptions": { + "emitDecoratorMetadata": true + } +} +``` + +Then `AssertType` will work with async methods and `Promise` returning methods automatically. +```typescript +import { ValidateClass, AssertType } from 'typescript-is'; + +@ValidateClass() +class A { + async method(@AssertType({ async: true }) value: number) { + // You can safely use value as a number + return value; + } + + methodPromise(@AssertType({ async: true }) value: number): Promise { + // You can safely use value as a number + return Promise.resolve(value); + } +} + +new A().method(42).then(value => value === 42 /* true */); +new A().method('42' as any).catch(error => { + // error will be of TypeGuardError type +}) +new A().methodPromise('42' as any).catch(error => { + // error will be of TypeGuardError type +}) +``` + +If you want to throw synchronously for some reason, you can override the behaviour using with `@AssertType({ async: false })`: +```typescript +import { ValidateClass, AssertType } from 'typescript-is'; + +@ValidateClass() +class A { + async method(@AssertType({ async: false }) value: number) { + // You can safely use value as a number + return value; + } +} + +new A().method(42).then(value => value === 42 /* true */); +new A().method('42' as any); // will throw error +``` + +If you cannot or don't want to enable decorators metadata, you still make AssertType reject with promise using `@AssertType({ async: true })` +```typescript +import { ValidateClass, AssertType } from 'typescript-is'; + +@ValidateClass() +class A { + async method(@AssertType({ async: true }) value: number) { + // You can safely use value as a number + return value; + } +} +``` + ## Strict equality (`equals`, `createEquals`, `assertEquals`, `createAssertEquals`) This family of functions check not only whether the passed object is assignable to the specified type, but also checks that the passed object does not contain any more than is necessary. In other words: the type is also "assignable" to the object. This functionality is equivalent to specifying `disallowSuperfluousObjectProperties` in the options, the difference is that this will apply only to the specific function call. For example: diff --git a/index.d.ts b/index.d.ts index be09c6d..74057be 100644 --- a/index.d.ts +++ b/index.d.ts @@ -125,7 +125,7 @@ export function createAssertEquals(): (object: any) => T; new A().method('0' as any); // will throw an error ``` */ -export function AssertType(): (target: object, propertyKey: string | symbol, parameterIndex: number) => void; +export function AssertType(options?: { async: boolean }): (target: object, propertyKey: string | symbol, parameterIndex: number) => void; /** * Overrides methods in the target class with a proxy that will first validate the argument types. diff --git a/index.js b/index.js index 6b657b2..c348fe7 100644 --- a/index.js +++ b/index.js @@ -45,7 +45,11 @@ function AssertType(assertion, options = {}) { require('reflect-metadata'); return function (target, propertyKey, parameterIndex) { const assertions = Reflect.getOwnMetadata(assertionsMetadataKey, target, propertyKey) || []; - assertions[parameterIndex] = { assertion, options }; + if(Reflect.getOwnMetadata('design:returntype', target, propertyKey) === Promise) { + assertions[parameterIndex] = { assertion, options: Object.assign({ async: true }, options) }; + } else { + assertions[parameterIndex] = { assertion, options }; + } Reflect.defineMetadata(assertionsMetadataKey, assertions, target, propertyKey); }; } @@ -64,7 +68,12 @@ function ValidateClass(errorConstructor = TypeGuardError) { } const errorObject = assertions[i].assertion(args[i]); if (errorObject !== null) { - throw new errorConstructor(errorObject, args[i]); + const errorInstance = new errorConstructor(errorObject, args[i]); + if(assertions[i].options.async) { + return Promise.reject(errorInstance); + } else { + throw errorInstance; + } } } return originalMethod.apply(this, args); diff --git a/test-fixtures/issue-104.ts b/test-fixtures/issue-104.ts new file mode 100644 index 0000000..e82f646 --- /dev/null +++ b/test-fixtures/issue-104.ts @@ -0,0 +1,21 @@ +import {AssertType, ValidateClass} from '../index'; + +@ValidateClass() +export class AsyncMethods { + async asyncMethod(@AssertType() body: { test: string }): Promise { + return true + } + async asyncMethodNoExplicitReturn(@AssertType() body: { test: string }) { + return true + } + promiseReturnMethod(@AssertType() body: { test: string }): Promise { + return Promise.resolve(true) + } + async asyncOverride(@AssertType({ async: false }) body: { test: string }): Promise { + return true + } + promiseOrOtherReturnMethod(@AssertType() body: { test: string }): Promise | boolean{ + return Promise.resolve(true) + } +} + diff --git a/test/issue-104.ts b/test/issue-104.ts new file mode 100644 index 0000000..88ed42c --- /dev/null +++ b/test/issue-104.ts @@ -0,0 +1,25 @@ +import * as assert from 'assert'; +import {AsyncMethods} from '../test-fixtures/issue-104'; + +describe('@ValidateClass(), @AssertType()', () => { + it('should return rejected promise for async methods', () => { + const instance = new AsyncMethods() + assert.rejects(instance.asyncMethod({invalid: 123} as any)) + }) + it('should return rejected promise for async methods with not explicit return type', () => { + const instance = new AsyncMethods() + assert.rejects(instance.asyncMethodNoExplicitReturn({invalid: 123} as any)) + }) + it('should return rejected promise for methods returning promise', () => { + const instance = new AsyncMethods() + assert.rejects(instance.promiseReturnMethod({invalid: 123} as any)) + }) + it('should throw synchronously if { async: false } option is set', () => { + const instance = new AsyncMethods() + assert.throws(() => instance.asyncOverride({invalid: 123} as any)) + }) + it('should throw synchronously method may return something other than promise', () => { + const instance = new AsyncMethods() + assert.throws(() => instance.promiseOrOtherReturnMethod({invalid: 123} as any)) + }) +}) diff --git a/tsconfig-test.json b/tsconfig-test.json index 38ee6cc..81db5db 100644 --- a/tsconfig-test.json +++ b/tsconfig-test.json @@ -5,6 +5,7 @@ "lib": [ "es6" ], + "emitDecoratorMetadata": true, "experimentalDecorators": true, "noImplicitAny": true, "noUnusedLocals": true, @@ -20,4 +21,4 @@ "include": [ "test" ] -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index ffc920c..3a32677 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "lib": [ "es6" ], + "emitDecoratorMetadata": true, "experimentalDecorators": true, "noImplicitAny": true, "noUnusedLocals": true, @@ -17,4 +18,4 @@ "include": [ "src" ] -} \ No newline at end of file +}