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()) 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"; 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()), ); } 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); } } 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))); + } +} 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; + } +} 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; + } +} diff --git a/src/Utils/isAssignableTo.ts b/src/Utils/isAssignableTo.ts index 0ec183989..f3f082f5e 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,139 @@ export function isAssignableTo( // Check literal types if (source instanceof LiteralType) { + if (target instanceof IntrinsicType) { + const argument = derefType(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 (index === -1 || !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 = derefType(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 +427,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])); + } +} 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; +} diff --git a/test/unit/isAssignableTo.test.ts b/test/unit/isAssignableTo.test.ts index f05744ee4..c9148eb7d 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,169 @@ 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 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")]); + + 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 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([ + 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 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")]); + + 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); + expect(isAssignableTo(templateLiteralType, new LiteralType("baz"))).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); + }); }); 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")); 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; } 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" } 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/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": {} + } + } +} 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 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" + } + } +} 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" + } + } +} 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/string-template-literals/schema.json b/test/valid-data/type-mapped-conditional/schema.json similarity index 59% rename from test/valid-data/string-template-literals/schema.json rename to test/valid-data/type-mapped-conditional/schema.json index 3a857d74a..00fbb9d88 100644 --- a/test/valid-data/string-template-literals/schema.json +++ b/test/valid-data/type-mapped-conditional/schema.json @@ -1,20 +1,23 @@ { - "$ref": "#/definitions/MyObject", + "$ref": "#/definitions/MyType", "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "MyObject": { + "MyType": { "additionalProperties": false, "properties": { - "foo": { - "enum": [ - "ok", - "fail", - "abort" - ], + "bar": { "type": "string" + }, + "baz": { + "type": "number" + }, + "foo": { + "type": "boolean" } }, "required": [ + "bar", + "baz", "foo" ], "type": "object"