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

Conversation

flugg
Copy link
Contributor

@flugg flugg commented Aug 25, 2024

Closes #1355

Adding full type inference for template literals were quite a lot more work than first anticipated, particularly because TemplateLiteralNodeParser would narrow the type to a LiteralType, StringType or UnionType before sent to isAssignableTo in ConditionalNodeParser. My proposed solution is to add a check within TemplateLiteralNodeParser to see if the node exists within an extends clause of a conditional, in which case we create a new TemplateLiteralType where we retain the original information stored in the template literal, which allows us to segment through the parts and verify if the source is assignable.

Additionally, I've done the same for IntrinsicNodeParser, which had the same limitations, where both StringType and InferType as an argument in a condiitonal type would fail.

I've added extensive test cases for both.

flugg added 20 commits August 25, 2024 22:53
Useful for extends types within conditional nodes
Added support for never types and template literal types for extends types
Useful for extends types within conditional nodes
Added support for returning intrinsic types for extends clauses. Also added support for Capitalize<string> expressions
…onditionals 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.
@domoritz
Copy link
Member

@arthurfiorette could you take a look at this?

@arthurfiorette
Copy link
Collaborator

Whoa! Sorry for the 3 months delay :/ I just saw this PR... If this happens again, fell free to tag me multiple times XD

Copy link
Collaborator

@arthurfiorette arthurfiorette left a comment

Choose a reason for hiding this comment

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

General implementation reviewed :)

Ik some breaking changes with past generations will happen but I'd a more detailed explanation of how the final output changed.

Unless I'm wrong here, just deleting older tests might not seem like the best approach, could you describe more about this as well?

@domoritz i'd love your help or from anyone else to review the tests results (main.ts vs schema.json) output in our tests to ensure generated json is the most correct for that situation.


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.

};
} 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 {

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]) {

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.

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...

@@ -182,11 +176,139 @@ export function isAssignableTo(

// Check literal types
if (source instanceof LiteralType) {
if (target instanceof IntrinsicType) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ik object properties are a pain in the ass to handle and parse but most of this implementation is very specific to this use case and hard to maintain, please add more comments about why you are doing it that way.

return isAssignableTo(part, new LiteralType(value), inferMap, insideTypes);
};

for (const part of parts) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

what does this all do?

return false;
}
} else if (type instanceof UnionType) {
const matchFound = type.getTypes().some((unionPart) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

?

if (infer === undefined) {
inferMap.set(key, source);
} else {
inferMap.set(key, new UnionType([infer, source]));
Copy link
Collaborator

Choose a reason for hiding this comment

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

check if infer is a union type and append to it instead of creating N levels of UnionType.

return true;
}
}
current = current.parent;
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can rewrite this into a do {} while (current = current.parent) statement :)

@arthurfiorette
Copy link
Collaborator

Also, nice job @flugg for such high quality implementation :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Type inference won't work with template literals
3 participants