https://github.com/ericsssan/ez-checker
TypeScript type checker extracted from Ez — per-file inference with cross-file resolution via ModuleResolver
https://github.com/ericsssan/ez-checker
Last synced: about 2 months ago
JSON representation
TypeScript type checker extracted from Ez — per-file inference with cross-file resolution via ModuleResolver
- Host: GitHub
- URL: https://github.com/ericsssan/ez-checker
- Owner: ericsssan
- License: mit
- Created: 2026-06-07T07:22:53.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-07T09:20:12.000Z (about 2 months ago)
- Last Synced: 2026-06-07T09:20:31.527Z (about 2 months ago)
- Language: Zig
- Size: 9.76 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 9
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# ez-checker
A reimplementation of the TypeScript type checker. Just the type system: infer the type of any expression, resolve declarations, narrow through control flow. No emit, no diagnostics, no `tsconfig.json`.
Originally extracted from the [Ez](https://github.com/ericsssan/Ez) linter, but the type inference engine is general-purpose — usable by linters, compilers, language servers, or any tool that needs to reason about TypeScript types.
Built on [es-parser](https://github.com/ericsssan/es-parser). Written in Zig.
---
## What it implements
**Expression inference**
Literals, template literals, arrays, objects, functions, classes, binary/unary operators, ternary, assignments, await, sequence, casts (`as`, `satisfies`, `!`).
**Type resolution**
Primitives, arrays, tuples, union, intersection, object types, function types, `typeof`, `keyof`, indexed access (`T[K]`), mapped types, conditional types (including `infer`), template literal types, generic type references, recursive type aliases.
**Utility types**
`Partial`, `Required`, `Readonly`, `Pick`, `Omit`, `Exclude`, `Extract`, `NonNullable`, `Record`, `ReturnType`, `Parameters`, `Awaited`.
**Control-flow narrowing**
Null/undefined checks, `typeof` guards, `instanceof`, type predicates (`x is T`), assertion functions (`asserts x`), discriminated unions, truthiness, assignment narrowing, early-return narrowing.
**Declarations**
Variables, functions (with overloads), classes (instance + static, inheritance), interfaces (extends, call/construct signatures, index signatures, declaration merging), type aliases, enums (numeric, string, mixed), namespaces (declaration merging), imports.
**Cross-file resolution**
Optional. Implement `ModuleResolver` and set `Checker.module_resolver` to resolve types from imported modules lazily.
---
## Known gaps
See [open issues](https://github.com/ericsssan/ez-checker/issues/9) for the full tracked list. Short version:
- `ConstructorParameters` and `InstanceType` not yet implemented
- `as const` on object literals (arrays work; objects do not)
- Optional chaining (`?.`) does not add `| undefined` to the result type
- `==`/`!=` (loose equality) does not narrow
- `throw` does not narrow subsequent code
- Class + interface declaration merging not supported
- `declare module` / `declare global` augmentation not supported
- Overload resolution picks the first matching signature, not the best one
- Global built-in types (~30 hand-curated entries; no full lib.dom or lib.es*)
---
## Usage
`build.zig.zon`:
```zig
.dependencies = .{
.ez_checker = .{ .path = "../ez-checker" },
.es_parser = .{ .path = "../es-parser" },
},
```
`build.zig`:
```zig
const ez_checker_mod = b.dependency("ez_checker", .{
.target = target,
.optimize = .ReleaseFast,
}).module("ez-checker");
your_module.addImport("ez_checker", ez_checker_mod);
```
---
## API
```zig
const ez = @import("ez_checker");
const Checker = ez.Checker;
const types = ez.types;
// Init from an already-parsed file.
var checker = try Checker.init(allocator, &ast, &semantic_result);
defer checker.deinit();
// Optional: wire cross-file resolution.
checker.file_path = "/abs/path/to/file.ts";
checker.module_resolver = my_module_cache.resolver();
// Query the type of any expression node.
const ty: types.TypeId = checker.typeOf(node_index);
// Classify the result.
if (types.isAny(&checker.store, ty)) { /* any */ }
if (types.containsAny(&checker.store, ty)) { /* any somewhere inside */ }
if (types.isAssignableTo(&checker.store, src, dst)) { /* assignable */ }
```
### `ModuleResolver`
Implement this to enable cross-file type resolution:
```zig
fn resolveImpl(
ctx: *anyopaque,
from_dir: []const u8,
module_spec: []const u8,
export_name: []const u8,
local_store: *types.TypeStore,
gpa: std.mem.Allocator,
) ?types.TypeId {
const cache: *MyCache = @ptrCast(@alignCast(ctx));
return cache.resolve(from_dir, module_spec, export_name, local_store, gpa);
}
checker.module_resolver = .{
.ctx = @ptrCast(my_cache),
.resolve_fn = &resolveImpl,
};
```
---
## License
MIT