Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for infer in template literals, intrinsic types and more #2053

Open
wants to merge 20 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
80ee9e6
feat: add new template literal type
flugg Aug 25, 2024
856fbc9
refactor: rename and improve teplate literal node parser
flugg Aug 25, 2024
4bba8a8
refactor: add proper type to infer map
flugg Aug 25, 2024
af3a78f
feat: add new intrinsic type
flugg Aug 25, 2024
ec22382
refactor: improve intrinsic node parser
flugg Aug 25, 2024
44ab3c5
feat: add new function to check if a node is part of extends clause
flugg Aug 25, 2024
9bb08c5
feat: add support for intrinsic types and template literal types in i…
flugg Aug 25, 2024
6a8c8ba
refactor: rename import in parser
flugg Aug 25, 2024
fc0deb7
chore: fix exports in barrel file
flugg Aug 25, 2024
cdba63a
test: add test cases for intrinsic types and template literal types t…
flugg Aug 25, 2024
9365228
test: refactor test files and add new test cases for string template …
flugg Aug 25, 2024
dee2bb4
chore: remove log from array function generics test
flugg Aug 25, 2024
b31e7e1
test: add test cases for string types within intrinsic types
flugg Aug 25, 2024
fef4968
test: add test case for never type within a template literal type
flugg Aug 25, 2024
947d1fe
test: add test cases for intrinsic types within extends clause of con…
flugg Aug 25, 2024
18bd782
test: add test cases for template literal types within extends clause…
flugg Aug 25, 2024
db60e28
test: add new test case for advanced usage of template literals and c…
flugg Aug 25, 2024
0426c48
test: update test files with new integration tests
flugg Aug 25, 2024
c48f7c3
refactor: defer and improve intrinsic argument
flugg Aug 25, 2024
e547725
test: improve test coverage
flugg Aug 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions factory/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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())
Expand Down
3 changes: 2 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
4 changes: 2 additions & 2 deletions src/NodeParser/ConditionalTypeNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, BaseType>();

// 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<string, BaseType>()),
);
}

Expand Down
40 changes: 29 additions & 11 deletions src/NodeParser/IntrinsicNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,51 @@ 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, ((v: string) => string) | undefined> = {
export const intrinsicMethods = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const intrinsicMethods = {
export const intrinsicMethods: Record<string, (str: string) => string> = {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to use as const satisfies here: Type it directly and avoid the method access, using intrinsicMethods[methodName] will also be easier to read.

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, ((v: string) => string) | undefined>;

function isIntrinsicMethod(methodName: string): methodName is keyof typeof intrinsicMethods {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function isIntrinsicMethod(methodName: string): methodName is keyof typeof intrinsicMethods {

return methodName in intrinsicMethods;
}

export class IntrinsicNodeParser implements SubNodeParser {
public supportsNode(node: ts.KeywordTypeNode): boolean {
return node.kind === ts.SyntaxKind.IntrinsicKeyword;
}
public createType(node: ts.KeywordTypeNode, context: Context): BaseType {
const methodName = getParentName(node);
const method = intrinsicMethods[methodName];

if (!method) {
if (!isIntrinsicMethod(methodName)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!isIntrinsicMethod(methodName)) {
if (!intrinsicMethods[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);
}
}

Expand Down
61 changes: 0 additions & 61 deletions src/NodeParser/StringTemplateLiteralNodeParser.ts

This file was deleted.

73 changes: 73 additions & 0 deletions src/NodeParser/TemplateLiteralNodeParser.ts
Original file line number Diff line number Diff line change
@@ -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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saw this in a lot of functions in your pr, catch-all might hide any kind of other errors that happen, please check errors into the ones you expect them and rethrow any others.

ik throwing errors is bad in general until we fix them but this only hides entirely any kind of faulty implementation that might be happening.

please first check that all your updated tests does not throws anything at all and those catches are only to handle future unknown behaviors.

}
}

if (expanded.length === 1) {
return new LiteralType(expanded[0]);
}

return new UnionType(expanded.map((literal) => new LiteralType(literal)));
}
}
23 changes: 23 additions & 0 deletions src/Type/IntrinsicType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { BaseType } from "./BaseType.js";
import { PrimitiveType } from "./PrimitiveType.js";

export class IntrinsicType extends PrimitiveType {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "intrinsic" is not formally defined in the TypeScript documentation, would be good if you write a jsdoc saying that is for generic built in types like Uppercase and so on...

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;
}
}
17 changes: 17 additions & 0 deletions src/Type/TemplateLiteralType.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading