{"id":18583005,"url":"https://github.com/astahmer/typemorph","last_synced_at":"2025-06-30T00:05:30.998Z","repository":{"id":205909761,"uuid":"714893857","full_name":"astahmer/typemorph","owner":"astahmer","description":"what if zod had a child with ts-morph","archived":false,"fork":false,"pushed_at":"2023-11-29T15:13:41.000Z","size":128,"stargazers_count":9,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-11T00:11:23.074Z","etag":null,"topics":["ast","matching","pattern","ts-morph","typescript","zod"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/astahmer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-11-06T03:59:56.000Z","updated_at":"2024-12-25T12:27:48.000Z","dependencies_parsed_at":"2025-04-10T11:36:51.047Z","dependency_job_id":"11b98455-df6d-4433-98e9-84755a61a4b9","html_url":"https://github.com/astahmer/typemorph","commit_stats":null,"previous_names":["astahmer/typemorph"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/astahmer/typemorph","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Ftypemorph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Ftypemorph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Ftypemorph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Ftypemorph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/astahmer","download_url":"https://codeload.github.com/astahmer/typemorph/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/astahmer%2Ftypemorph/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262685681,"owners_count":23348451,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ast","matching","pattern","ts-morph","typescript","zod"],"created_at":"2024-11-07T00:18:53.816Z","updated_at":"2025-06-30T00:05:30.976Z","avatar_url":"https://github.com/astahmer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# TypeMorph\n\nTypeMorph is a TypeScript library designed to streamline working with abstract syntax trees (ASTs). With a focus on\npattern matching, TypeMorph simplifies the process of analyzing and manipulating TypeScript code.\n\n## Pattern Matching\n\nTypeMorph provides the `ast` class for creating pattern matchers for TypeScript syntax, enabling complex AST queries and\ntransformations to be expressed in a simple and concise manner.\n\n## Installation\n\n```sh\npnpm install typemorph\n```\n\n## Usage\n\nHere is a basic example of how to use TypeMorph to match TypeScript AST nodes:\n\n```typescript\nimport { ast } from 'typemorph'\n\n// Match a string literal\nconst stringMatcher = ast.string('hello')\n// will match: `const myString = 'hello'`\n//                                ^^^^^^^\n\n// Match a numeric literal\nconst numberMatcher = ast.number(42)\n// will match: `const myNumber = 42`\n//                                ^^\n\n// Match any node\nconst anyMatcher = ast.any()\n// will match: `const myNumber = 42`\n//               ^^^^^^^^^^^^^^^^^^^\n\n// Will resolve to any node with a name or identifier of 'something'\nconst namedMatcher = ast.named('something')\n// will match: `const something = 42`\n//                     ^^^^^^^^^\n\n// Match a type-only import declaration\nast.importDeclaration('node:path', 'path', true)\n// will match: `import type * as path from 'node:path'`\n\n// Match a named import declaration\nast.importDeclaration('node:fs', ['writeFile', 'readFile'])\n// will match: `import { writeFile, readFile } from 'node:fs'`\n\n// Match using a tuple pattern for a more complex case\nast.importDeclaration('node:fs', ast.tuple(ast.importSpecifier('writeFile'), ast.rest(ast.any())))\n// will match: `import { writeFile, readFile, readFileSync, readdir } from \"node:fs\";`\n// the ast.rest() pattern will match any number of import specifiers after the previous patterns have been matched.\n```\n\n### Project setup example\n\n```ts\nimport { Node, Project, SourceFile, ts } from 'ts-morph'\nimport { Pattern, ast } from 'typemorph'\n\nconst createProject = () =\u003e {\n  return new Project({\n    compilerOptions: {\n      jsx: ts.JsxEmit.React,\n      jsxFactory: 'React.createElement',\n      jsxFragmentFactory: 'React.Fragment',\n      module: ts.ModuleKind.ESNext,\n      target: ts.ScriptTarget.ESNext,\n      noUnusedParameters: false,\n      noEmit: true,\n      useVirtualFileSystem: true,\n      allowJs: true,\n    },\n    skipAddingFilesFromTsConfig: true,\n    skipFileDependencyResolution: true,\n    skipLoadingLibFiles: true,\n    useInMemoryFileSystem: true,\n  })\n}\n\nconst project = createProject()\n\nconst traverse = \u003cTPattern extends Pattern\u003e(\n  sourceFile: SourceFile,\n  pattern: TPattern,\n  stopOnMatch: boolean = false,\n) =\u003e {\n  let match: Pattern | undefined\n  sourceFile.forEachDescendant((node, traversal) =\u003e {\n    if (pattern.match(node)) {\n      match = pattern\n      stopOnMatch \u0026\u0026 traversal.stop()\n    }\n  })\n\n  return match\n}\n\nconst sourceFile = project.addSourceFileAtPath('./file.tsx')\nconst pattern = traverse(\n  sourceFile,\n  ast.node(ts.SyntaxKind.CallExpression, {\n    expression: ast.ref('fn'),\n    arguments: ast.tuple(ast.ref('arg1'), ast.ref('arg2'), ast.rest(ast.ref('rest'))),\n  }),\n)\n\n// file.tsx = `someFunction({ arg1: { foo: 'bar' }}, true, \"a\", 2, \"c\")`\nif (pattern) {\n  const captured = pattern.collectCaptures()\n  captured.fn // Pattern\u003cIdentifier\u003e\n  captured.fn.lastMatch // Identifier\n  captured.fn.lastMatch.getText() // 'someFunction'\n\n  captured.arg1 // Pattern\u003cObjectLiteral\u003e\n  captured.arg1.lastMatch // ObjectLiteral\n  captured.arg1.lastMatch.getText() // '{ foo: \"bar\" }'\n\n  captured.arg2 // Pattern\u003cTrueKeyword\u003e\n  captured.arg2.lastMatch // TrueKeyword\n\n  captured.rest // Pattern\u003c[StringLiteral, NumericLiteral, StringLiteral]\u003e\n}\n```\n\n## Examples\n\n### Example 1: Escape hatch\n\nAnytime you need to match a node that does not have a dedicated method, you can use the `ast.node` matcher to match any\nAST node of a specific kind.\n\n```ts\nconst specificImportSpecifierMatcher = ast.node(SyntaxKind.ImportSpecifier, {\n  name: ast.identifier('specificName'),\n  propertyName: ast.identifier('specificPropertyName'),\n})\n\n// will match:\n// `import { specificName as specificPropertyName } from 'my-module'`\n// `import { specificName } from 'my-module'`\n```\n\n### Example 1: Flexible Patterns\n\n```ts\nconst flexibleMatcherWithRest = ast.importDeclaration(\n  'node:fs',\n  ast.rest(ast.any()), // This will match any number of import specifiers in the import.\n)\n\n// will match:\n// `import { readFile } from 'fs'`\n// `import { readFile, writeFile } from 'fs'`\n// `import { type writeFile, createReadStream } from 'fs'`\n```\n\n### Example 2: Refining Matchers\n\n```ts\nconst typeImportMatcher = ast.refine(\n  ast.importDeclaration(ast.any(), ast.any(), true), // This matches any import declaration that is a type import.\n  (importDeclarationNode) =\u003e {\n    // This function can further process the node if needed.\n    return importDeclarationNode.isTypeOnly() // Returns true if the import is type-only.\n  },\n)\n\n// will match:\n// `import type { MyType, MyOtherType } from 'my-module'`\n// `import type { MyType as MyRenamedType } from 'another-module'`\n```\n\n### Example 3: Combining Matchers for Complex Patterns\n\n```ts\nconst functionReturningPromiseOfSpecificTypeMatcher = ast.node(SyntaxKind.FunctionDeclaration, {\n  name: ast.identifier('myFunction'), // Match a function declaration named \"myFunction\".\n  type: ast.node(SyntaxKind.TypeReference, {\n    // Match the return type.\n    typeName: ast.identifier('Promise'),\n    typeArguments: ast.tuple(\n      ast.node(SyntaxKind.TypeReference, {\n        typeName: ast.identifier('MySpecificType'), // Match \"Promise\" that resolves to \"MySpecificType\".\n      }),\n    ),\n  }),\n})\n\n// will match:\n// `function myFunction(): Promise\u003cMySpecificType\u003e { ... }`\n```\n\nFor more advanced use-cases, refer to the ~~detailed API documentation provided with the library~~\n[test folder](./tests/pattern-matching.test.ts).\n\n## Performance\n\nThere's a few simple cases benchmarked in the [pattern-matching.bench.ts](./tests/pattern-matching.bench.ts) file. The\nresults are as follows:\n\n```\n  raw traversal - tests/pattern-matching.bench.ts \u003e simple ast.callExpression case\n    1.22x faster than pattern matching\n\n  raw traversal - tests/pattern-matching.bench.ts \u003e simple ast.importDeclaration case\n    1.22x faster than pattern matching\n\n  raw traversal - tests/pattern-matching.bench.ts \u003e simple ast.object case\n    1.03x faster than pattern matching\n\n```\n\nSo, it's not as fast as raw traversal, but it's not too far off either. The performance is good enough for most use\ncases especially when you consider the DX benefits of pattern matching over raw traversal.\n\n## Contributing\n\nContributions are welcome! If you have an idea for an improvement or have found a bug, please open an issue or submit a\npull request.\n\n- `pnpm i`\n- `pnpm build`\n- `pnpm test`\n- `pnpm changeset`\n\nWhen you're done with your changes, please run `pnpm changeset` in the root of the repo and follow the instructions\ndescribed [here](https://github.com/changesets/changesets/blob/main/docs/intro-to-using-changesets.md).\n\ntl;dr: `pnpm changeset` will create a new changeset file in the `.changeset` folder. Please commit this file along with\nyour changes. Don't consume the changeset, as this will be done by the CI.\n\n---\n\nPlease refer to the actual codebase of TypeMorph for more complex and detailed patterns and utilities. This README\nprovides a starting point for understanding and integrating the library into your projects.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fastahmer%2Ftypemorph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fastahmer%2Ftypemorph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fastahmer%2Ftypemorph/lists"}