-
Notifications
You must be signed in to change notification settings - Fork 201
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
base: next
Are you sure you want to change the base?
Changes from all commits
80ee9e6
856fbc9
4bba8a8
af3a78f
ec22382
44ab3c5
9bb08c5
6a8c8ba
fc0deb7
cdba63a
9365228
dee2bb4
b31e7e1
fef4968
947d1fe
18bd782
db60e28
0426c48
c48f7c3
e547725
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 = { | ||||||
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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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); | ||||||
} | ||||||
} | ||||||
|
||||||
|
This file was deleted.
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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))); | ||
} | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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; | ||
} | ||
} |
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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.