Skip to content

Commit 6d8c04b

Browse files
Add support for treating 'true | false' as a Boolean type (#7)
1 parent 5d1b6ea commit 6d8c04b

File tree

5 files changed

+59
-10
lines changed

5 files changed

+59
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ This changelog documents the changes between release versions.
44
## main
55
Changes to be included in the next upcoming release
66

7+
- Add support for treating 'true | false' as a Boolean type ([#7](https://github.com/hasura/ndc-nodejs-lambda/pull/7))
8+
79
## v0.12.0
810
- Add support for JSDoc descriptions from object types ([#3](https://github.com/hasura/ndc-nodejs-lambda/pull/3))
911
- Fix type name conflicts when using generic interfaces ([#4](https://github.com/hasura/ndc-nodejs-lambda/pull/4))

ndc-lambda-sdk/src/inference.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ function deriveSchemaTypeIfTsArrayType(tsType: ts.Type, typePath: TypePathSegmen
334334
}
335335

336336
function deriveSchemaTypeIfScalarType(tsType: ts.Type, context: TypeDerivationContext): Result<schema.TypeDefinition, string[]> | undefined {
337-
if (tsutils.isIntrinsicBooleanType(tsType)) {
337+
if (tsutils.isIntrinsicBooleanType(tsType) || isBooleanUnionType(tsType)) {
338338
context.scalarTypeDefinitions[schema.BuiltInScalarTypeName.Boolean] = {};
339339
return new Ok({ type: "named", kind: "scalar", name: schema.BuiltInScalarTypeName.Boolean });
340340
}
@@ -394,6 +394,18 @@ function isMapType(tsType: ts.Type): boolean {
394394
return symbol.escapedName === "Map" && symbol.members?.has(ts.escapeLeadingUnderscores("keys")) === true && symbol.members?.has(ts.escapeLeadingUnderscores("values")) === true && symbol.members?.has(ts.escapeLeadingUnderscores("entries")) === true;
395395
}
396396

397+
// Identifies the 'true | false' type (which is distinct from the 'boolean' type)
398+
function isBooleanUnionType(tsType: ts.Type): boolean {
399+
if (!tsutils.isUnionType(tsType)) return false;
400+
401+
return tsType.types.length === 2 && unionTypeContainsBothBooleanLiterals(tsType);
402+
}
403+
404+
function unionTypeContainsBothBooleanLiterals(tsUnionType: ts.UnionType): boolean {
405+
return tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "true") !== undefined
406+
&& tsUnionType.types.find(tsType => tsutils.isBooleanLiteralType(tsType) && tsType.intrinsicName === "false") !== undefined;
407+
}
408+
397409
function isJSONValueType(tsType: ts.Type, ndcLambdaSdkModule: ts.ResolvedModuleFull): boolean {
398410
// Must be a class type
399411
if (!tsutils.isObjectType(tsType) || !tsutils.isObjectFlagSet(tsType, ts.ObjectFlags.Class))
@@ -411,7 +423,7 @@ function isJSONValueType(tsType: ts.Type, ndcLambdaSdkModule: ts.ResolvedModuleF
411423
}
412424

413425
function deriveSchemaTypeIfNullableType(tsType: ts.Type, typePath: TypePathSegment[], context: TypeDerivationContext, recursionDepth: number): Result<schema.TypeDefinition, string[]> | undefined {
414-
const notNullableResult = unwrapNullableType(tsType);
426+
const notNullableResult = unwrapNullableType(tsType, context.typeChecker);
415427
if (notNullableResult !== null) {
416428
const [notNullableType, nullOrUndefinability] = notNullableResult;
417429
return deriveSchemaTypeForTsType(notNullableType, typePath, context, recursionDepth + 1)
@@ -456,11 +468,11 @@ function unwrapPromiseType(tsType: ts.Type, typeChecker: ts.TypeChecker): ts.Typ
456468
}
457469
}
458470

459-
function unwrapNullableType(ty: ts.Type): [ts.Type, schema.NullOrUndefinability] | null {
460-
if (!ty.isUnion()) return null;
471+
function unwrapNullableType(tsType: ts.Type, typeChecker: ts.TypeChecker): [ts.Type, schema.NullOrUndefinability] | null {
472+
if (!tsType.isUnion()) return null;
461473

462-
const isNullable = ty.types.find(tsutils.isIntrinsicNullType) !== undefined;
463-
const isUndefined = ty.types.find(tsutils.isIntrinsicUndefinedType) !== undefined;
474+
const isNullable = tsType.types.find(tsutils.isIntrinsicNullType) !== undefined;
475+
const isUndefined = tsType.types.find(tsutils.isIntrinsicUndefinedType) !== undefined;
464476
const nullOrUndefinability =
465477
isNullable
466478
? ( isUndefined
@@ -472,12 +484,21 @@ function unwrapNullableType(ty: ts.Type): [ts.Type, schema.NullOrUndefinability]
472484
: null
473485
);
474486

475-
const typesWithoutNullAndUndefined = ty.types
487+
const typesWithoutNullAndUndefined = tsType.types
476488
.filter(t => !tsutils.isIntrinsicNullType(t) && !tsutils.isIntrinsicUndefinedType(t));
477489

478-
return typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability
479-
? [typesWithoutNullAndUndefined[0]!, nullOrUndefinability]
480-
: null;
490+
// The case where one type is unioned with either or both of null and undefined
491+
if (typesWithoutNullAndUndefined.length === 1 && nullOrUndefinability) {
492+
return [typesWithoutNullAndUndefined[0]!, nullOrUndefinability];
493+
}
494+
// The weird edge case where null or undefined is unioned with both 'true' and 'false'
495+
// We simplify this to being unioned with 'boolean' instead
496+
else if (nullOrUndefinability && typesWithoutNullAndUndefined.length === 2 && unionTypeContainsBothBooleanLiterals(tsType)) {
497+
return [typeChecker.getBooleanType(), nullOrUndefinability];
498+
}
499+
else {
500+
return null;
501+
}
481502
}
482503

483504
type PropertyTypeInfo = {

ndc-lambda-sdk/test/inference/basic-inference/basic-inference.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ describe("basic inference", function() {
254254
name: "String"
255255
}
256256
},
257+
{
258+
argumentName: "booleanUnion",
259+
description: null,
260+
type: {
261+
type: "named",
262+
kind: "scalar",
263+
name: "Boolean"
264+
}
265+
},
257266
{
258267
argumentName: "array",
259268
description: null,
@@ -575,6 +584,7 @@ describe("basic inference", function() {
575584
},
576585
},
577586
scalarTypes: {
587+
Boolean: {},
578588
Float: {},
579589
String: {},
580590
}
@@ -707,6 +717,19 @@ describe("basic inference", function() {
707717
},
708718
},
709719
},
720+
{
721+
propertyName: "optionalBoolean",
722+
description: null,
723+
type: {
724+
type: "nullable",
725+
nullOrUndefinability: NullOrUndefinability.AcceptsUndefinedOnly,
726+
underlyingType: {
727+
kind: "scalar",
728+
name: "Boolean",
729+
type: "named",
730+
},
731+
},
732+
},
710733
{
711734
propertyName: "undefinedString",
712735
description: null,
@@ -737,6 +760,7 @@ describe("basic inference", function() {
737760
},
738761
},
739762
scalarTypes: {
763+
Boolean: {},
740764
String: {},
741765
},
742766
}

ndc-lambda-sdk/test/inference/basic-inference/complex-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function bar(
3636
string: string,
3737
aliasedString: AliasedString,
3838
genericScalar: GenericScalar<string>,
39+
booleanUnion: true | false,
3940
array: string[],
4041
anonObj: {a: number, b: string},
4142
aliasedObj: Bar,

ndc-lambda-sdk/test/inference/basic-inference/nullable-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ type MyObject = {
22
string: string,
33
nullableString: string | null,
44
optionalString?: string
5+
optionalBoolean?: boolean
56
undefinedString: string | undefined
67
nullOrUndefinedString: string | undefined | null
78
}

0 commit comments

Comments
 (0)