From 80ee9e6d63a3d0b4de3773d1bae5148760e65149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:53:29 +0200 Subject: [PATCH 01/20] feat: add new template literal type Useful for extends types within conditional nodes --- src/Type/TemplateLiteralType.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/Type/TemplateLiteralType.ts diff --git a/src/Type/TemplateLiteralType.ts b/src/Type/TemplateLiteralType.ts new file mode 100644 index 000000000..e6af2e3fc --- /dev/null +++ b/src/Type/TemplateLiteralType.ts @@ -0,0 +1,17 @@ +import { BaseType } from "./BaseType.js"; + +export class TemplateLiteralType extends BaseType { + public constructor(private types: readonly BaseType[]) { + super(); + } + + public getId(): string { + return `template-literal-${this.getParts() + .map((part) => part.getId()) + .join("-")}`; + } + + public getParts(): readonly BaseType[] { + return this.types; + } +} From 856fbc90666c4a63a1e17df358ac02611518ec06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:53:45 +0200 Subject: [PATCH 02/20] refactor: rename and improve teplate literal node parser Added support for never types and template literal types for extends types --- .../StringTemplateLiteralNodeParser.ts | 61 ---------------- src/NodeParser/TemplateLiteralNodeParser.ts | 73 +++++++++++++++++++ 2 files changed, 73 insertions(+), 61 deletions(-) delete mode 100644 src/NodeParser/StringTemplateLiteralNodeParser.ts create mode 100644 src/NodeParser/TemplateLiteralNodeParser.ts diff --git a/src/NodeParser/StringTemplateLiteralNodeParser.ts b/src/NodeParser/StringTemplateLiteralNodeParser.ts deleted file mode 100644 index 78f1e80b2..000000000 --- a/src/NodeParser/StringTemplateLiteralNodeParser.ts +++ /dev/null @@ -1,61 +0,0 @@ -import ts from "typescript"; -import type { Context, NodeParser } from "../NodeParser.js"; -import type { SubNodeParser } from "../SubNodeParser.js"; -import type { BaseType } from "../Type/BaseType.js"; -import { LiteralType } from "../Type/LiteralType.js"; -import { StringType } from "../Type/StringType.js"; -import { UnionType } from "../Type/UnionType.js"; -import { extractLiterals } from "../Utils/extractLiterals.js"; -import { UnknownTypeError } from "../Error/Errors.js"; - -export class StringTemplateLiteralNodeParser implements SubNodeParser { - public constructor(protected childNodeParser: NodeParser) {} - - public supportsNode(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode): boolean { - return ( - node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateLiteralType - ); - } - public createType(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode, context: Context): BaseType { - if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { - return new LiteralType(node.text); - } - - try { - const prefix = node.head.text; - const matrix: string[][] = [[prefix]].concat( - node.templateSpans.map((span) => { - const suffix = span.literal.text; - const type = this.childNodeParser.createType(span.type, context); - return extractLiterals(type).map((value) => value + suffix); - }), - ); - - const expandedLiterals = expand(matrix); - - const expandedTypes = expandedLiterals.map((literal) => new LiteralType(literal)); - - if (expandedTypes.length === 1) { - return expandedTypes[0]; - } - - return new UnionType(expandedTypes); - } catch (error) { - if (error instanceof UnknownTypeError) { - return new StringType(); - } - - throw error; - } - } -} - -function expand(matrix: string[][]): string[] { - if (matrix.length === 1) { - return matrix[0]; - } - const head = matrix[0]; - const nested = expand(matrix.slice(1)); - const combined = head.map((prefix) => nested.map((suffix) => prefix + suffix)); - return ([] as string[]).concat(...combined); -} diff --git a/src/NodeParser/TemplateLiteralNodeParser.ts b/src/NodeParser/TemplateLiteralNodeParser.ts new file mode 100644 index 000000000..5f1679e38 --- /dev/null +++ b/src/NodeParser/TemplateLiteralNodeParser.ts @@ -0,0 +1,73 @@ +import ts from "typescript"; +import type { Context, NodeParser } from "../NodeParser.js"; +import type { SubNodeParser } from "../SubNodeParser.js"; +import type { BaseType } from "../Type/BaseType.js"; +import { LiteralType } from "../Type/LiteralType.js"; +import { TemplateLiteralType } from "../Type/TemplateLiteralType.js"; // New type +import { NeverType } from "../Type/NeverType.js"; +import { extractLiterals } from "../Utils/extractLiterals.js"; +import { StringType } from "../Type/StringType.js"; +import { UnionType } from "../Type/UnionType.js"; +import { isExtendsType } from "../Utils/isExtendsType.js"; + +export class TemplateLiteralNodeParser implements SubNodeParser { + public constructor(protected childNodeParser: NodeParser) {} + + public supportsNode(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode): boolean { + return ( + node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || node.kind === ts.SyntaxKind.TemplateLiteralType + ); + } + + public createType(node: ts.NoSubstitutionTemplateLiteral | ts.TemplateLiteralTypeNode, context: Context): BaseType { + if (node.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + return new LiteralType(node.text); + } + + const types: BaseType[] = []; + + const prefix = node.head.text; + if (prefix) { + types.push(new LiteralType(prefix)); + } + + for (const span of node.templateSpans) { + types.push(this.childNodeParser.createType(span.type, context)); + + const suffix = span.literal.text; + if (suffix) { + types.push(new LiteralType(suffix)); + } + } + + if (isExtendsType(node)) { + return new TemplateLiteralType(types); + } + + return this.expandTypes(types); + } + + protected expandTypes(types: BaseType[]): BaseType { + let expanded: string[] = [""]; + + for (const type of types) { + // Any `never` type in the template literal will make the whole type `never` + if (type instanceof NeverType) { + return new NeverType(); + } + + try { + const literals = extractLiterals(type); + expanded = expanded.flatMap((prefix) => literals.map((suffix) => prefix + suffix)); + } catch { + return new StringType(); + } + } + + if (expanded.length === 1) { + return new LiteralType(expanded[0]); + } + + return new UnionType(expanded.map((literal) => new LiteralType(literal))); + } +} From 4bba8a8c9dfb9f46cce3cd712ba17d2fb993e63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:53:58 +0200 Subject: [PATCH 03/20] refactor: add proper type to infer map --- src/NodeParser/ConditionalTypeNodeParser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NodeParser/ConditionalTypeNodeParser.ts b/src/NodeParser/ConditionalTypeNodeParser.ts index 6e05b1bd2..2e7543fea 100644 --- a/src/NodeParser/ConditionalTypeNodeParser.ts +++ b/src/NodeParser/ConditionalTypeNodeParser.ts @@ -29,14 +29,14 @@ export class ConditionalTypeNodeParser implements SubNodeParser { const extendsType = this.childNodeParser.createType(node.extendsType, context); const checkTypeParameterName = this.getTypeParameterName(node.checkType); - const inferMap = new Map(); + const inferMap = new Map(); // If check-type is not a type parameter then condition is very simple, no type narrowing needed if (checkTypeParameterName == null) { const result = isAssignableTo(extendsType, checkType, inferMap); return this.childNodeParser.createType( result ? node.trueType : node.falseType, - this.createSubContext(node, context, undefined, result ? inferMap : new Map()), + this.createSubContext(node, context, undefined, result ? inferMap : new Map()), ); } From af3a78f87328578f24e1fd744005e3233c5e76b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:54:17 +0200 Subject: [PATCH 04/20] feat: add new intrinsic type Useful for extends types within conditional nodes --- src/Type/IntrinsicType.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Type/IntrinsicType.ts diff --git a/src/Type/IntrinsicType.ts b/src/Type/IntrinsicType.ts new file mode 100644 index 000000000..e5cae0ba6 --- /dev/null +++ b/src/Type/IntrinsicType.ts @@ -0,0 +1,23 @@ +import type { BaseType } from "./BaseType.js"; +import { PrimitiveType } from "./PrimitiveType.js"; + +export class IntrinsicType extends PrimitiveType { + constructor( + protected method: (v: string) => string, + protected argument: BaseType, + ) { + super(); + } + + public getId(): string { + return `${this.getMethod().name}<${this.getArgument().getId()}>`; + } + + public getMethod(): (v: string) => string { + return this.method; + } + + public getArgument(): BaseType { + return this.argument; + } +} From ec22382da1bac0c553cc912f6136d4a1d2943a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:55:25 +0200 Subject: [PATCH 05/20] refactor: improve intrinsic node parser Added support for returning intrinsic types for extends clauses. Also added support for Capitalize expressions --- src/NodeParser/IntrinsicNodeParser.ts | 40 +++++++++++++++++++-------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/NodeParser/IntrinsicNodeParser.ts b/src/NodeParser/IntrinsicNodeParser.ts index 2acaab4b2..18b735103 100644 --- a/src/NodeParser/IntrinsicNodeParser.ts +++ b/src/NodeParser/IntrinsicNodeParser.ts @@ -6,13 +6,20 @@ import type { BaseType } from "../Type/BaseType.js"; import { LiteralType } from "../Type/LiteralType.js"; import { UnionType } from "../Type/UnionType.js"; import { extractLiterals } from "../Utils/extractLiterals.js"; +import { isExtendsType } from "../Utils/isExtendsType.js"; +import { IntrinsicType } from "../Type/IntrinsicType.js"; +import { StringType } from "../Type/StringType.js"; -export const intrinsicMethods: Record string) | undefined> = { +export const intrinsicMethods = { Uppercase: (v) => v.toUpperCase(), Lowercase: (v) => v.toLowerCase(), Capitalize: (v) => v[0].toUpperCase() + v.slice(1), Uncapitalize: (v) => v[0].toLowerCase() + v.slice(1), -}; +} as const satisfies Record string) | undefined>; + +function isIntrinsicMethod(methodName: string): methodName is keyof typeof intrinsicMethods { + return methodName in intrinsicMethods; +} export class IntrinsicNodeParser implements SubNodeParser { public supportsNode(node: ts.KeywordTypeNode): boolean { @@ -20,19 +27,30 @@ export class IntrinsicNodeParser implements SubNodeParser { } public createType(node: ts.KeywordTypeNode, context: Context): BaseType { const methodName = getParentName(node); - const method = intrinsicMethods[methodName]; - - if (!method) { + if (!isIntrinsicMethod(methodName)) { throw new LogicError(node, `Unknown intrinsic method: ${methodName}`); } - const literals = extractLiterals(context.getArguments()[0]) - .map(method) - .map((literal) => new LiteralType(literal)); - if (literals.length === 1) { - return literals[0]; + const method = intrinsicMethods[methodName]; + const argument = context.getArguments()[0]; + + try { + const literals = extractLiterals(argument) + .map(method) + .map((literal) => new LiteralType(literal)); + + if (literals.length === 1) { + return literals[0]; + } + + return new UnionType(literals); + } catch (error) { + if (isExtendsType(context.getReference())) { + return new IntrinsicType(method, argument); + } + + return new StringType(); } - return new UnionType(literals); } } From 44ab3c5660dd90ee20bfa85bdd34df3981cb32ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:55:49 +0200 Subject: [PATCH 06/20] feat: add new function to check if a node is part of extends clause --- src/Utils/isExtendsType.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Utils/isExtendsType.ts diff --git a/src/Utils/isExtendsType.ts b/src/Utils/isExtendsType.ts new file mode 100644 index 000000000..e5cab9b74 --- /dev/null +++ b/src/Utils/isExtendsType.ts @@ -0,0 +1,27 @@ +import ts from "typescript"; + +/** + * Recursively checks each parent of the given node to determine if it is part of an extends type in a conditional type. + * + * @param node - The node to check. + * @returns Whether the given node is part of an extends type. + */ +export function isExtendsType(node: ts.Node | undefined): boolean { + if (!node) { + return false; + } + + let current = node; + + while (current.parent) { + if (ts.isConditionalTypeNode(current.parent)) { + const conditionalNode = current.parent; + if (conditionalNode.extendsType === current) { + return true; + } + } + current = current.parent; + } + + return false; +} From 9bb08c51dbeb19f96f2580a72164f8e56b70a2d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:56:47 +0200 Subject: [PATCH 07/20] feat: add support for intrinsic types and template literal types in isAssignableTo --- src/Utils/isAssignableTo.ts | 159 +++++++++++++++++++++++++++++++++--- 1 file changed, 148 insertions(+), 11 deletions(-) diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index 0ec183989..e3f1a4eb2 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -19,6 +19,8 @@ import { BooleanType } from "../Type/BooleanType.js"; import { InferType } from "../Type/InferType.js"; import { RestType } from "../Type/RestType.js"; import { NeverType } from "../Type/NeverType.js"; +import { IntrinsicType } from "../Type/IntrinsicType.js"; +import { TemplateLiteralType } from "../Type/TemplateLiteralType.js"; /** * Returns the combined types from the given intersection. Currently only object types are combined. Maybe more @@ -49,7 +51,7 @@ function combineIntersectingTypes(intersection: IntersectionType): BaseType[] { * Returns all object properties of the given type and all its base types. * * @param type - The type for which to return the properties. If type is not an object type or object has no properties - * Then an empty list ist returned. + * Then an empty list is returned. * @return All object properties of the type. Empty if none. */ function getObjectProperties(type: BaseType): ObjectProperty[] { @@ -109,15 +111,7 @@ export function isAssignableTo( // Infer type can become anything if (target instanceof InferType) { - const key = target.getName(); - const infer = inferMap.get(key); - - if (infer === undefined) { - inferMap.set(key, source); - } else { - inferMap.set(key, new UnionType([infer, source])); - } - + setInferredType(target.getName(), source, inferMap); return true; } @@ -182,11 +176,144 @@ export function isAssignableTo( // Check literal types if (source instanceof LiteralType) { + if (target instanceof IntrinsicType) { + const argument = target.getArgument(); + const method = target.getMethod(); + + if (argument instanceof LiteralType) { + const value = method(argument.getValue().toString()); + return isAssignableTo(new LiteralType(value), source, inferMap, insideTypes); + } + + if (argument instanceof StringType || argument instanceof InferType) { + if (argument instanceof InferType) { + setInferredType(argument.getName(), new StringType(), inferMap); + } + const value = method(source.getValue().toString()); + return isAssignableTo(new LiteralType(value), source, inferMap, insideTypes); + } + + if (argument instanceof UnionType) { + return argument + .getTypes() + .reduce( + (isAssignable, type) => + isAssignableTo(new IntrinsicType(method, type), source, inferMap, insideTypes) || + isAssignable, + false, + ); + } + + return false; + } + + if (target instanceof TemplateLiteralType) { + if (!source.isString) { + return false; + } + + let remaining = source.getValue().toString(); + const parts = target.getParts(); + + const isPartAssignable = (part: BaseType, sliceLength: number) => { + const value = remaining.slice(0, sliceLength); + remaining = remaining.slice(sliceLength); + return isAssignableTo(part, new LiteralType(value), inferMap, insideTypes); + }; + + for (const part of parts) { + const type = derefType(part); + if (type instanceof LiteralType) { + const targetValue = type.getValue().toString(); + if (!isPartAssignable(type, targetValue.length)) { + return false; + } + } else if (type instanceof InferType || type instanceof StringType) { + const nextPart = parts[parts.indexOf(type) + 1]; + + if (nextPart instanceof InferType || nextPart instanceof StringType) { + // When the next part is also a non-literal type, we infer one character at a time + if (!isPartAssignable(type, 1)) { + return false; + } + } else if (nextPart instanceof LiteralType) { + // Use remaining value up to the next matching segment, or last match if the next part is the final part + const nextValue = nextPart.getValue().toString(); + const isLastPart = parts.indexOf(nextPart) === parts.length - 1; + const index = isLastPart ? remaining.lastIndexOf(nextValue) : remaining.indexOf(nextValue); + + // If no matching segment is found, the source is not assignable + if (index === -1) { + return false; + } + + if (!isPartAssignable(type, index)) { + return false; + } + } else if (!nextPart) { + // Match the remaining value when there are no more parts + if (!isPartAssignable(type, remaining.length)) { + return false; + } + } + } else if (type instanceof NumberType) { + const match = remaining.match(/^\d+/); + if (match) { + const value = match[0]; + remaining = remaining.slice(value.length); + } else { + return false; + } + } else if (type instanceof UnionType) { + const matchFound = type.getTypes().some((unionPart) => { + const matchLength = + unionPart instanceof LiteralType ? unionPart.getValue().toString().length : 0; + const valueToCheck = remaining.slice(0, matchLength); + const result = isAssignableTo(unionPart, new LiteralType(valueToCheck), inferMap, insideTypes); + if (result) { + remaining = remaining.slice(matchLength); + } + return result; + }); + + if (!matchFound) { + return false; + } + } else if (type instanceof IntrinsicType) { + const argument = type.getArgument(); + + if (argument instanceof LiteralType) { + const targetValue = argument.getValue().toString(); + if (!isPartAssignable(type, targetValue.length)) { + return false; + } + } else if (argument instanceof InferType || argument instanceof StringType) { + if (!isPartAssignable(type, 1)) { + return false; + } + } else if (argument instanceof UnionType) { + if (!isAssignableTo(type, source, inferMap, insideTypes)) { + return false; + } + + remaining = ""; + } + } + } + + return remaining.length === 0; + } + return isAssignableTo(target, getPrimitiveType(source.getValue()), inferMap); } + if (source instanceof StringType && target instanceof TemplateLiteralType) { + // String types are only assignable to template literal types with solely string types + return target.getParts().every((part) => part instanceof StringType); + } + if (target instanceof ObjectType) { - // primitives are not assignable to `object` + // Primitives are not assignable to `object` if ( target.getNonPrimitive() && (source instanceof NumberType || source instanceof StringType || source instanceof BooleanType) @@ -305,3 +432,13 @@ export function isAssignableTo( return false; } + +function setInferredType(key: string, source: BaseType, inferMap: Map): void { + const infer = inferMap.get(key); + + if (infer === undefined) { + inferMap.set(key, source); + } else { + inferMap.set(key, new UnionType([infer, source])); + } +} From 6a8c8ba7a4a02d19c93c47be812d6cc9862de4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:57:13 +0200 Subject: [PATCH 08/20] refactor: rename import in parser --- factory/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/parser.ts b/factory/parser.ts index 6b3ca867a..8fd738e5b 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -42,7 +42,7 @@ import { PrefixUnaryExpressionNodeParser } from "../src/NodeParser/PrefixUnaryEx import { PropertyAccessExpressionParser } from "../src/NodeParser/PropertyAccessExpressionParser.js"; import { RestTypeNodeParser } from "../src/NodeParser/RestTypeNodeParser.js"; import { StringLiteralNodeParser } from "../src/NodeParser/StringLiteralNodeParser.js"; -import { StringTemplateLiteralNodeParser } from "../src/NodeParser/StringTemplateLiteralNodeParser.js"; +import { TemplateLiteralNodeParser } from "../src/NodeParser/TemplateLiteralNodeParser.js"; import { StringTypeNodeParser } from "../src/NodeParser/StringTypeNodeParser.js"; import { SymbolTypeNodeParser } from "../src/NodeParser/SymbolTypeNodeParser.js"; import { TupleNodeParser } from "../src/NodeParser/TupleNodeParser.js"; @@ -109,7 +109,7 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme .addNodeParser(new SatisfiesNodeParser(chainNodeParser)) .addNodeParser(withJsDoc(new ParameterParser(chainNodeParser))) .addNodeParser(new StringLiteralNodeParser()) - .addNodeParser(new StringTemplateLiteralNodeParser(chainNodeParser)) + .addNodeParser(new TemplateLiteralNodeParser(chainNodeParser)) .addNodeParser(new IntrinsicNodeParser()) .addNodeParser(new NumberLiteralNodeParser()) .addNodeParser(new BooleanLiteralNodeParser()) From fc0deb7ca9188d709068324e245f3194f0fd9f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:57:26 +0200 Subject: [PATCH 09/20] chore: fix exports in barrel file --- index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.ts b/index.ts index f4a723d42..e3d04fcf9 100644 --- a/index.ts +++ b/index.ts @@ -54,6 +54,7 @@ export * from "./src/Type/ReferenceType.js"; export * from "./src/Type/RestType.js"; export * from "./src/Type/StringType.js"; export * from "./src/Type/SymbolType.js"; +export * from "./src/Type/TemplateLiteralType.js"; export * from "./src/Type/TupleType.js"; export * from "./src/Type/UndefinedType.js"; export * from "./src/Type/UnionType.js"; @@ -135,7 +136,7 @@ export * from "./src/NodeParser/PrefixUnaryExpressionNodeParser.js"; export * from "./src/NodeParser/PropertyAccessExpressionParser.js"; export * from "./src/NodeParser/RestTypeNodeParser.js"; export * from "./src/NodeParser/StringLiteralNodeParser.js"; -export * from "./src/NodeParser/StringTemplateLiteralNodeParser.js"; +export * from "./src/NodeParser/TemplateLiteralNodeParser.js"; export * from "./src/NodeParser/StringTypeNodeParser.js"; export * from "./src/NodeParser/SymbolTypeNodeParser.js"; export * from "./src/NodeParser/TupleNodeParser.js"; From cdba63a8b665e6816eb57d550527cef82b8ce70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:58:02 +0200 Subject: [PATCH 10/20] test: add test cases for intrinsic types and template literal types to isAssignableTo unit tests --- test/unit/isAssignableTo.test.ts | 168 +++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index f05744ee4..9f2378a7b 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -1,11 +1,14 @@ +import { intrinsicMethods } from "../../src/NodeParser/IntrinsicNodeParser.js"; import { AliasType } from "../../src/Type/AliasType.js"; import { AnnotatedType } from "../../src/Type/AnnotatedType.js"; import { AnyType } from "../../src/Type/AnyType.js"; import { ArrayType } from "../../src/Type/ArrayType.js"; +import type { BaseType } from "../../src/Type/BaseType.js"; import { BooleanType } from "../../src/Type/BooleanType.js"; import { DefinitionType } from "../../src/Type/DefinitionType.js"; import { InferType } from "../../src/Type/InferType.js"; import { IntersectionType } from "../../src/Type/IntersectionType.js"; +import { IntrinsicType } from "../../src/Type/IntrinsicType.js"; import { LiteralType } from "../../src/Type/LiteralType.js"; import { NeverType } from "../../src/Type/NeverType.js"; import { NullType } from "../../src/Type/NullType.js"; @@ -15,6 +18,7 @@ import { OptionalType } from "../../src/Type/OptionalType.js"; import { ReferenceType } from "../../src/Type/ReferenceType.js"; import { RestType } from "../../src/Type/RestType.js"; import { StringType } from "../../src/Type/StringType.js"; +import { TemplateLiteralType } from "../../src/Type/TemplateLiteralType.js"; import { TupleType } from "../../src/Type/TupleType.js"; import { UndefinedType } from "../../src/Type/UndefinedType.js"; import { UnionType } from "../../src/Type/UnionType.js"; @@ -32,6 +36,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new UndefinedType(), new UndefinedType())).toBe(true); expect(isAssignableTo(new VoidType(), new VoidType())).toBe(true); }); + it("returns false for different types", () => { expect(isAssignableTo(new BooleanType(), new NullType())).toBe(false); expect(isAssignableTo(new NullType(), new NumberType())).toBe(false); @@ -41,21 +46,26 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new UndefinedType(), new BooleanType())).toBe(false); expect(isAssignableTo(new ArrayType(new StringType()), new StringType())).toBe(false); }); + it("returns true for arrays with same item type", () => { expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new StringType()))).toBe(true); }); + it("returns false when array item types do not match", () => { expect(isAssignableTo(new ArrayType(new StringType()), new ArrayType(new NumberType()))).toBe(false); }); + it("returns true when source type is compatible to target union type", () => { const union = new UnionType([new StringType(), new NumberType()]); expect(isAssignableTo(union, new StringType())).toBe(true); expect(isAssignableTo(union, new NumberType())).toBe(true); }); + it("returns false when source type is not compatible to target union type", () => { const union = new UnionType([new StringType(), new NumberType()]); expect(isAssignableTo(union, new BooleanType())).toBe(false); }); + it("derefs reference types", () => { const stringRef = new ReferenceType(); stringRef.setType(new StringType()); @@ -70,6 +80,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(stringRef, anotherStringRef)).toBe(true); expect(isAssignableTo(numberRef, stringRef)).toBe(false); }); + it("derefs alias types", () => { const stringAlias = new AliasType("a", new StringType()); const anotherStringAlias = new AliasType("b", new StringType()); @@ -81,6 +92,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(stringAlias, anotherStringAlias)).toBe(true); expect(isAssignableTo(numberAlias, stringAlias)).toBe(false); }); + it("derefs annotated types", () => { const annotatedString = new AnnotatedType(new StringType(), {}, false); const anotherAnnotatedString = new AnnotatedType(new StringType(), {}, false); @@ -92,6 +104,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(annotatedString, anotherAnnotatedString)).toBe(true); expect(isAssignableTo(annotatedNumber, annotatedString)).toBe(false); }); + it("derefs definition types", () => { const stringDefinition = new DefinitionType("a", new StringType()); const anotherStringDefinition = new DefinitionType("b", new StringType()); @@ -103,6 +116,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(stringDefinition, anotherStringDefinition)).toBe(true); expect(isAssignableTo(numberDefinition, stringDefinition)).toBe(false); }); + it("lets type 'any' to be assigned to anything except 'never'", () => { expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); expect(isAssignableTo(new ArrayType(new NumberType()), new AnyType())).toBe(true); @@ -123,6 +137,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new TupleType([new StringType(), new NumberType()]), new AnyType())).toBe(true); expect(isAssignableTo(new UndefinedType(), new AnyType())).toBe(true); }); + it("lets type 'never' to be assigned to anything", () => { expect(isAssignableTo(new AnyType(), new NeverType())).toBe(true); expect(isAssignableTo(new ArrayType(new NumberType()), new NeverType())).toBe(true); @@ -143,6 +158,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new TupleType([new StringType(), new NumberType()]), new NeverType())).toBe(true); expect(isAssignableTo(new UndefinedType(), new NeverType())).toBe(true); }); + it("lets anything to be assigned to type 'any'", () => { expect(isAssignableTo(new AnyType(), new AnyType())).toBe(true); expect(isAssignableTo(new AnyType(), new ArrayType(new NumberType()))).toBe(true); @@ -163,6 +179,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new AnyType(), new TupleType([new StringType(), new NumberType()]))).toBe(true); expect(isAssignableTo(new AnyType(), new UndefinedType())).toBe(true); }); + it("lets anything to be assigned to type 'unknown'", () => { expect(isAssignableTo(new UnknownType(), new AnyType())).toBe(true); expect(isAssignableTo(new UnknownType(), new ArrayType(new NumberType()))).toBe(true); @@ -183,6 +200,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(new UnknownType(), new TupleType([new StringType(), new NumberType()]))).toBe(true); expect(isAssignableTo(new UnknownType(), new UndefinedType())).toBe(true); }); + it("lets 'unknown' only to be assigned to type 'unknown' or 'any'", () => { expect(isAssignableTo(new AnyType(), new UnknownType())).toBe(true); expect(isAssignableTo(new ArrayType(new NumberType()), new UnknownType())).toBe(false); @@ -228,6 +246,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(typeAorB, new UnionType([typeA, typeB]))).toBe(true); expect(isAssignableTo(typeAorB, new UnionType([typeAB, typeB, typeC]))).toBe(false); }); + it("lets tuple type to be assigned to array type if item types match", () => { expect( isAssignableTo(new ArrayType(new StringType()), new TupleType([new StringType(), new StringType()])), @@ -239,6 +258,7 @@ describe("isAssignableTo", () => { isAssignableTo(new ArrayType(new StringType()), new TupleType([new StringType(), new NumberType()])), ).toBe(false); }); + it("lets array types to be assigned to array-like object", () => { const fixedLengthArrayLike = new ObjectType( "fixedLengthArrayLike", @@ -278,6 +298,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(optionalLengthArrayLike, tupleType)).toBe(false); expect(isAssignableTo(nonArrayLike, tupleType)).toBe(false); }); + it("lets only compatible tuple type to be assigned to tuple type", () => { expect( isAssignableTo(new TupleType([new StringType(), new StringType()]), new ArrayType(new StringType())), @@ -335,6 +356,7 @@ describe("isAssignableTo", () => { ), ).toBe(true); }); + it("lets anything except null and undefined to be assigned to empty object type", () => { const empty = new ObjectType("empty", [], [], false); expect(isAssignableTo(empty, new AnyType())).toBe(true); @@ -353,6 +375,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(empty, new TupleType([new StringType(), new NumberType()]))).toBe(true); expect(isAssignableTo(empty, new UndefinedType())).toBe(false); }); + it("lets only compatible object types to be assigned to object type", () => { const typeA = new ObjectType("a", [], [new ObjectProperty("a", new StringType(), true)], false); const typeB = new ObjectType("b", [], [new ObjectProperty("b", new StringType(), true)], false); @@ -365,6 +388,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(typeAB, typeA)).toBe(false); expect(isAssignableTo(typeAB, typeB)).toBe(false); }); + it("does let object to be assigned to object with optional properties and at least one property in common", () => { const typeA = new ObjectType( "a", @@ -375,17 +399,20 @@ describe("isAssignableTo", () => { const typeB = new ObjectType("b", [], [new ObjectProperty("b", new StringType(), false)], false); expect(isAssignableTo(typeB, typeA)).toBe(true); }); + it("does not let object to be assigned to object with only optional properties and no properties in common", () => { const typeA = new ObjectType("a", [], [new ObjectProperty("a", new StringType(), true)], false); const typeB = new ObjectType("b", [], [new ObjectProperty("b", new StringType(), false)], false); expect(isAssignableTo(typeB, typeA)).toBe(false); }); + it("correctly handles primitive source intersection types", () => { const numberAndString = new IntersectionType([new StringType(), new NumberType()]); expect(isAssignableTo(new StringType(), numberAndString)).toBe(true); expect(isAssignableTo(new NumberType(), numberAndString)).toBe(true); expect(isAssignableTo(new BooleanType(), numberAndString)).toBe(false); }); + it("correctly handles intersection types with objects", () => { const a = new ObjectType("a", [], [new ObjectProperty("a", new StringType(), true)], false); const b = new ObjectType("b", [], [new ObjectProperty("b", new StringType(), true)], false); @@ -407,6 +434,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(aAndB, ab)).toBe(true); expect(isAssignableTo(aAndB, aAndB)).toBe(true); }); + it("correctly handles circular dependencies", () => { const nodeTypeARef = new ReferenceType(); const nodeTypeA = new ObjectType("a", [], [new ObjectProperty("parent", nodeTypeARef, false)], false); @@ -428,6 +456,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(nodeTypeA, nodeTypeC)).toBe(false); expect(isAssignableTo(nodeTypeB, nodeTypeC)).toBe(false); }); + it("can handle deep union structures", () => { const objectType = new ObjectType( "interface-src/test.ts-0-53-src/test.ts-0-317", @@ -443,6 +472,7 @@ describe("isAssignableTo", () => { const def = new DefinitionType("NumericValueRef", objectType); expect(isAssignableTo(outerUnion, def)).toBe(true); }); + it("correctly handles literal types", () => { expect(isAssignableTo(new StringType(), new LiteralType("foo"))).toBe(true); expect(isAssignableTo(new NumberType(), new LiteralType("foo"))).toBe(false); @@ -478,4 +508,142 @@ describe("isAssignableTo", () => { expect(isAssignableTo(obj2, new StringType())).toBe(false); expect(isAssignableTo(obj2, new BooleanType())).toBe(false); }); + + it("correctly handle intrinsic string check with literal type", () => { + const literalType = new IntrinsicType(intrinsicMethods.Capitalize, new StringType()); + + expect(isAssignableTo(literalType, new LiteralType("Foo"))).toBe(true); + expect(isAssignableTo(literalType, new LiteralType("foo"))).toBe(false); + expect(isAssignableTo(literalType, new StringType())).toBe(false); + }); + + it("correctly handle intrinsic string check with infer type", () => { + const inferMap = new Map(); + const inferType = new IntrinsicType(intrinsicMethods.Uppercase, new InferType("A")); + + expect(isAssignableTo(inferType, new LiteralType("FOO"), inferMap)).toBe(true); + expect(inferMap.get("A")).toBeInstanceOf(StringType); + + expect(isAssignableTo(inferType, new LiteralType("foo"))).toBe(false); + expect(isAssignableTo(inferType, new StringType())).toBe(false); + }); + + it("correctly handle intrinsic string check with union type", () => { + const inferMap = new Map(); + const unionType = new IntrinsicType( + intrinsicMethods.Lowercase, + new UnionType([new LiteralType("FOO"), new LiteralType("BAR"), new InferType("A")]), + ); + + expect(isAssignableTo(unionType, new LiteralType("foo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toBeInstanceOf(StringType); + + expect(isAssignableTo(unionType, new LiteralType("FOO"))).toBe(false); + expect(isAssignableTo(unionType, new StringType())).toBe(false); + }); + + it("correctly handle template literal", () => { + const templateLiteralType = new TemplateLiteralType([new LiteralType("foo")]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("bar"))).toBe(false); + expect(isAssignableTo(templateLiteralType, new StringType())).toBe(false); + }); + + it("correctly handle template literal with string", () => { + const templateLiteralType = new TemplateLiteralType([new StringType()]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("bar"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new StringType())).toBe(true); + }); + + it("correctly handle template literal with number", () => { + const templateLiteralType = new TemplateLiteralType([new NumberType()]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("123"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(false); + }); + + it("correctly handle template literal with number", () => { + const templateLiteralType = new TemplateLiteralType([new LiteralType("foo"), new NumberType()]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo123"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("foo123bar"))).toBe(false); + }); + + it("correctly handle template literal with infer", () => { + const inferMap = new Map(); + const templateLiteralType = new TemplateLiteralType([ + new LiteralType("f"), + new InferType("A"), + new LiteralType("o"), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toStrictEqual(new LiteralType("o")); + }); + + it("correctly handle template literal with multiple infers", () => { + const inferMap = new Map(); + const templateLiteralType = new TemplateLiteralType([ + new LiteralType("f"), + new InferType("A"), + new StringType(), + new StringType(), + new InferType("B"), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo bar"), inferMap)).toBe(true); + expect(inferMap.get("A")).toStrictEqual(new LiteralType("o")); + expect(inferMap.get("B")).toStrictEqual(new LiteralType("bar")); + }); + + it("correctly handle template literal with infer and literal type as last part", () => { + const inferMap = new Map(); + const templateLiteralType = new TemplateLiteralType([new InferType("A"), new LiteralType("o")]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toStrictEqual(new LiteralType("fo")); + + inferMap.delete("A"); + expect(isAssignableTo(templateLiteralType, new LiteralType("fo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toStrictEqual(new LiteralType("f")); + }); + + it("correctly handle template literal with union", () => { + const templateLiteralType = new TemplateLiteralType([ + new UnionType([new LiteralType("foo"), new LiteralType("bar")]), + new LiteralType("123"), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo123"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("bar123"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(false); + expect(isAssignableTo(templateLiteralType, new LiteralType("foo456"))).toBe(false); + }); + + it("correctly handle template literal with intrinsic string manipulation", () => { + const templateLiteralType = new TemplateLiteralType([ + new IntrinsicType(intrinsicMethods.Uppercase, new LiteralType("f")), + new StringType(), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("Foo"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(false); + }); + + it("correctly handle template literal with intrinsic string manipulation", () => { + const inferMap = new Map(); + const templateLiteralType = new TemplateLiteralType([ + new IntrinsicType( + intrinsicMethods.Lowercase, + new UnionType([new LiteralType("FOO"), new LiteralType("BAR"), new InferType("A")]), + ), + new StringType(), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toBeInstanceOf(StringType); + }); }); From 9365228cfa063ccaf9fd975788bc9b74a1cb4a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:58:59 +0200 Subject: [PATCH 11/20] test: refactor test files and add new test cases for string template literals --- .../main.ts | 5 -- .../schema.json | 23 ------ .../main.ts | 14 ---- .../schema.json | 49 ----------- .../string-template-literals/main.ts | 5 -- .../string-template-literals/schema.json | 23 ------ test/valid-data/template-literals/main.ts | 28 +++++++ test/valid-data/template-literals/schema.json | 82 +++++++++++++++++++ .../types.ts | 0 9 files changed, 110 insertions(+), 119 deletions(-) delete mode 100644 test/valid-data/string-template-expression-literals-import/main.ts delete mode 100644 test/valid-data/string-template-expression-literals-import/schema.json delete mode 100644 test/valid-data/string-template-expression-literals/main.ts delete mode 100644 test/valid-data/string-template-expression-literals/schema.json delete mode 100644 test/valid-data/string-template-literals/main.ts delete mode 100644 test/valid-data/string-template-literals/schema.json create mode 100644 test/valid-data/template-literals/main.ts create mode 100644 test/valid-data/template-literals/schema.json rename test/valid-data/{string-template-expression-literals-import => template-literals}/types.ts (100%) diff --git a/test/valid-data/string-template-expression-literals-import/main.ts b/test/valid-data/string-template-expression-literals-import/main.ts deleted file mode 100644 index 6a6cd3ec1..000000000 --- a/test/valid-data/string-template-expression-literals-import/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MyType } from "./types"; - -export interface MyObject { - value: `_${MyType}`; -} diff --git a/test/valid-data/string-template-expression-literals-import/schema.json b/test/valid-data/string-template-expression-literals-import/schema.json deleted file mode 100644 index 73fbdb00f..000000000 --- a/test/valid-data/string-template-expression-literals-import/schema.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "MyObject": { - "additionalProperties": false, - "properties": { - "value": { - "enum": [ - "_one", - "_two", - "_three" - ], - "type": "string" - } - }, - "required": [ - "value" - ], - "type": "object" - } - } -} diff --git a/test/valid-data/string-template-expression-literals/main.ts b/test/valid-data/string-template-expression-literals/main.ts deleted file mode 100644 index 369f5c90c..000000000 --- a/test/valid-data/string-template-expression-literals/main.ts +++ /dev/null @@ -1,14 +0,0 @@ -type OK = "ok"; -type Result = OK | "fail" | `abort`; -type PrivateResultId = `__${Result}_id`; -type OK_ID = `id_${OK}`; -type Num = `${number}%`; -type Bool = `${boolean}!`; - -export interface MyObject { - foo: Result; - _foo: PrivateResultId; - ok: OK_ID; - num: Num; - bool: Bool; -} diff --git a/test/valid-data/string-template-expression-literals/schema.json b/test/valid-data/string-template-expression-literals/schema.json deleted file mode 100644 index 3f733e782..000000000 --- a/test/valid-data/string-template-expression-literals/schema.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "MyObject": { - "additionalProperties": false, - "properties": { - "_foo": { - "enum": [ - "__ok_id", - "__fail_id", - "__abort_id" - ], - "type": "string" - }, - "bool": { - "enum": [ - "true!", - "false!" - ], - "type": "string" - }, - "foo": { - "enum": [ - "ok", - "fail", - "abort" - ], - "type": "string" - }, - "num": { - "type": "string" - }, - "ok": { - "const": "id_ok", - "type": "string" - } - }, - "required": [ - "foo", - "_foo", - "ok", - "num", - "bool" - ], - "type": "object" - } - } -} diff --git a/test/valid-data/string-template-literals/main.ts b/test/valid-data/string-template-literals/main.ts deleted file mode 100644 index 6739d36a1..000000000 --- a/test/valid-data/string-template-literals/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -type Result = "ok" | "fail" | `abort`; - -export interface MyObject { - foo: Result; -} diff --git a/test/valid-data/string-template-literals/schema.json b/test/valid-data/string-template-literals/schema.json deleted file mode 100644 index 3a857d74a..000000000 --- a/test/valid-data/string-template-literals/schema.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$ref": "#/definitions/MyObject", - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "MyObject": { - "additionalProperties": false, - "properties": { - "foo": { - "enum": [ - "ok", - "fail", - "abort" - ], - "type": "string" - } - }, - "required": [ - "foo" - ], - "type": "object" - } - } -} diff --git a/test/valid-data/template-literals/main.ts b/test/valid-data/template-literals/main.ts new file mode 100644 index 000000000..8f470d217 --- /dev/null +++ b/test/valid-data/template-literals/main.ts @@ -0,0 +1,28 @@ +import { MyType } from "./types"; + +type StringLiteral = "two"; +type Empty = ``; +type NoSubstitution = `one_two_three`; +type Interpolation = `one_${StringLiteral}_three`; +type Union = "one" | StringLiteral | `three`; +type NestedUnion = `_${Union}_`; +type Number = `${number}%`; +type Boolean = `${boolean}!`; +type Any = `one_${any}_three`; +type Definiiton = `${MyType}`; +type Generic = `${T}`; +type Intrinsic = `${Capitalize}`; + +export interface MyObject { + empty: Empty; + noSubstitution: NoSubstitution; + interpolation: Interpolation; + union: Union; + nestedUnion: NestedUnion; + number: Number; + boolean: Boolean; + any: Any; + definition: Definiiton; + generic: Generic<"foo">; + intrinsic: Intrinsic<"foo">; +} diff --git a/test/valid-data/template-literals/schema.json b/test/valid-data/template-literals/schema.json new file mode 100644 index 000000000..6cd3605e5 --- /dev/null +++ b/test/valid-data/template-literals/schema.json @@ -0,0 +1,82 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "any": { + "type": "string" + }, + "boolean": { + "enum": [ + "true!", + "false!" + ], + "type": "string" + }, + "definition": { + "enum": [ + "one", + "two", + "three" + ], + "type": "string" + }, + "empty": { + "const": "", + "type": "string" + }, + "generic": { + "const": "foo", + "type": "string" + }, + "interpolation": { + "const": "one_two_three", + "type": "string" + }, + "intrinsic": { + "const": "Foo", + "type": "string" + }, + "nestedUnion": { + "enum": [ + "_one_", + "_two_", + "_three_" + ], + "type": "string" + }, + "noSubstitution": { + "const": "one_two_three", + "type": "string" + }, + "number": { + "type": "string" + }, + "union": { + "enum": [ + "one", + "two", + "three" + ], + "type": "string" + } + }, + "required": [ + "empty", + "noSubstitution", + "interpolation", + "union", + "nestedUnion", + "number", + "boolean", + "any", + "definition", + "generic", + "intrinsic" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/string-template-expression-literals-import/types.ts b/test/valid-data/template-literals/types.ts similarity index 100% rename from test/valid-data/string-template-expression-literals-import/types.ts rename to test/valid-data/template-literals/types.ts From dee2bb40e8e6a4b3a52d2dd326b1b64192d99c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:59:14 +0200 Subject: [PATCH 12/20] chore: remove log from array function generics test --- test/valid-data/array-function-generics/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/valid-data/array-function-generics/main.ts b/test/valid-data/array-function-generics/main.ts index 9f1d80cbc..71f4cfdeb 100644 --- a/test/valid-data/array-function-generics/main.ts +++ b/test/valid-data/array-function-generics/main.ts @@ -1,4 +1,3 @@ export function arrayGenerics(a: T[], b: T[]): T[] { - console.log(a, b); return b; } From b31e7e180edc9a6d9212e0e52cb90b1689bf3a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:59:28 +0200 Subject: [PATCH 13/20] test: add test cases for string types within intrinsic types --- .../string-literals-intrinsic/main.ts | 8 ++++++++ .../string-literals-intrinsic/schema.json | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/test/valid-data/string-literals-intrinsic/main.ts b/test/valid-data/string-literals-intrinsic/main.ts index b304e2c7d..6351a9da7 100644 --- a/test/valid-data/string-literals-intrinsic/main.ts +++ b/test/valid-data/string-literals-intrinsic/main.ts @@ -4,6 +4,10 @@ type ResultUpper = Uppercase; type ResultLower = Lowercase; type ResultCapitalize = Capitalize; type ResultUncapitalize = Uncapitalize; +type ResultUpperString = Uppercase; +type ResultLowerString = Lowercase; +type ResultCapitalizeString = Capitalize; +type ResultUncapitalizeString = Uncapitalize; export interface MyObject { result: Result; @@ -11,4 +15,8 @@ export interface MyObject { resultLower: ResultLower; resultCapitalize: ResultCapitalize; resultUncapitalize: ResultUncapitalize; + resultUpperString: ResultUpperString; + resultLowerString: ResultLowerString; + resultCapitalizeString: ResultCapitalizeString; + resultUncapitalizeString: ResultUncapitalizeString; } diff --git a/test/valid-data/string-literals-intrinsic/schema.json b/test/valid-data/string-literals-intrinsic/schema.json index 724f98802..187573e6d 100644 --- a/test/valid-data/string-literals-intrinsic/schema.json +++ b/test/valid-data/string-literals-intrinsic/schema.json @@ -23,6 +23,9 @@ ], "type": "string" }, + "resultCapitalizeString": { + "type": "string" + }, "resultLower": { "enum": [ "ok", @@ -32,6 +35,9 @@ ], "type": "string" }, + "resultLowerString": { + "type": "string" + }, "resultUncapitalize": { "enum": [ "ok", @@ -41,6 +47,9 @@ ], "type": "string" }, + "resultUncapitalizeString": { + "type": "string" + }, "resultUpper": { "enum": [ "OK", @@ -49,6 +58,9 @@ "SUCCESS" ], "type": "string" + }, + "resultUpperString": { + "type": "string" } }, "required": [ @@ -56,7 +68,11 @@ "resultUpper", "resultLower", "resultCapitalize", - "resultUncapitalize" + "resultUncapitalize", + "resultUpperString", + "resultLowerString", + "resultCapitalizeString", + "resultUncapitalizeString" ], "type": "object" } From fef4968a98da6ff72da6d249426e3c537960688f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 22:59:41 +0200 Subject: [PATCH 14/20] test: add test case for never type within a template literal type --- test/valid-data/template-literals-never/main.ts | 1 + test/valid-data/template-literals-never/schema.json | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 test/valid-data/template-literals-never/main.ts create mode 100644 test/valid-data/template-literals-never/schema.json diff --git a/test/valid-data/template-literals-never/main.ts b/test/valid-data/template-literals-never/main.ts new file mode 100644 index 000000000..b8ff23b8b --- /dev/null +++ b/test/valid-data/template-literals-never/main.ts @@ -0,0 +1 @@ +export type MyType = `one_${never}_three`; diff --git a/test/valid-data/template-literals-never/schema.json b/test/valid-data/template-literals-never/schema.json new file mode 100644 index 000000000..202072279 --- /dev/null +++ b/test/valid-data/template-literals-never/schema.json @@ -0,0 +1,9 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "not": {} + } + } +} From 947d1fe0fb5db45f7e9a6911622befa2cc2827dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 23:00:15 +0200 Subject: [PATCH 15/20] test: add test cases for intrinsic types within extends clause of conditional types --- .../type-conditional-intrinsic/main.ts | 20 ++++++ .../type-conditional-intrinsic/schema.json | 68 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 test/valid-data/type-conditional-intrinsic/main.ts create mode 100644 test/valid-data/type-conditional-intrinsic/schema.json diff --git a/test/valid-data/type-conditional-intrinsic/main.ts b/test/valid-data/type-conditional-intrinsic/main.ts new file mode 100644 index 000000000..5eded94c7 --- /dev/null +++ b/test/valid-data/type-conditional-intrinsic/main.ts @@ -0,0 +1,20 @@ +type Capitalized = T extends Capitalize ? true : false; +type Uncapitalized = T extends Uncapitalize ? true : false; +type Uppercased = T extends Uppercase ? true : false; +type Lowercased = T extends Lowercase ? true : false; +type Union = T extends Capitalize<"foo" | "bar"> ? true : false; +type Infer = T extends Capitalize ? U : false; + +export type MyObject = { + capitalized: Capitalized<"Foo">; + notCapitalized: Capitalized<"foo">; + uncapitalized: Uncapitalized<"foo">; + notUncapitalized: Uncapitalized<"Foo">; + uppercased: Uppercased<"FOO">; + notUppercased: Uppercased<"Foo">; + lowercased: Lowercased<"foo">; + notLowercased: Lowercased<"FOO">; + union: Union<"Foo">; + infer: Infer<"Foo">; + inferNonMatch: Infer<"foo">; +}; diff --git a/test/valid-data/type-conditional-intrinsic/schema.json b/test/valid-data/type-conditional-intrinsic/schema.json new file mode 100644 index 000000000..1f443a6b5 --- /dev/null +++ b/test/valid-data/type-conditional-intrinsic/schema.json @@ -0,0 +1,68 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "capitalized": { + "const": true, + "type": "boolean" + }, + "infer": { + "type": "string" + }, + "inferNonMatch": { + "const": false, + "type": "boolean" + }, + "lowercased": { + "const": true, + "type": "boolean" + }, + "notCapitalized": { + "const": false, + "type": "boolean" + }, + "notLowercased": { + "const": false, + "type": "boolean" + }, + "notUncapitalized": { + "const": false, + "type": "boolean" + }, + "notUppercased": { + "const": false, + "type": "boolean" + }, + "uncapitalized": { + "const": true, + "type": "boolean" + }, + "union": { + "const": true, + "type": "boolean" + }, + "uppercased": { + "const": true, + "type": "boolean" + } + }, + "required": [ + "capitalized", + "notCapitalized", + "uncapitalized", + "notUncapitalized", + "uppercased", + "notUppercased", + "lowercased", + "notLowercased", + "union", + "infer", + "inferNonMatch" + ], + "type": "object" + } + } +} From 18bd782de1aa34cf54927924c33fad9262caba09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 23:00:44 +0200 Subject: [PATCH 16/20] test: add test cases for template literal types within extends clause of conditional types --- .../main.ts | 22 ++++++ .../schema.json | 74 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 test/valid-data/type-conditional-template-literals/main.ts create mode 100644 test/valid-data/type-conditional-template-literals/schema.json diff --git a/test/valid-data/type-conditional-template-literals/main.ts b/test/valid-data/type-conditional-template-literals/main.ts new file mode 100644 index 000000000..cfbf2433f --- /dev/null +++ b/test/valid-data/type-conditional-template-literals/main.ts @@ -0,0 +1,22 @@ +type String = T extends `${string}` ? T : false; +type Number = T extends `${number}` ? T : false; +type Infer = T extends `${infer A}` ? A : false; +type MixedInfer = T extends `foo${infer A}bar` ? A : false; +type MixedNumber = T extends `foo${number}` ? T : false; +type Capitalized = T extends `${Capitalize<"foo">}` ? T : false; +type Union = T extends `foo${"123" | "456"}` ? T : false; + +export type MyObject = { + string: String<"foo">; + number: Number<"123">; + notNumber: Number<"foo">; + infer: Infer<"foo">; + mixedInfer: MixedInfer<"foo123bar">; + mixedInferNonMatch: MixedInfer<"bar123foo">; + mixedNumber: MixedNumber<"foo123">; + mixedNumberNonMatch: MixedNumber<"bar123">; + capitalized: Capitalized<"Foo">; + union1: Union<"foo123">; + union2: Union<"foo456">; + unionNonMatch: Union<"bar123">; +}; diff --git a/test/valid-data/type-conditional-template-literals/schema.json b/test/valid-data/type-conditional-template-literals/schema.json new file mode 100644 index 000000000..448dad4cd --- /dev/null +++ b/test/valid-data/type-conditional-template-literals/schema.json @@ -0,0 +1,74 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "capitalized": { + "const": "Foo", + "type": "string" + }, + "infer": { + "const": "foo", + "type": "string" + }, + "mixedInfer": { + "const": "123", + "type": "string" + }, + "mixedInferNonMatch": { + "const": false, + "type": "boolean" + }, + "mixedNumber": { + "const": "foo123", + "type": "string" + }, + "mixedNumberNonMatch": { + "const": false, + "type": "boolean" + }, + "notNumber": { + "const": false, + "type": "boolean" + }, + "number": { + "const": "123", + "type": "string" + }, + "string": { + "const": "foo", + "type": "string" + }, + "union1": { + "const": "foo123", + "type": "string" + }, + "union2": { + "const": "foo456", + "type": "string" + }, + "unionNonMatch": { + "const": false, + "type": "boolean" + } + }, + "required": [ + "string", + "number", + "notNumber", + "infer", + "mixedInfer", + "mixedInferNonMatch", + "mixedNumber", + "mixedNumberNonMatch", + "capitalized", + "union1", + "union2", + "unionNonMatch" + ], + "type": "object" + } + } +} From db60e282cc43929d55f285f3e09b4181cdb0f6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 23:02:03 +0200 Subject: [PATCH 17/20] test: add new test case for advanced usage of template literals and conditionals with mapped type This type is the original type from our code which brought me onto this journey of adding support for more advanced inference in the first place. Thought it was good to have it here for reference in the future. --- .../type-mapped-conditional/main.ts | 16 ++++++++++++ .../type-mapped-conditional/schema.json | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 test/valid-data/type-mapped-conditional/main.ts create mode 100644 test/valid-data/type-mapped-conditional/schema.json diff --git a/test/valid-data/type-mapped-conditional/main.ts b/test/valid-data/type-mapped-conditional/main.ts new file mode 100644 index 000000000..5b4e527ec --- /dev/null +++ b/test/valid-data/type-mapped-conditional/main.ts @@ -0,0 +1,16 @@ +type MapProperties, P extends string> = Omit}${string}`> & { + [Key in keyof T as Key extends `${P}${infer U}` + ? U extends Capitalize + ? Uncapitalize + : never + : never]: T[Key]; +}; + +export type MyType = MapProperties< + { + isFoo: boolean; + isBar: string; + isBaz: number; + }, + "is" +>; diff --git a/test/valid-data/type-mapped-conditional/schema.json b/test/valid-data/type-mapped-conditional/schema.json new file mode 100644 index 000000000..00fbb9d88 --- /dev/null +++ b/test/valid-data/type-mapped-conditional/schema.json @@ -0,0 +1,26 @@ +{ + "$ref": "#/definitions/MyType", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyType": { + "additionalProperties": false, + "properties": { + "bar": { + "type": "string" + }, + "baz": { + "type": "number" + }, + "foo": { + "type": "boolean" + } + }, + "required": [ + "bar", + "baz", + "foo" + ], + "type": "object" + } + } +} From 0426c48d019ac4af3a07bc0ba28be2e29b2a3d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Sun, 25 Aug 2024 23:02:20 +0200 Subject: [PATCH 18/20] test: update test files with new integration tests --- test/valid-data-other.test.ts | 9 +++------ test/valid-data-type.test.ts | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/test/valid-data-other.test.ts b/test/valid-data-other.test.ts index 2b6ddd181..a82af3925 100644 --- a/test/valid-data-other.test.ts +++ b/test/valid-data-other.test.ts @@ -26,12 +26,9 @@ describe("valid-data-other", () => { it("string-literals-intrinsic", assertValidSchema("string-literals-intrinsic", "MyObject")); it("string-literals-null", assertValidSchema("string-literals-null", "MyObject")); it("string-literals-hack", assertValidSchema("string-literals-hack", "MyObject")); - it("string-template-literals", assertValidSchema("string-template-literals", "MyObject")); - it("string-template-expression-literals", assertValidSchema("string-template-expression-literals", "MyObject")); - it( - "string-template-expression-literals-import", - assertValidSchema("string-template-expression-literals-import", "MyObject"), - ); + + it("template-literals", assertValidSchema("template-literals", "MyObject")); + it("template-literals-never", assertValidSchema("template-literals-never", "MyType")); it("namespace-deep-1", assertValidSchema("namespace-deep-1", "RootNamespace.Def")); it("namespace-deep-2", assertValidSchema("namespace-deep-2", "RootNamespace.SubNamespace.HelperA")); diff --git a/test/valid-data-type.test.ts b/test/valid-data-type.test.ts index 8e06518fc..74af82411 100644 --- a/test/valid-data-type.test.ts +++ b/test/valid-data-type.test.ts @@ -113,6 +113,7 @@ describe("valid-data-type", () => { it("type-mapped-never", assertValidSchema("type-mapped-never", "MyObject")); it("type-mapped-empty-exclude", assertValidSchema("type-mapped-empty-exclude", "MyObject")); it("type-mapped-annotated-string", assertValidSchema("type-mapped-annotated-string", "*")); + it("type-mapped-conditional", assertValidSchema("type-mapped-conditional", "MyType")); it("type-conditional-simple", assertValidSchema("type-conditional-simple", "MyObject")); it("type-conditional-inheritance", assertValidSchema("type-conditional-inheritance", "MyObject")); @@ -125,7 +126,8 @@ describe("valid-data-type", () => { it("type-conditional-narrowing", assertValidSchema("type-conditional-narrowing", "MyObject")); it("type-conditional-omit", assertValidSchema("type-conditional-omit", "MyObject")); it("type-conditional-jsdoc", assertValidSchema("type-conditional-jsdoc", "MyObject")); - + it("type-conditional-intrinsic", assertValidSchema("type-conditional-intrinsic", "MyObject")); + it("type-conditional-template-literals", assertValidSchema("type-conditional-template-literals", "MyObject")); it("type-conditional-infer", assertValidSchema("type-conditional-infer", "MyType")); it("type-conditional-infer-nested", assertValidSchema("type-conditional-infer-nested", "MyType")); it("type-conditional-infer-recursive", assertValidSchema("type-conditional-infer-recursive", "MyType")); From c48f7c307c7f29eec1bc94d654d67890b03bea62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Mon, 26 Aug 2024 01:02:23 +0200 Subject: [PATCH 19/20] refactor: defer and improve intrinsic argument --- src/Utils/isAssignableTo.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index e3f1a4eb2..f3f082f5e 100644 --- a/src/Utils/isAssignableTo.ts +++ b/src/Utils/isAssignableTo.ts @@ -177,7 +177,7 @@ export function isAssignableTo( // Check literal types if (source instanceof LiteralType) { if (target instanceof IntrinsicType) { - const argument = target.getArgument(); + const argument = derefType(target.getArgument()); const method = target.getMethod(); if (argument instanceof LiteralType) { @@ -242,12 +242,7 @@ export function isAssignableTo( const isLastPart = parts.indexOf(nextPart) === parts.length - 1; const index = isLastPart ? remaining.lastIndexOf(nextValue) : remaining.indexOf(nextValue); - // If no matching segment is found, the source is not assignable - if (index === -1) { - return false; - } - - if (!isPartAssignable(type, index)) { + if (index === -1 || !isPartAssignable(type, index)) { return false; } } else if (!nextPart) { @@ -280,7 +275,7 @@ export function isAssignableTo( return false; } } else if (type instanceof IntrinsicType) { - const argument = type.getArgument(); + const argument = derefType(type.getArgument()); if (argument instanceof LiteralType) { const targetValue = argument.getValue().toString(); From e5477258db0249d7d2529f46c7cf986596c15ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20T=C3=B8mmer=C3=A5s?= Date: Mon, 26 Aug 2024 01:02:31 +0200 Subject: [PATCH 20/20] test: improve test coverage --- test/unit/isAssignableTo.test.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index 9f2378a7b..c9148eb7d 100644 --- a/test/unit/isAssignableTo.test.ts +++ b/test/unit/isAssignableTo.test.ts @@ -542,6 +542,12 @@ describe("isAssignableTo", () => { expect(isAssignableTo(unionType, new StringType())).toBe(false); }); + it("correctly handle intrinsic string check with unknown type", () => { + const unionType = new IntrinsicType(intrinsicMethods.Lowercase, new UnknownType()); + + expect(isAssignableTo(unionType, new UnknownType())).toBe(false); + }); + it("correctly handle template literal", () => { const templateLiteralType = new TemplateLiteralType([new LiteralType("foo")]); @@ -565,13 +571,20 @@ describe("isAssignableTo", () => { expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(false); }); - it("correctly handle template literal with number", () => { + it("correctly handle template literal with literal and number", () => { const templateLiteralType = new TemplateLiteralType([new LiteralType("foo"), new NumberType()]); expect(isAssignableTo(templateLiteralType, new LiteralType("foo123"))).toBe(true); expect(isAssignableTo(templateLiteralType, new LiteralType("foo123bar"))).toBe(false); }); + it("correctly handle template literal with string and literal", () => { + const templateLiteralType = new TemplateLiteralType([new StringType(), new LiteralType("foo")]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(true); + expect(isAssignableTo(templateLiteralType, new LiteralType("bar"))).toBe(false); + }); + it("correctly handle template literal with infer", () => { const inferMap = new Map(); const templateLiteralType = new TemplateLiteralType([ @@ -599,6 +612,19 @@ describe("isAssignableTo", () => { expect(inferMap.get("B")).toStrictEqual(new LiteralType("bar")); }); + it("correctly handle template literal with consecutive infer types", () => { + const inferMap = new Map(); + const templateLiteralType = new TemplateLiteralType([ + new LiteralType("f"), + new InferType("A"), + new InferType("B"), + ]); + + expect(isAssignableTo(templateLiteralType, new LiteralType("foo"), inferMap)).toBe(true); + expect(inferMap.get("A")).toStrictEqual(new LiteralType("o")); + expect(inferMap.get("B")).toStrictEqual(new LiteralType("o")); + }); + it("correctly handle template literal with infer and literal type as last part", () => { const inferMap = new Map(); const templateLiteralType = new TemplateLiteralType([new InferType("A"), new LiteralType("o")]); @@ -621,6 +647,7 @@ describe("isAssignableTo", () => { expect(isAssignableTo(templateLiteralType, new LiteralType("bar123"))).toBe(true); expect(isAssignableTo(templateLiteralType, new LiteralType("foo"))).toBe(false); expect(isAssignableTo(templateLiteralType, new LiteralType("foo456"))).toBe(false); + expect(isAssignableTo(templateLiteralType, new LiteralType("baz"))).toBe(false); }); it("correctly handle template literal with intrinsic string manipulation", () => {