cheat sheet
Common TypeScript Errors
A reference for the most frequent TypeScript compiler errors — their meaning, a minimal reproduction, and the correct fix. Covers TS2304, TS2345, TS2339, TS2322, TS2532, TS2554, TS2307, TS2366, TS7006, TS2571, TS2769, TS2693, plus type assertions and ts-expect-error.
Common TypeScript Errors
What it is
A practical reference for the TypeScript compiler errors encountered most often in real codebases. Each entry shows the error code, what it means, a minimal reproduction, and the correct fix — not just a cast or suppression.
TS2304 — Cannot find name
Meaning: TypeScript cannot find a name in the current scope. Usually caused by a missing import, missing @types package, or a typo.
// Error
const result = formatDate(new Date()); // TS2304: Cannot find name 'formatDate'
Fix — import the symbol:
import { formatDate } from "date-fns";
const result = formatDate(new Date(), "yyyy-MM-dd");
Fix — install missing type definitions:
npm install -D @types/node
# Then use: process.env, Buffer, __dirname, etc.
Output: (none — exits 0 on success)
Fix — declare the global in a .d.ts file:
// global.d.ts
declare const __APP_VERSION__: string;
TS2345 — Argument of type X is not assignable to parameter of type Y
Meaning: You are passing a value of the wrong type to a function or method.
function greet(name: string): string {
return `Hello, ${name}`;
}
greet(42); // TS2345: Argument of type 'number' is not assignable to parameter of type 'string'
Fix — pass the correct type:
greet("Alice"); // OK
greet(String(42)); // OK — explicit conversion
Common variant — union type not narrowed:
function process(val: string | null): void {
sendToApi(val); // TS2345 — null not assignable to string
}
// Fix — narrow first
function process(val: string | null): void {
if (val === null) return;
sendToApi(val); // val: string
}
TS2339 — Property does not exist on type
Meaning: You are accessing a property that TypeScript does not know exists on the type.
interface User { id: number; name: string }
const user: User = { id: 1, name: "Alice" };
console.log(user.email); // TS2339: Property 'email' does not exist on type 'User'
Fix — add the property to the type:
interface User { id: number; name: string; email?: string }
Fix — use type narrowing or a type guard:
function getEmail(obj: unknown): string | undefined {
if (typeof obj === "object" && obj !== null && "email" in obj) {
return (obj as { email: string }).email;
}
}
Common variant — accessing property on union before narrowing:
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
function area(s: Shape): number {
return s.radius * Math.PI; // TS2339 — radius doesn't exist on Square
}
// Fix — narrow first
function area(s: Shape): number {
if (s.kind === "circle") return s.radius * Math.PI;
return s.side ** 2;
}
TS2322 — Type X is not assignable to type Y
Meaning: A value or expression is being assigned to a variable or returned from a function whose type is incompatible.
const count: number = "five"; // TS2322: Type 'string' is not assignable to type 'number'
function getCount(): number {
return "five"; // TS2322
}
Fix — use the correct type or widen the target:
const count: number = 5;
const countOrLabel: number | string = "five"; // OK
Common variant — object literal with extra properties:
interface Point { x: number; y: number }
const p: Point = { x: 1, y: 2, z: 3 }; // TS2322 — excess property 'z'
// Fix — remove the extra property
const p: Point = { x: 1, y: 2 };
// Fix — widen the type
interface Point3D extends Point { z: number }
const p: Point3D = { x: 1, y: 2, z: 3 };
TS2532 — Object is possibly undefined
Meaning: You are accessing a property or calling a method on a value that might be undefined. Requires strictNullChecks: true.
const arr = [1, 2, 3];
const first = arr[0]; // first: number | undefined (with noUncheckedIndexedAccess)
console.log(first.toFixed(2)); // TS2532: Object is possibly 'undefined'
Fix — narrow with a null check:
if (first !== undefined) {
console.log(first.toFixed(2)); // OK
}
// Or use optional chaining
console.log(first?.toFixed(2)); // OK — returns undefined if first is undefined
Fix — use the non-null assertion operator (only when you are certain):
console.log(first!.toFixed(2)); // OK — you assert first is not undefined
The non-null assertion
!is a promise to TypeScript, not a runtime check. If the value actually isundefined, you will get a runtime error. Only use it when you are certain the value exists and have a comment explaining why.
TS2554 — Expected N arguments, but got M
Meaning: You are calling a function with the wrong number of arguments.
function greet(name: string, greeting: string): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // TS2554: Expected 2 arguments, but got 1
Fix — provide all required arguments:
greet("Alice", "Hello");
Fix — make the second argument optional:
function greet(name: string, greeting = "Hello"): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // OK — greeting defaults to "Hello"
Fix — use rest parameters if arity varies:
function log(...messages: string[]): void {
console.log(messages.join(" "));
}
log("a", "b", "c"); // OK
TS2307 — Cannot find module or its corresponding type declarations
Meaning: TypeScript cannot resolve the module path, or the module exists but has no type declarations.
import { parse } from "some-library"; // TS2307: Cannot find module 'some-library'
Fix — install the package:
npm install some-library
Output: (none — exits 0 on success)
Fix — install type declarations:
npm install -D @types/some-library
Output: (none — exits 0 on success)
Fix — write your own declaration if no types exist:
// some-library.d.ts
declare module "some-library" {
export function parse(input: string): unknown;
}
Fix — check path aliases in tsconfig if it is a relative import:
{
"compilerOptions": {
"paths": { "@/*": ["src/*"] }
}
}
TS2366 — Function lacks ending return statement and return type does not include undefined
Meaning: Not all code paths in a function return a value, but the return type does not allow undefined. Enabled by noImplicitReturns: true.
function getLabel(code: number): string {
if (code === 200) return "OK";
if (code === 404) return "Not Found";
// TS2366 — what if code is 500?
}
Fix — add a return on all paths:
function getLabel(code: number): string {
if (code === 200) return "OK";
if (code === 404) return "Not Found";
return "Unknown";
}
Fix — include undefined in the return type:
function getLabel(code: number): string | undefined {
if (code === 200) return "OK";
if (code === 404) return "Not Found";
}
Fix — use exhaustive checking with never:
type StatusCode = 200 | 404 | 500;
function getLabel(code: StatusCode): string {
switch (code) {
case 200: return "OK";
case 404: return "Not Found";
case 500: return "Internal Server Error";
}
}
// TypeScript knows all cases are covered — no default needed
TS7006 — Parameter implicitly has an 'any' type
Meaning: A function parameter has no type annotation and TypeScript cannot infer it. Requires noImplicitAny: true (included in strict).
function double(x) { // TS7006: Parameter 'x' implicitly has an 'any' type
return x * 2;
}
Fix — annotate the parameter:
function double(x: number): number {
return x * 2;
}
Common variant — callbacks:
[1, 2, 3].reduce((acc, val) => acc + val, 0); // OK — inferred from array type
const fn = (x) => x * 2; // TS7006 — cannot infer x
const fn = (x: number) => x * 2; // OK
TS2571 — Object is of type 'unknown'
Meaning: You are trying to access properties or call methods on a value typed as unknown, which TypeScript treats as the safe top type. You must narrow unknown before using it.
async function fetchData(): Promise<unknown> {
return (await fetch("/api/data")).json();
}
const data = await fetchData();
console.log(data.name); // TS2571: Object is of type 'unknown'
Fix — narrow with a type guard:
if (typeof data === "object" && data !== null && "name" in data) {
console.log((data as { name: string }).name);
}
Fix — use Zod or a manual type guard:
import { z } from "zod";
const schema = z.object({ name: z.string() });
const parsed = schema.parse(data);
console.log(parsed.name); // OK
Fix — assert with as only when you are certain of the shape:
const typed = data as { name: string };
console.log(typed.name);
unknownis the correct type for data from external sources (API responses, JSON parsing, try/catch errors). It forces you to validate before use, which is the right behavior. Preferunknownoveranyin all new code.
TS2769 — No overload matches this call
Meaning: A function has multiple overloads and none of them match the arguments provided.
function format(value: string): string;
function format(value: number, precision: number): string;
function format(value: string | number, precision?: number): string {
if (typeof value === "number") return value.toFixed(precision ?? 2);
return value.trim();
}
format(42); // TS2769 — number overload requires precision argument
format(42, 2); // OK
format("hello"); // OK
Fix — provide required arguments for the matching overload:
format(42, 2); // matches: format(value: number, precision: number): string
format("hello"); // matches: format(value: string): string
Fix — adjust overload signatures if the restriction is wrong:
function format(value: string): string;
function format(value: number, precision?: number): string; // precision now optional
function format(value: string | number, precision = 2): string {
if (typeof value === "number") return value.toFixed(precision);
return value.trim();
}
format(42); // OK now
format(42, 3); // OK
TS7053 — Element implicitly has an 'any' type because expression of type X can't be used to index type Y
Meaning: You are indexing an object with a key the compiler can't prove is one of its own keys. Often a plain string index over an object whose keys are a specific literal union. Triggered by noImplicitAny: true.
const labels = { en: "Hello", fr: "Bonjour", es: "Hola" };
function greet(lang: string): string {
return labels[lang]; // TS7053 — string can't index { en, fr, es }
}
Fix — narrow the key to the literal union:
type Lang = keyof typeof labels;
function greet(lang: Lang): string {
return labels[lang];
}
Fix — assert after a runtime check:
function greet(lang: string): string {
if (lang in labels) {
return labels[lang as keyof typeof labels];
}
return "Hello";
}
Fix — give the object a wider index signature:
const labels: Record<string, string> = { en: "Hello", fr: "Bonjour", es: "Hola" };
function greet(lang: string): string { return labels[lang] ?? "Hello"; }
The trade-off: a Record<string, string> index signature loses the precise key union. Prefer narrowing the input when you can.
TS18046 — X is of type 'unknown'
Meaning: Closely related to TS2571 but raised in catch-clauses since TS 4.4 changed the default catch parameter type from any to unknown. You must narrow the error before using it.
try {
JSON.parse("not json");
} catch (e) {
console.log(e.message); // TS18046 — 'e' is of type 'unknown'
}
Fix — narrow with instanceof Error:
try {
JSON.parse("not json");
} catch (e) {
if (e instanceof Error) {
console.log(e.message);
} else {
console.log("unknown error:", e);
}
}
Fix — extract once with a typed helper:
function errorMessage(e: unknown): string {
if (e instanceof Error) return e.message;
if (typeof e === "string") return e;
return JSON.stringify(e);
}
try { /* ... */ } catch (e) { console.log(errorMessage(e)); }
Fix — explicit : any on the catch (legacy, discouraged):
try { /* ... */ } catch (e: any) { console.log(e.message); }
The : any annotation is the only place TypeScript lets you opt into any inside a catch; it survives noImplicitAny because you're being explicit.
TS2740 — Type X is missing the following properties from type Y
Meaning: You are assigning an object to a variable typed as Y, but the object lacks one or more required properties. Common when adapting between schema versions or partial mocks.
interface User {
id: number;
name: string;
email: string;
}
const u: User = { id: 1, name: "Alice" };
// TS2740 — Type '{ id: number; name: string; }' is missing the following
// properties from type 'User': email
Fix — supply the missing properties:
const u: User = { id: 1, name: "Alice", email: "alice@example.com" };
Fix — make the missing fields optional in the type:
interface User {
id: number;
name: string;
email?: string;
}
Fix — use Partial<User> if every field should be optional in this context:
const u: Partial<User> = { id: 1, name: "Alice" }; // OK
Fix — use as only when you're producing a deliberately partial test mock:
const u = { id: 1 } as User; // explicit shortcut — write a comment explaining why
TS2741 — Property X is missing in type Y but required in type Z
Meaning: A variant of TS2740 raised for a single missing property — the compiler picked the first missing required field to report. Same fixes apply.
interface Config { host: string; port: number; tls: boolean }
const c: Config = { host: "localhost", port: 3000 };
// TS2741 — Property 'tls' is missing in type ... but required in type 'Config'
Fix — same options as TS2740:
const c: Config = { host: "localhost", port: 3000, tls: false }; // supply it
// or make tls optional: tls?: boolean
TS2531 — Object is possibly 'null'
Meaning: Twin of TS2532 but for the null half of null | T. Same narrowing patterns apply.
const el: HTMLElement | null = document.querySelector(".btn");
el.click(); // TS2531
Fix — narrow:
if (el !== null) el.click();
// or
el?.click();
TS2552 — Cannot find name X. Did you mean Y?
Meaning: Typo. The compiler caught a close-match symbol in scope and is suggesting it. Always read the suggestion — it's correct ~95% of the time.
const arr = [1, 2, 3];
console.log(arr.lenght); // TS2552: Did you mean 'length'?
Fix — accept the suggestion:
console.log(arr.length);
TS2367 — This comparison appears to be unintentional because the types X and Y have no overlap
Meaning: You are using === or !== to compare values whose types have no possible overlap — the comparison is always false. Almost always a real bug.
function isReady(status: "loading" | "done"): boolean {
return status === "ready"; // TS2367 — "ready" can never equal "loading" | "done"
}
Fix — use a value actually in the union:
function isReady(status: "loading" | "done"): boolean {
return status === "done";
}
Fix — widen the type if "ready" is genuinely a possible state:
type Status = "loading" | "done" | "ready";
TS2786 — X cannot be used as a JSX component
Meaning: Specific to React projects — typically caused by mismatched @types/react versions between your app and a library, or by a component returning a type React rejects (e.g. Promise<JSX.Element> outside a Server Component).
Fix — align React types:
npm install -D @types/react@^18 @types/react-dom@^18
Output: (none — exits 0 on success)
Fix — ensure pinned versions in pnpm workspaces:
{
"pnpm": {
"overrides": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0"
}
}
}
TS2459 — Type X has no property Y and no string index signature
Meaning: You're trying to read a property the compiler can't see on a type — typically a destructuring assignment from an opaque value.
function f(opts: { a: number }): void {
const { a, b } = opts; // TS2459 — b doesn't exist
}
Fix — add the property or constrain the input:
function f(opts: { a: number; b?: number }): void {
const { a, b } = opts;
}
TS2614 — Module X has no exported member Y. Did you mean to use 'import Y from X' instead?
Meaning: You did import { foo } from "lib" but foo is the default export. Common when migrating between CommonJS and ESM libraries.
// Error
import { React } from "react";
// Fix
import React from "react";
// or, ESM-friendly:
import * as React from "react";
TS2576 — Property X is a static member of type Y
Meaning: You're trying to access a static member through an instance, or vice versa.
class HttpClient {
static DEFAULT_TIMEOUT = 5000;
baseUrl = "/";
}
const c = new HttpClient();
console.log(c.DEFAULT_TIMEOUT); // TS2576
// Fix — use the class, not the instance
console.log(HttpClient.DEFAULT_TIMEOUT);
TS2693 — X only refers to a type, but is being used as a value here
Meaning: You are trying to use a type alias or interface as a runtime value (e.g., calling new MyType() or passing it as an argument).
type Config = { host: string; port: number };
const config = new Config(); // TS2693: 'Config' only refers to a type, but is being used as a value
Fix — use a class instead of a type alias if you need a constructor:
class Config {
constructor(public host: string, public port: number) {}
}
const config = new Config("localhost", 3000); // OK
Fix — if you need both a type and a constructor, use a class with an interface:
interface IConfig { host: string; port: number }
class Config implements IConfig {
constructor(public host: string, public port: number) {}
}
const config: IConfig = new Config("localhost", 3000);
Type assertion patterns
as Type
Overrides TypeScript's inferred type. Use when you have more information than TypeScript can infer, typically at runtime boundaries.
const el = document.getElementById("app") as HTMLDivElement;
el.style.backgroundColor = "red"; // OK — HTMLDivElement has .style
const data = JSON.parse(raw) as { name: string; age: number };
When it is appropriate:
- DOM queries where you know the element type
- Values from
JSON.parsewhen you have validated the shape - Casting after
Object.keys()orObject.entries()which TypeScript types loosely
When it is a smell:
- Silencing legitimate type errors instead of fixing the underlying issue
- Casting
unknownto a specific type without validating
as unknown as Type
Double assertion. Bypasses all type checking. Necessary in rare scenarios where two types are unrelated.
// Legitimate: converting a mocked object in tests
const req = {} as unknown as Request;
// Legitimate: interop with a legacy API that returns a poorly typed value
const typed = (legacyValue as unknown) as MyNewType;
as unknown as Typeis a last resort. If you find yourself using it frequently, it usually signals a design problem — a missing type definition, an overly strict return type, or a missing overload.
ts-expect-error vs ts-ignore
Both suppress the next TypeScript error on the following line, but they behave differently when the error disappears.
ts-ignore
Suppresses the error unconditionally. If the error is later fixed, the suppression silently remains with no warning.
// @ts-ignore
const x: number = "hello"; // suppressed — no error
ts-expect-error
Asserts that the next line will produce an error. If no error is produced (e.g., after an upgrade that fixed the issue), TypeScript reports an "unused ts-expect-error" warning.
// @ts-expect-error — TS2322: string not assignable to number (intentional test)
const x: number = "hello";
Rule of thumb: Always prefer @ts-expect-error over @ts-ignore. It self-documents the intent and alerts you when the suppression is no longer needed.
Use cases for @ts-expect-error:
- Testing that certain type combinations are correctly rejected
- Temporary workarounds for third-party type bugs (with a link to the issue)
- Overriding type definitions in test files
Type-check only without emitting
Running tsc --noEmit validates types without writing any .js output. This is the standard way to gate CI on type correctness.
npx tsc --noEmit
Output (no errors):
(no output — exit code 0)
Output (with errors):
src/api.ts:12:5 - error TS2339: Property 'userId' does not exist on type 'Request'.
12 const id = req.userId;
~~~~~~
src/utils.ts:8:3 - error TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'.
8 processInput(value);
~~~~~~~~~~~~~~~~~~~~
Found 2 errors.
Use in CI:
- name: Type check
run: npx tsc --noEmit
The exit code is 0 on success and non-zero on any type error, making it compatible with all CI systems.
For Vite or esbuild projects where
noEmit: trueis already set intsconfig.json, just runnpx tscwithout any flag — it is equivalent.
TS error code quick-reference
A categorised cheat-sheet of the errors covered above plus a few more you'll see less often. Use it to identify which section of this article (or which neighbouring article) to consult.
Module / import errors
| Code | Meaning | Section |
|---|---|---|
| TS2304 | Cannot find name | above |
| TS2307 | Cannot find module | above |
| TS2614 | Module has no exported member | above |
| TS2552 | Cannot find name — did you mean | above |
| TS6133 | Declared but never used | enable noUnusedLocals / noUnusedParameters |
| TS1005 / TS1109 | Syntax — expected token | usually a real parse error; check the location |
Type-checking errors
| Code | Meaning | Section |
|---|---|---|
| TS2322 | Type X not assignable to Y | above |
| TS2339 | Property does not exist | above |
| TS2345 | Argument type mismatch | above |
| TS2740 | Missing multiple properties | above |
| TS2741 | Missing a single property | above |
| TS2367 | Comparison has no overlap | above |
| TS2769 | No overload matches | above |
| TS2353 | Object literal has unknown property | excess-property check on a fresh object literal |
| TS2459 | No property + no index signature | above |
Null / undefined narrowing
| Code | Meaning | Section |
|---|---|---|
| TS2531 | Possibly null | above |
| TS2532 | Possibly undefined | above |
| TS18047 | Property may be undefined (with noUncheckedIndexedAccess) | narrow array access |
| TS18048 | Property may be undefined | narrow optional access |
unknown / never / any
| Code | Meaning | Section |
|---|---|---|
| TS2571 | Object is of type unknown | above |
| TS18046 | X is of type unknown (catch clauses) | above |
| TS7006 | Parameter implicitly any | above |
| TS7053 | Element implicitly any (index expression) | above |
| TS2538 | Type cannot be used as an index type | union/symbol used where literal/string/number expected |
Function signatures
| Code | Meaning | Section |
|---|---|---|
| TS2554 | Wrong arity | above |
| TS2366 | Function lacks return statement | above |
| TS2769 | No overload matches | above |
| TS2576 | Static member accessed on instance | above |
Type-vs-value confusion
| Code | Meaning | Section |
|---|---|---|
| TS2693 | Used as value but is a type | above |
| TS2749 | Refers to value but is being used as type | inverse of TS2693 — needs typeof X |
Framework / ecosystem
| Code | Meaning | Section |
|---|---|---|
| TS2786 | Not a valid JSX component | above |
| TS2742 | Inferred type cannot be named | needs an explicit annotation (often from monorepo cross-package refs) |
Common pitfalls
- Reaching for
asfirst — Most type errors signal a real shape mismatch. A cast hides the symptom; narrow or fix the type instead. Reserveasfor runtime boundaries. @ts-ignoreleft in long-term code — Use@ts-expect-errorso the suppression itself complains when no longer needed.- Silencing
anywith// @ts-ignore— The error is the warning; deal with it. If you must, switch tounknownand narrow. - Ignoring TS2367 — The compiler can prove the comparison is always false; the code is almost certainly buggy.
- Letting TS7053 spread — One quiet implicit
anyin an indexer cascades into many. Narrow the key once at the boundary. - Casting through
unknown—(x as unknown) as MyTypeis a last resort. If you write it twice, you have a design problem. - Ignoring TS18046 — Catch-clause errors are
unknownfor a reason. Narrow withinstanceof Erroror run them through a typed extractor. noImplicitAny: false— Disabling this defeats most of TypeScript's value. Keepstrict: truefrom project day one.- Skipping the suggestion in TS2552 — The compiler's spelling suggestion is right most of the time; take it.
- Believing
Omitis strict —Omit<T, "missing-key">returnsTunchanged. UseExceptfromtype-festfor actual checking.
Real-world recipes
Recipe 1 — Centralised error normaliser
A single helper that converts any thrown value into a normalised Error. Lifts TS18046 friction out of every catch block.
function toError(e: unknown): Error {
if (e instanceof Error) return e;
if (typeof e === "string") return new Error(e);
try {
return new Error(JSON.stringify(e));
} catch {
return new Error(String(e));
}
}
try {
JSON.parse("not json");
} catch (e) {
const err = toError(e);
console.log("normalised:", err.message);
}
Output:
normalised: Unexpected token o in JSON at position 1
Recipe 2 — Exhaustive switch with never
Pair a discriminated union with a never-typed default branch so adding a new variant becomes a compile error — closing the TS2366 ("not all paths return") class of bugs at the source.
type Status = "draft" | "published" | "archived";
function color(s: Status): string {
switch (s) {
case "draft": return "#888";
case "published": return "#0a0";
case "archived": return "#a00";
default: {
const _exhaustive: never = s;
return _exhaustive;
}
}
}
console.log(color("published"));
Output:
#0a0
Recipe 3 — Strict object literal type with Except
Using Except from type-fest upgrades Omit's silent typo behaviour into a compile error — surfacing TS2741-equivalent issues earlier.
import type { Except } from "type-fest";
interface User { id: number; name: string; email: string }
type CreateUserBody = Except<User, "id">;
const body: CreateUserBody = { name: "Alice", email: "alice@example.com" }; // OK
// const bad: Except<User, "emial"> = { ... }; // Error — "emial" not a key
Recipe 4 — Type-safe environment variable getter
A getter that returns the right runtime type for each key and rejects typos. Fixes TS7053 at the point of use.
const env = {
PORT: 3000,
HOST: "0.0.0.0",
DEBUG: false,
} as const;
type EnvKey = keyof typeof env;
type EnvValue<K extends EnvKey> = typeof env[K];
function getEnv<K extends EnvKey>(key: K): EnvValue<K> {
const raw = process.env[key];
const fallback = env[key];
if (raw === undefined) return fallback;
if (typeof fallback === "number") return Number(raw) as EnvValue<K>;
if (typeof fallback === "boolean") return (raw === "true") as EnvValue<K>;
return raw as EnvValue<K>;
}
const port = getEnv("PORT");
// const bad = getEnv("PORTT"); // Error TS2345
console.log(port);
Output:
3000
Recipe 5 — JSON.parse safe wrapper with Zod
Turn an unknown from JSON.parse into a fully validated, fully typed value. Fixes TS2571/TS18046 at the boundary instead of sprinkling casts through the codebase.
import { z } from "zod";
const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;
function parseUser(raw: string): User {
const parsed: unknown = JSON.parse(raw);
return UserSchema.parse(parsed); // throws on shape mismatch
}
console.log(parseUser('{ "id": 1, "name": "Alice" }'));
Output:
{ id: 1, name: 'Alice' }
Recipe 6 — in-operator narrowing for TS2339
When you don't control the input shape, in narrows a union before property access — TypeScript's recommended replacement for blanket casts.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(s: Shape): number {
if ("radius" in s) return Math.PI * s.radius ** 2;
return s.side ** 2;
}
console.log(area({ kind: "circle", radius: 3 }));
console.log(area({ kind: "square", side: 4 }));
Output:
28.274333882308138
16
Recipe 7 — Typed fetch wrapper using Awaited
A wrapper that infers the JSON response shape from a Zod schema, eliminating both TS2571 (unknown) and TS2339 (property doesn't exist) at the call site.
import { z } from "zod";
async function getJson<S extends z.ZodTypeAny>(
url: string,
schema: S
): Promise<z.infer<S>> {
const res = await fetch(url);
const raw: unknown = await res.json();
return schema.parse(raw);
}
const UserSchema = z.object({ id: z.number(), name: z.string() });
const user = await getJson("/users/1", UserSchema);
console.log(user.name); // typed as string
Recipe 8 — CI gate: tsc --noEmit plus tags
A repeatable CI step that gates on type correctness and tags the failure category so reviewers know whether the issue is a real type bug or a missing declaration file.
npx tsc --noEmit --pretty false 2>&1 | tee tsc.log
grep -E "TS2(304|307)" tsc.log && echo "::warning::Missing types"
grep -E "TS2(322|339|345|740|741)" tsc.log && echo "::warning::Real type bug"
exit ${PIPESTATUS[0]}
Output:
src/api.ts:12:5 - error TS2339: Property 'userId' does not exist on type 'Request'.
::warning::Real type bug