Skip to content

Commit 6938d9d

Browse files
authored
Merge pull request #105 from HitoriSensei/fix/issue-104
#104 @AssertType rejects a promise when used in async methods
2 parents 68c0bf8 + 62648cc commit 6938d9d

File tree

7 files changed

+129
-4
lines changed

7 files changed

+129
-4
lines changed

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,74 @@ new A().method(42) === 42; // true
259259
new A().method('42' as any); // will throw error
260260
```
261261

262+
### async and `Promise` returning methods
263+
`AssertType` can also work correctly with `async` methods, returning promise rejected with `TypeGuardError`
264+
265+
To enable this functionality, you need to emit decorators metadata for your TypeScript project.
266+
267+
```json
268+
{
269+
"compilerOptions": {
270+
"emitDecoratorMetadata": true
271+
}
272+
}
273+
```
274+
275+
Then `AssertType` will work with async methods and `Promise` returning methods automatically.
276+
```typescript
277+
import { ValidateClass, AssertType } from 'typescript-is';
278+
279+
@ValidateClass()
280+
class A {
281+
async method(@AssertType({ async: true }) value: number) {
282+
// You can safely use value as a number
283+
return value;
284+
}
285+
286+
methodPromise(@AssertType({ async: true }) value: number): Promise<number> {
287+
// You can safely use value as a number
288+
return Promise.resolve(value);
289+
}
290+
}
291+
292+
new A().method(42).then(value => value === 42 /* true */);
293+
new A().method('42' as any).catch(error => {
294+
// error will be of TypeGuardError type
295+
})
296+
new A().methodPromise('42' as any).catch(error => {
297+
// error will be of TypeGuardError type
298+
})
299+
```
300+
301+
If you want to throw synchronously for some reason, you can override the behaviour using with `@AssertType({ async: false })`:
302+
```typescript
303+
import { ValidateClass, AssertType } from 'typescript-is';
304+
305+
@ValidateClass()
306+
class A {
307+
async method(@AssertType({ async: false }) value: number) {
308+
// You can safely use value as a number
309+
return value;
310+
}
311+
}
312+
313+
new A().method(42).then(value => value === 42 /* true */);
314+
new A().method('42' as any); // will throw error
315+
```
316+
317+
If you cannot or don't want to enable decorators metadata, you still make AssertType reject with promise using `@AssertType({ async: true })`
318+
```typescript
319+
import { ValidateClass, AssertType } from 'typescript-is';
320+
321+
@ValidateClass()
322+
class A {
323+
async method(@AssertType({ async: true }) value: number) {
324+
// You can safely use value as a number
325+
return value;
326+
}
327+
}
328+
```
329+
262330
## Strict equality (`equals`, `createEquals`, `assertEquals`, `createAssertEquals`)
263331

264332
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:

index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export function createAssertEquals<T>(): (object: any) => T;
125125
new A().method('0' as any); // will throw an error
126126
```
127127
*/
128-
export function AssertType(): (target: object, propertyKey: string | symbol, parameterIndex: number) => void;
128+
export function AssertType(options?: { async: boolean }): (target: object, propertyKey: string | symbol, parameterIndex: number) => void;
129129

130130
/**
131131
* Overrides methods in the target class with a proxy that will first validate the argument types.

index.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ function AssertType(assertion, options = {}) {
4545
require('reflect-metadata');
4646
return function (target, propertyKey, parameterIndex) {
4747
const assertions = Reflect.getOwnMetadata(assertionsMetadataKey, target, propertyKey) || [];
48-
assertions[parameterIndex] = { assertion, options };
48+
if(Reflect.getOwnMetadata('design:returntype', target, propertyKey) === Promise) {
49+
assertions[parameterIndex] = { assertion, options: Object.assign({ async: true }, options) };
50+
} else {
51+
assertions[parameterIndex] = { assertion, options };
52+
}
4953
Reflect.defineMetadata(assertionsMetadataKey, assertions, target, propertyKey);
5054
};
5155
}
@@ -64,7 +68,12 @@ function ValidateClass(errorConstructor = TypeGuardError) {
6468
}
6569
const errorObject = assertions[i].assertion(args[i]);
6670
if (errorObject !== null) {
67-
throw new errorConstructor(errorObject, args[i]);
71+
const errorInstance = new errorConstructor(errorObject, args[i]);
72+
if(assertions[i].options.async) {
73+
return Promise.reject(errorInstance);
74+
} else {
75+
throw errorInstance;
76+
}
6877
}
6978
}
7079
return originalMethod.apply(this, args);

test-fixtures/issue-104.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {AssertType, ValidateClass} from '../index';
2+
3+
@ValidateClass()
4+
export class AsyncMethods {
5+
async asyncMethod(@AssertType() body: { test: string }): Promise<boolean> {
6+
return true
7+
}
8+
async asyncMethodNoExplicitReturn(@AssertType() body: { test: string }) {
9+
return true
10+
}
11+
promiseReturnMethod(@AssertType() body: { test: string }): Promise<boolean> {
12+
return Promise.resolve(true)
13+
}
14+
async asyncOverride(@AssertType({ async: false }) body: { test: string }): Promise<boolean> {
15+
return true
16+
}
17+
promiseOrOtherReturnMethod(@AssertType() body: { test: string }): Promise<boolean> | boolean{
18+
return Promise.resolve(true)
19+
}
20+
}
21+

test/issue-104.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as assert from 'assert';
2+
import {AsyncMethods} from '../test-fixtures/issue-104';
3+
4+
describe('@ValidateClass(), @AssertType()', () => {
5+
it('should return rejected promise for async methods', () => {
6+
const instance = new AsyncMethods()
7+
assert.rejects(instance.asyncMethod({invalid: 123} as any))
8+
})
9+
it('should return rejected promise for async methods with not explicit return type', () => {
10+
const instance = new AsyncMethods()
11+
assert.rejects(instance.asyncMethodNoExplicitReturn({invalid: 123} as any))
12+
})
13+
it('should return rejected promise for methods returning promise', () => {
14+
const instance = new AsyncMethods()
15+
assert.rejects(instance.promiseReturnMethod({invalid: 123} as any))
16+
})
17+
it('should throw synchronously if { async: false } option is set', () => {
18+
const instance = new AsyncMethods()
19+
assert.throws(() => instance.asyncOverride({invalid: 123} as any))
20+
})
21+
it('should throw synchronously method may return something other than promise', () => {
22+
const instance = new AsyncMethods()
23+
assert.throws(() => instance.promiseOrOtherReturnMethod({invalid: 123} as any))
24+
})
25+
})

tsconfig-test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lib": [
66
"es6"
77
],
8+
"emitDecoratorMetadata": true,
89
"experimentalDecorators": true,
910
"noImplicitAny": true,
1011
"noUnusedLocals": true,

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lib": [
66
"es6"
77
],
8+
"emitDecoratorMetadata": true,
89
"experimentalDecorators": true,
910
"noImplicitAny": true,
1011
"noUnusedLocals": true,
@@ -17,4 +18,4 @@
1718
"include": [
1819
"src"
1920
]
20-
}
21+
}

0 commit comments

Comments
 (0)