diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 645848e5b..f20f796a8 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -23,6 +23,7 @@ import { MappedType, SignatureReflection, } from "../models"; +import { OptionalType } from "../models/types/optional"; import { RestType } from "../models/types/rest"; import { TemplateLiteralType } from "../models/types/template-literal"; import { zip } from "../utils/array"; @@ -65,6 +66,7 @@ export function loadConverters() { intersectionConverter, jsDocVariadicTypeConverter, keywordConverter, + optionalConverter, parensConverter, predicateConverter, queryConverter, @@ -453,6 +455,17 @@ const keywordConverter: TypeConverter = { }, }; +const optionalConverter: TypeConverter = { + kind: [ts.SyntaxKind.OptionalType], + convert(context, node) { + return new OptionalType( + removeUndefined(convertType(context, node.type)) + ); + }, + // Handled by the tuple converter + convertType: requestBugReport, +}; + const parensConverter: TypeConverter = { kind: [ts.SyntaxKind.ParenthesizedType], convert(context, node) { @@ -881,7 +894,14 @@ const tupleConverter: TypeConverter = { return new RestType(new ArrayType(el)); } - // TODO: Optional elements + + if ( + type.target.elementFlags[i] & ts.ElementFlags.Optional && + !(el instanceof NamedTupleMember) + ) { + return new OptionalType(removeUndefined(el)); + } + return el; }); } diff --git a/src/lib/models/types/index.ts b/src/lib/models/types/index.ts index 28951bc88..7e735752e 100644 --- a/src/lib/models/types/index.ts +++ b/src/lib/models/types/index.ts @@ -7,13 +7,14 @@ export { IntersectionType } from "./intersection"; export { IntrinsicType } from "./intrinsic"; export { LiteralType } from "./literal"; export { MappedType } from "./mapped"; +export { OptionalType } from "./optional"; export { PredicateType } from "./predicate"; export { QueryType } from "./query"; export { ReferenceType } from "./reference"; export { ReflectionType } from "./reflection"; +export { RestType } from "./rest"; export { TemplateLiteralType } from "./template-literal"; export { NamedTupleMember, TupleType } from "./tuple"; -export { RestType } from "./rest"; export { TypeOperatorType } from "./type-operator"; export { TypeParameterType } from "./type-parameter"; export { UnionType } from "./union"; diff --git a/src/lib/models/types/optional.ts b/src/lib/models/types/optional.ts new file mode 100644 index 000000000..9e5c6beb6 --- /dev/null +++ b/src/lib/models/types/optional.ts @@ -0,0 +1,67 @@ +import { Type } from "./abstract"; +import { UnionType } from "./union"; +import { IntersectionType } from "./intersection"; + +/** + * Represents an optional type + * ```ts + * type Z = [1, 2?] + * // ^^ + * ``` + */ +export class OptionalType extends Type { + /** + * The type of the rest array elements. + */ + elementType: Type; + + /** + * The type name identifier. + */ + readonly type = "optional"; + + /** + * Create a new OptionalType instance. + * + * @param elementType The type of the element + */ + constructor(elementType: Type) { + super(); + this.elementType = elementType; + } + + /** + * Clone this type. + * + * @return A clone of this type. + */ + clone(): Type { + return new OptionalType(this.elementType.clone()); + } + + /** + * Test whether this type equals the given type. + * + * @param type The type that should be checked for equality. + * @returns TRUE if the given type equals this type, FALSE otherwise. + */ + equals(type: Type): boolean { + if (!(type instanceof OptionalType)) { + return false; + } + return type.elementType.equals(this.elementType); + } + + /** + * Return a string representation of this type. + */ + toString() { + if ( + this.elementType instanceof UnionType || + this.elementType instanceof IntersectionType + ) { + return `(${this.elementType.toString()})?`; + } + return `${this.elementType.toString()}?`; + } +} diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index f9c8167b5..63073004f 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -76,8 +76,12 @@ type _ModelToObject = ? IntersectionType : T extends M.IntrinsicType ? IntrinsicType + : T extends M.OptionalType + ? OptionalType : T extends M.PredicateType ? PredicateType + : T extends M.QueryType + ? QueryType : T extends M.ReferenceType ? ReferenceType : T extends M.ReflectionType @@ -223,7 +227,9 @@ export type SomeType = | IntersectionType | IntrinsicType | LiteralType + | OptionalType | PredicateType + | QueryType | ReferenceType | ReflectionType | RestType @@ -260,12 +266,16 @@ export interface IntrinsicType extends Type, S {} -export interface QueryType extends Type, S {} +export interface OptionalType + extends Type, + S {} export interface PredicateType extends Type, S {} +export interface QueryType extends Type, S {} + export interface ReferenceType extends Type, S { diff --git a/src/lib/serialization/serializer.ts b/src/lib/serialization/serializer.ts index 099feed15..7e48076b1 100644 --- a/src/lib/serialization/serializer.ts +++ b/src/lib/serialization/serializer.ts @@ -141,8 +141,9 @@ const serializerComponents: (new ( S.InferredTypeSerializer, S.IntersectionTypeSerializer, S.IntrinsicTypeSerializer, - S.QueryTypeSerializer, + S.OptionalTypeSerializer, S.PredicateTypeSerializer, + S.QueryTypeSerializer, S.ReferenceTypeSerializer, S.ReferenceTypeSerializer, S.ReflectionTypeSerializer, diff --git a/src/lib/serialization/serializers/types/index.ts b/src/lib/serialization/serializers/types/index.ts index fd7052fb1..c4d2f394b 100644 --- a/src/lib/serialization/serializers/types/index.ts +++ b/src/lib/serialization/serializers/types/index.ts @@ -7,6 +7,7 @@ export * from "./intersection"; export * from "./intrinsic"; export * from "./literal"; export * from "./mapped"; +export * from "./optional"; export * from "./predicate"; export * from "./query"; export * from "./reference"; diff --git a/src/lib/serialization/serializers/types/optional.ts b/src/lib/serialization/serializers/types/optional.ts new file mode 100644 index 000000000..58f4d664b --- /dev/null +++ b/src/lib/serialization/serializers/types/optional.ts @@ -0,0 +1,25 @@ +import { OptionalType } from "../../../models"; + +import { TypeSerializerComponent } from "../../components"; +import { OptionalType as JSONOptionalType } from "../../schema"; + +export class OptionalTypeSerializer extends TypeSerializerComponent { + supports(t: unknown) { + return t instanceof OptionalType; + } + + /** + * Will be run after [[TypeSerializer]] so `type` will already be set. + * @param type + * @param obj + */ + toObject( + type: OptionalType, + obj: Pick + ): JSONOptionalType { + return { + ...obj, + elementType: this.owner.toObject(type.elementType), + }; + } +} diff --git a/src/test/converter/types/specs.json b/src/test/converter/types/specs.json index cac3bf8dc..fe696d5fa 100644 --- a/src/test/converter/types/specs.json +++ b/src/test/converter/types/specs.json @@ -598,6 +598,36 @@ ] } }, + { + "id": 42, + "name": "WithOptionalElements", + "kind": 4194304, + "kindString": "Type alias", + "flags": {}, + "type": { + "type": "tuple", + "elements": [ + { + "type": "literal", + "value": 1 + }, + { + "type": "optional", + "elementType": { + "type": "literal", + "value": 2 + } + }, + { + "type": "optional", + "elementType": { + "type": "literal", + "value": 3 + } + } + ] + } + }, { "id": 38, "name": "WithRestType", @@ -690,6 +720,39 @@ }, "defaultValue": "..." }, + { + "id": 43, + "name": "withOptionalElements", + "kind": 32, + "kindString": "Variable", + "flags": { + "isConst": true + }, + "type": { + "type": "tuple", + "elements": [ + { + "type": "literal", + "value": 1 + }, + { + "type": "optional", + "elementType": { + "type": "literal", + "value": 2 + } + }, + { + "type": "optional", + "elementType": { + "type": "literal", + "value": 3 + } + } + ] + }, + "defaultValue": "..." + }, { "id": 39, "name": "withRestType", @@ -830,6 +893,7 @@ "kind": 4194304, "children": [ 36, + 42, 38, 40 ] @@ -839,6 +903,7 @@ "kind": 32, "children": [ 37, + 43, 39, 41 ] @@ -853,14 +918,14 @@ ] }, { - "id": 42, + "id": 44, "name": "type-operator", "kind": 1, "kindString": "Module", "flags": {}, "children": [ { - "id": 44, + "id": 46, "name": "B", "kind": 4194304, "kindString": "Type alias", @@ -878,7 +943,7 @@ } }, { - "id": 45, + "id": 47, "name": "C", "kind": 4194304, "kindString": "Type alias", @@ -886,14 +951,14 @@ "type": { "type": "reflection", "declaration": { - "id": 46, + "id": 48, "name": "__type", "kind": 65536, "kindString": "Type literal", "flags": {}, "children": [ { - "id": 47, + "id": 49, "name": "prop1", "kind": 1024, "kindString": "Property", @@ -904,7 +969,7 @@ } }, { - "id": 48, + "id": 50, "name": "prop2", "kind": 1024, "kindString": "Property", @@ -920,8 +985,8 @@ "title": "Properties", "kind": 1024, "children": [ - 47, - 48 + 49, + 50 ] } ] @@ -929,7 +994,7 @@ } }, { - "id": 49, + "id": 51, "name": "D", "kind": 4194304, "kindString": "Type alias", @@ -939,13 +1004,13 @@ "operator": "keyof", "target": { "type": "reference", - "id": 45, + "id": 47, "name": "C" } } }, { - "id": 43, + "id": 45, "name": "a", "kind": 32, "kindString": "Variable", @@ -968,29 +1033,29 @@ "title": "Type aliases", "kind": 4194304, "children": [ - 44, - 45, - 49 + 46, + 47, + 51 ] }, { "title": "Variables", "kind": 32, "children": [ - 43 + 45 ] } ] }, { - "id": 50, + "id": 52, "name": "union-or-intersection", "kind": 1, "kindString": "Module", "flags": {}, "children": [ { - "id": 51, + "id": 53, "name": "FirstType", "kind": 256, "kindString": "Interface", @@ -1000,7 +1065,7 @@ }, "children": [ { - "id": 52, + "id": 54, "name": "firstProperty", "kind": 1024, "kindString": "Property", @@ -1019,13 +1084,13 @@ "title": "Properties", "kind": 1024, "children": [ - 52 + 54 ] } ] }, { - "id": 53, + "id": 55, "name": "SecondType", "kind": 256, "kindString": "Interface", @@ -1035,7 +1100,7 @@ }, "children": [ { - "id": 54, + "id": 56, "name": "secondProperty", "kind": 1024, "kindString": "Property", @@ -1054,13 +1119,13 @@ "title": "Properties", "kind": 1024, "children": [ - 54 + 56 ] } ] }, { - "id": 55, + "id": 57, "name": "ThirdType", "kind": 256, "kindString": "Interface", @@ -1070,7 +1135,7 @@ }, "children": [ { - "id": 58, + "id": 60, "name": "thirdComplexProperty", "kind": 1024, "kindString": "Property", @@ -1094,12 +1159,12 @@ "types": [ { "type": "reference", - "id": 51, + "id": 53, "name": "FirstType" }, { "type": "reference", - "id": 53, + "id": 55, "name": "SecondType" } ] @@ -1110,7 +1175,7 @@ } }, { - "id": 57, + "id": 59, "name": "thirdIntersectionProperty", "kind": 1024, "kindString": "Property", @@ -1123,19 +1188,19 @@ "types": [ { "type": "reference", - "id": 51, + "id": 53, "name": "FirstType" }, { "type": "reference", - "id": 55, + "id": 57, "name": "ThirdType" } ] } }, { - "id": 56, + "id": 58, "name": "thirdUnionProperty", "kind": 1024, "kindString": "Property", @@ -1148,12 +1213,12 @@ "types": [ { "type": "reference", - "id": 51, + "id": 53, "name": "FirstType" }, { "type": "reference", - "id": 53, + "id": 55, "name": "SecondType" } ] @@ -1165,9 +1230,9 @@ "title": "Properties", "kind": 1024, "children": [ - 58, - 57, - 56 + 60, + 59, + 58 ] } ] @@ -1178,9 +1243,9 @@ "title": "Interfaces", "kind": 256, "children": [ - 51, 53, - 55 + 55, + 57 ] } ] @@ -1196,8 +1261,8 @@ 24, 28, 31, - 42, - 50 + 44, + 52 ] } ] diff --git a/src/test/converter/types/tuple.ts b/src/test/converter/types/tuple.ts index 7b4364d3d..566adec63 100644 --- a/src/test/converter/types/tuple.ts +++ b/src/test/converter/types/tuple.ts @@ -12,6 +12,9 @@ export const withRestType = returnMapped(); export type WithRestTypeNames = [a: 123, ...b: 456[]]; export const withRestTypeNames = returnMapped(); +export type WithOptionalElements = [1, 2?, 3?]; +export const withOptionalElements = returnMapped(); + // Helper to force TS to give us types, rather than type nodes, for a given declaration. function returnMapped() { return ({} as any) as { [K in keyof T]: T[K] };