From c8fb061c5af49776f5572df2f1832bdda14efd52 Mon Sep 17 00:00:00 2001 From: Akihiro Omori Date: Fri, 1 Mar 2024 13:35:56 +0900 Subject: [PATCH 1/2] check property existance --- src/index.ts | 74 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index f73c28c..9f7e473 100644 --- a/src/index.ts +++ b/src/index.ts @@ -188,6 +188,14 @@ function typeOf(varName: string, type: string): string { return eq(`typeof ${varName}`, `"${type}"`) } +function inOp(propName: string, varName: string): string { + return `"${propName}" in ${varName}` +} + +function not(statement: string): string { + return `!(${statement})` +} + function typeUnionConditions( varName: string, types: Type[], @@ -412,6 +420,7 @@ To disable this warning, put comment "${suppressComment}" before the declaration ...declaration.getMethods(), ].map(p => ({ name: propertyName(p), + isOptional: p.hasQuestionToken(), type: p.getType(), })) conditions.push( @@ -464,6 +473,7 @@ To disable this warning, put comment "${suppressComment}" before the declaration : p.getTypeAtLocation((typeDeclarations || [])[0]) return { name: p.getName(), + isOptional: p.isOptional(), type: typeAtLocation, } }) @@ -730,7 +740,7 @@ function typeConditions( function propertyConditions( objName: string, - property: { name: string; type: Type }, + property: { name: string; isOptional: boolean; type: Type }, addDependency: IAddDependency, project: Project, path: string, @@ -738,7 +748,7 @@ function propertyConditions( records: readonly IRecord[], outFile: SourceFile, options: IProcessOptions -): string | null { +): string { const { debug } = options const propertyName = property.name @@ -747,18 +757,28 @@ function propertyConditions( const propertyPath = `${path}["${strippedName}"]` let expectedType = property.type.getText() - const conditions = typeConditions( - varName, - property.type, - addDependency, - project, - propertyPath, - arrayDepth, - true, - records, - outFile, - options + const hasPropertyCondition = inOp(strippedName, objName) + let conditions = ands( + ...([ + hasPropertyCondition, + typeConditions( + varName, + property.type, + addDependency, + project, + propertyPath, + arrayDepth, + true, + records, + outFile, + options + ), + ].filter(v => v != null) as string[]) ) + if (property.isOptional) { + conditions = ors(not(hasPropertyCondition), conditions) + } + if (debug) { if (expectedType.indexOf('import') > -1) { const standardizedCwd = FileUtils.standardizeSlashes(process.cwd()) @@ -776,7 +796,7 @@ function propertyConditions( function propertiesConditions( varName: string, - properties: ReadonlyArray<{ name: string; type: Type }>, + properties: ReadonlyArray<{ name: string; isOptional: boolean; type: Type }>, addDependency: IAddDependency, project: Project, path: string, @@ -785,21 +805,19 @@ function propertiesConditions( outFile: SourceFile, options: IProcessOptions ): string[] { - return properties - .map(prop => - propertyConditions( - varName, - prop, - addDependency, - project, - path, - arrayDepth, - records, - outFile, - options - ) + return properties.map(prop => + propertyConditions( + varName, + prop, + addDependency, + project, + path, + arrayDepth, + records, + outFile, + options ) - .filter(v => v !== null) as string[] + ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars From ab9291831a53a0e579500612b60b554d69a8943f Mon Sep 17 00:00:00 2001 From: Akihiro Omori Date: Fri, 1 Mar 2024 13:36:06 +0900 Subject: [PATCH 2/2] some tests --- ...or_unions_with_disjoint_properties.test.ts | 32 +++++++++++++++ ..._for_interface_with_optional_field.test.ts | 29 ++++++++------ ...s_type_guards_for_simple_interface.test.ts | 2 + ...uards_for_type_with_optional_field.test.ts | 39 +++++++++++++++++++ 4 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 tests/features/generated_type_guards_for_unions_with_disjoint_properties.test.ts create mode 100644 tests/features/generates_type_guards_for_type_with_optional_field.test.ts diff --git a/tests/features/generated_type_guards_for_unions_with_disjoint_properties.test.ts b/tests/features/generated_type_guards_for_unions_with_disjoint_properties.test.ts new file mode 100644 index 0000000..3865b84 --- /dev/null +++ b/tests/features/generated_type_guards_for_unions_with_disjoint_properties.test.ts @@ -0,0 +1,32 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generated type guards for unions with disjoint properties', + { + 'test.ts': ` + export type X = { key1: string } | { key2: number } + `, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { X } from "./test"; + + export function isX(obj: unknown): obj is X { + const typedObj = obj as X + return ( + ((typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + "key1" in typedObj && + typeof typedObj["key1"] === "string" || + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + "key2" in typedObj && + typeof typedObj["key2"] === "number") + ) + }`, + }, + { options: { exportAll: true } } +) diff --git a/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts b/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts index c1e4833..002ec81 100644 --- a/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts +++ b/tests/features/generates_type_guards_for_interface_with_optional_field.test.ts @@ -17,18 +17,23 @@ testProcessProject( import { Foo } from "./test"; export function isFoo(obj: unknown): obj is Foo { - const typedObj = obj as Foo - return ( - (typedObj !== null && - typeof typedObj === "object" || - typeof typedObj === "function") && - ( typeof typedObj["foo"] === "undefined" || - typeof typedObj["foo"] === "number" ) && - ( typeof typedObj["bar"] === "undefined" || - typeof typedObj["bar"] === "number" ) && - ( typeof typedObj["baz"] === "undefined" || - typeof typedObj["baz"] === "number" ) - ) + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + ( !("foo" in typedObj) || + "foo" in typedObj && + (typeof typedObj["foo"] === "undefined" || + typeof typedObj["foo"] === "number" )) && + "bar" in typedObj && + ( typeof typedObj["bar"] === "undefined" || + typeof typedObj["bar"] === "number" ) && + ( !("baz" in typedObj) || + "baz" in typedObj && + (typeof typedObj["baz"] === "undefined" || + typeof typedObj["baz"] === "number" )) + ) }`, } ) diff --git a/tests/features/generates_type_guards_for_simple_interface.test.ts b/tests/features/generates_type_guards_for_simple_interface.test.ts index a6560df..4a62aa4 100644 --- a/tests/features/generates_type_guards_for_simple_interface.test.ts +++ b/tests/features/generates_type_guards_for_simple_interface.test.ts @@ -21,7 +21,9 @@ testProcessProject( (typedObj !== null && typeof typedObj === "object" || typeof typedObj === "function") && + "foo" in typedObj && typeof typedObj["foo"] === "number" && + "bar" in typedObj && typeof typedObj["bar"] === "string" ) }`, diff --git a/tests/features/generates_type_guards_for_type_with_optional_field.test.ts b/tests/features/generates_type_guards_for_type_with_optional_field.test.ts new file mode 100644 index 0000000..1e3543c --- /dev/null +++ b/tests/features/generates_type_guards_for_type_with_optional_field.test.ts @@ -0,0 +1,39 @@ +import { testProcessProject } from '../generate' + +testProcessProject( + 'generates type guards for interface with optional field', + { + 'test.ts': ` + /** @see {isFoo} ts-auto-guard:type-guard */ + export type Foo { + foo?: number, + bar: number | undefined, + baz?: number | undefined + }`, + }, + { + 'test.ts': null, + 'test.guard.ts': ` + import { Foo } from "./test"; + + export function isFoo(obj: unknown): obj is Foo { + const typedObj = obj as Foo + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + (!("foo" in typedObj) || + "foo" in typedObj && + (typeof typedObj["foo"] === "undefined" || + typeof typedObj["foo"] === "number")) && + "bar" in typedObj && + (typeof typedObj["bar"] === "undefined" || + typeof typedObj["bar"] === "number") && + (!("baz" in typedObj) || + "baz" in typedObj && + (typeof typedObj["baz"] === "undefined" || + typeof typedObj["baz"] === "number")) + ) + }`, + } +)