Skip to content

Commit bb84bc4

Browse files
committed
Merge branch 'master' of github.com:woutervh-/typescript-is
2 parents fe17771 + 6938d9d commit bb84bc4

18 files changed

+239
-17
lines changed

README.md

+70
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ There are some options to configure the transformer.
137137
| `ignoreFunctions` *(deprecated, use `functionBehavior` instead)* | Boolean (default: `false`). If `true`, when the transformer encounters a function, it will ignore it and simply return `true`. If `false`, an error is generated at compile time. |
138138
| `functionBehavior` | One of `error`, `ignore`, or `basic` (default: `error`). Determines the behavior of transformer when encountering a function. `error` will cause a compile-time error, `ignore` will cause the validation function to always return `true`, and `basic` will do a simple function-type-check. Overrides `ignoreFunctions`. |
139139
| `disallowSuperfluousObjectProperties` | Boolean (default: `false`). If `true`, objects are checked for having superfluous properties and will cause the validation to fail if they do. If `false`, no check for superfluous properties is made. |
140+
| `transformNonNullExpressions` | Boolean (default: `false`). If `true`, non-null expressions (eg. `foo!.bar`) are checked to not be `null` or `undefined` |
140141
| `emitDetailedErrors` | Boolean or `auto` (default: `auto`). The generated validation functions can return detailed error messages, pointing out where and why validation failed. These messages are used by `assertType<T>()`, but are ignored by `is<T>()`. If `false`, validation functions return empty error messages, decreasing code size. `auto` will generate detailed error messages for assertions, but not for type checks. `true` will always generate detailed error messages, matching the behaviour of version 0.18.3 and older. |
141142

142143
If you are using `ttypescript`, you can include the options in your `tsconfig.json`:
@@ -152,6 +153,7 @@ If you are using `ttypescript`, you can include the options in your `tsconfig.js
152153
"ignoreMethods": true,
153154
"functionBehavior": "ignore",
154155
"disallowSuperfluousObjectProperties": true,
156+
"transformNonNullExpressions": true,
155157
"emitDetailedErrors": "auto"
156158
}
157159
]
@@ -259,6 +261,74 @@ new A().method(42) === 42; // true
259261
new A().method('42' as any); // will throw error
260262
```
261263

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

264334
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

+1-1
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

+11-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ function AssertType(assertion, options = {}) {
4747
require('reflect-metadata');
4848
return function (target, propertyKey, parameterIndex) {
4949
const assertions = Reflect.getOwnMetadata(assertionsMetadataKey, target, propertyKey) || [];
50-
assertions[parameterIndex] = { assertion, options };
50+
if(Reflect.getOwnMetadata('design:returntype', target, propertyKey) === Promise) {
51+
assertions[parameterIndex] = { assertion, options: Object.assign({ async: true }, options) };
52+
} else {
53+
assertions[parameterIndex] = { assertion, options };
54+
}
5155
Reflect.defineMetadata(assertionsMetadataKey, assertions, target, propertyKey);
5256
};
5357
}
@@ -66,7 +70,12 @@ function ValidateClass(errorConstructor = TypeGuardError) {
6670
}
6771
const errorObject = assertions[i].assertion(args[i]);
6872
if (errorObject !== null) {
69-
throw new errorConstructor(errorObject, args[i]);
73+
const errorInstance = new errorConstructor(errorObject, args[i]);
74+
if(assertions[i].options.async) {
75+
return Promise.reject(errorInstance);
76+
} else {
77+
throw errorInstance;
78+
}
7079
}
7180
}
7281
return originalMethod.apply(this, args);

src/transform-inline/transform-node.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import { sliceMapValues } from './utils';
88
function createArrowFunction(type: ts.Type, rootName: string, optional: boolean, partialVisitorContext: PartialVisitorContext) {
99
const functionMap: VisitorContext['functionMap'] = new Map();
1010
const functionNames: VisitorContext['functionNames'] = new Set();
11-
const visitorContext = { ...partialVisitorContext, functionNames, functionMap };
11+
const typeIdMap: VisitorContext['typeIdMap'] = new Map();
12+
const visitorContext: VisitorContext = { ...partialVisitorContext, functionNames, functionMap, typeIdMap };
1213
const emitDetailedErrors = !!visitorContext.options.emitDetailedErrors;
1314
const functionName = partialVisitorContext.options.shortCircuit
1415
? visitShortCircuit(visitorContext)
@@ -136,6 +137,55 @@ export function transformNode(node: ts.Node, visitorContext: PartialVisitorConte
136137
]
137138
);
138139
}
140+
} else if (visitorContext.options.transformNonNullExpressions && ts.isNonNullExpression(node)) {
141+
const expression = node.expression
142+
return ts.factory.updateNonNullExpression(node, ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression(
143+
ts.factory.createParenthesizedExpression(ts.factory.createBinaryExpression(
144+
ts.factory.createBinaryExpression(
145+
ts.factory.createTypeOfExpression(expression),
146+
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken),
147+
ts.factory.createStringLiteral('undefined')
148+
),
149+
ts.factory.createToken(ts.SyntaxKind.BarBarToken),
150+
ts.factory.createBinaryExpression(
151+
expression,
152+
ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken),
153+
ts.factory.createNull()
154+
)
155+
)),
156+
ts.factory.createToken(ts.SyntaxKind.QuestionToken),
157+
ts.factory.createCallExpression(
158+
ts.factory.createParenthesizedExpression(ts.factory.createArrowFunction(
159+
undefined,
160+
undefined,
161+
[],
162+
undefined,
163+
ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
164+
ts.factory.createBlock(
165+
[ts.factory.createThrowStatement(ts.factory.createNewExpression(
166+
ts.factory.createIdentifier('Error'),
167+
undefined,
168+
[ts.factory.createTemplateExpression(
169+
ts.factory.createTemplateHead(`${expression.getText()} was non-null asserted but is `),
170+
[ts.factory.createTemplateSpan(
171+
expression,
172+
ts.factory.createTemplateTail(
173+
''
174+
)
175+
)]
176+
)]
177+
))
178+
],
179+
false
180+
)
181+
)),
182+
undefined,
183+
[]
184+
),
185+
ts.factory.createToken(ts.SyntaxKind.ColonToken),
186+
expression
187+
)))
139188
}
140189
return node;
141190
}
191+

src/transform-inline/transformer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default function transformer(program: ts.Program, options?: { [Key: strin
4242
ignoreMethods: !!(options && options.ignoreMethods),
4343
functionBehavior: getFunctionBehavior(options),
4444
disallowSuperfluousObjectProperties: !!(options && options.disallowSuperfluousObjectProperties),
45+
transformNonNullExpressions: !!(options && options.transformNonNullExpressions),
4546
emitDetailedErrors: getEmitDetailedErrors(options)
4647
},
4748
typeMapperStack: [],

src/transform-inline/visitor-context.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ interface Options {
66
ignoreMethods: boolean;
77
functionBehavior: 'error' | 'ignore' | 'basic';
88
disallowSuperfluousObjectProperties: boolean;
9+
transformNonNullExpressions: boolean;
910
emitDetailedErrors: boolean | 'auto';
1011
}
1112

1213
export interface VisitorContext extends PartialVisitorContext {
1314
functionNames: Set<string>;
1415
functionMap: Map<string, ts.FunctionDeclaration>;
16+
typeIdMap: Map<string, string>;
1517
}
1618

1719
export interface PartialVisitorContext {

src/transform-inline/visitor-type-name.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,19 @@ function visitArrayObjectType(type: ts.ObjectType, visitorContext: VisitorContex
3939
return `sa_${numberIndexName}_ea`;
4040
}
4141

42-
function visitRegularObjectType(type: ts.ObjectType) {
43-
const id: string = (type as unknown as { id: string }).id;
44-
return `_${id}`;
42+
function getTypeIndexById(type: ts.Type, { typeIdMap }: VisitorContext) {
43+
const id = (type as unknown as { id: string | number }).id.toString();
44+
let index = typeIdMap.get(id);
45+
if (index === undefined) {
46+
index = typeIdMap.size.toString();
47+
typeIdMap.set(id, index);
48+
}
49+
return index;
50+
}
51+
52+
function visitRegularObjectType(type: ts.Type, visitorContext: VisitorContext) {
53+
const index = getTypeIndexById(type, visitorContext);
54+
return `_${index}`;
4555
}
4656

4757
function visitTypeReference(type: ts.TypeReference, visitorContext: VisitorContext, mode: NameMode) {
@@ -71,7 +81,7 @@ function visitObjectType(type: ts.ObjectType, visitorContext: VisitorContext, mo
7181
} else if (checkIsDateClass(type)) {
7282
return '_date';
7383
} else {
74-
return visitRegularObjectType(type);
84+
return visitRegularObjectType(type, visitorContext);
7585
}
7686
}
7787

@@ -98,7 +108,7 @@ function visitIndexedAccessType(type: ts.IndexedAccessType, visitorContext: Visi
98108

99109
export function visitType(type: ts.Type, visitorContext: VisitorContext, mode: NameMode): string {
100110
let name: string;
101-
const id: string = (type as unknown as { id: string }).id;
111+
const index = getTypeIndexById(type, visitorContext);
102112
if ((ts.TypeFlags.Any & type.flags) !== 0) {
103113
name = VisitorUtils.getAnyFunction(visitorContext);
104114
} else if ((ts.TypeFlags.Unknown & type.flags) !== 0) {
@@ -118,25 +128,25 @@ export function visitType(type: ts.Type, visitorContext: VisitorContext, mode: N
118128
} else if ((ts.TypeFlags.String & type.flags) !== 0) {
119129
name = VisitorUtils.getStringFunction(visitorContext);
120130
} else if ((ts.TypeFlags.BooleanLiteral & type.flags) !== 0) {
121-
name = `_${id}`;
131+
name = `_${index}`;
122132
} else if (tsutils.isTypeReference(type) && visitorContext.previousTypeReference !== type) {
123133
name = visitTypeReference(type, visitorContext, mode);
124134
} else if ((ts.TypeFlags.TypeParameter & type.flags) !== 0) {
125135
name = visitTypeParameter(type, visitorContext, mode);
126136
} else if (tsutils.isObjectType(type)) {
127137
name = visitObjectType(type, visitorContext, mode);
128138
} else if (tsutils.isLiteralType(type)) {
129-
name = `_${id}`;
139+
name = `_${index}`;
130140
} else if (tsutils.isUnionOrIntersectionType(type)) {
131141
name = visitUnionOrIntersectionType(type, visitorContext, mode);
132142
} else if ((ts.TypeFlags.NonPrimitive & type.flags) !== 0) {
133-
name = `_${id}`;
143+
name = `_${index}`;
134144
} else if ((ts.TypeFlags.Index & type.flags) !== 0) {
135145
name = visitIndexType(type, visitorContext);
136146
} else if (tsutils.isIndexedAccessType(type)) {
137147
name = visitIndexedAccessType(type, visitorContext);
138148
} else if ((ts.TypeFlags.TemplateLiteral & type.flags) !== 0) {
139-
name = `_${id}`;
149+
name = `_${index}`;
140150
} else {
141151
throw new Error('Could not generate type-check; unsupported type with flags: ' + type.flags);
142152
}
@@ -153,7 +163,8 @@ export function visitType(type: ts.Type, visitorContext: VisitorContext, mode: N
153163
if (tsutils.isTypeReference(type) && type.typeArguments !== undefined) {
154164
for (const typeArgument of type.typeArguments) {
155165
const resolvedType = VisitorUtils.getResolvedTypeParameter(typeArgument, visitorContext) || typeArgument;
156-
name += `_${(resolvedType as unknown as { id: string }).id}`;
166+
const resolvedTypeIndex = getTypeIndexById(resolvedType, visitorContext);
167+
name += `_${resolvedTypeIndex}`;
157168
}
158169
}
159170
return name;

test-fixtures/issue-104.ts

+21
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

+25
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+
})

test/issue-16.ts

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('visitor', () => {
3737
functionBehavior: 'error',
3838
shortCircuit: false,
3939
disallowSuperfluousObjectProperties: false,
40+
transformNonNullExpressions: false,
4041
emitDetailedErrors: 'auto'
4142
},
4243
typeMapperStack: [],
@@ -63,6 +64,7 @@ describe('visitor', () => {
6364
functionBehavior: 'error',
6465
shortCircuit: false,
6566
disallowSuperfluousObjectProperties: false,
67+
transformNonNullExpressions: false,
6668
emitDetailedErrors: 'auto'
6769
},
6870
typeMapperStack: [],
@@ -93,6 +95,7 @@ describe('visitor', () => {
9395
functionBehavior: 'error',
9496
shortCircuit: false,
9597
disallowSuperfluousObjectProperties: false,
98+
transformNonNullExpressions: false,
9699
emitDetailedErrors: 'auto'
97100
},
98101
typeMapperStack: [],
@@ -122,6 +125,7 @@ describe('visitor', () => {
122125
functionBehavior: 'error',
123126
shortCircuit: false,
124127
disallowSuperfluousObjectProperties: false,
128+
transformNonNullExpressions: false,
125129
emitDetailedErrors: 'auto'
126130
},
127131
typeMapperStack: [],
@@ -151,6 +155,7 @@ describe('visitor', () => {
151155
functionBehavior: 'error',
152156
shortCircuit: false,
153157
disallowSuperfluousObjectProperties: false,
158+
transformNonNullExpressions: false,
154159
emitDetailedErrors: 'auto'
155160
},
156161
typeMapperStack: [],

test/issue-27.ts

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('visitor', () => {
3535
functionBehavior: 'error',
3636
shortCircuit: false,
3737
disallowSuperfluousObjectProperties: false,
38+
transformNonNullExpressions: false,
3839
emitDetailedErrors: 'auto'
3940
},
4041
typeMapperStack: [],

0 commit comments

Comments
 (0)