TypeScript From Zero
Common Errors, Strictness Migration and Reading Error Messages
Reading long errors bottom-up, the common error codes, and a measured step-by-step migration to strict mode.
On this page
In one line: most TypeScript pain is not hard types, it is not knowing how to read the message. Learn to read an error from the bottom up, learn the dozen error codes that cover most of your day, and when you move an old project to strict, turn the checks on one at a time and fix each group before the next.
This is the last lesson of the series. The first lessons showed single errors as they came up; here we collect them, show how to read the long ones, and then do a real migration: a small loose project, step by step, with an error count after every step. We finish with @ts-expect-error, any against unknown, and putting the check in CI and in a linter. For interview-style questions on the same topics see Full-Stack Q&A, Lesson 3 and Frontend Depth, Lesson 4. The settings used are from Lesson 0 and Lesson 5.
What I ran / checked. Compiler errors and counts are from TypeScript 7.0.2 (tsc --pretty false) on Node v22.22.0. The migration project is a small one I wrote for this lesson (four files, about 70 lines); its counts are for that project only, not a general rule. For the lint section I used a separate folder with TypeScript 6.0.3, eslint 10.12.0, typescript-eslint 8.71.1 and @eslint/js 10.0.1, because npm install refused to put typescript-eslint 8.71.1 next to TypeScript 7.0.2: its peer range is typescript >=4.8.4 <6.1.0. I did not test whether the lint rules work on TypeScript 7. The CI snippet is not run (I have no CI here); the exit codes it relies on I did run. I did not read the TypeScript handbook for this lesson.
Words used in this lesson
- Diagnostic is the compiler's word for an error, warning or suggestion. An error has a code such as
TS2322. - Strict flag is one of the checks inside
strict(noImplicitAny,strictNullChecksand so on). - Migration here means moving a project from loose or plain JavaScript to strict TypeScript.
allowJsletstscread.jsfiles.checkJsmakes it also type check them.- Type assertion is
x as T: you tell the checker to trust you. - Suppression comment is
// @ts-ignoreor// @ts-expect-error: it hides an error on the next line. - Linter is a tool (ESLint) that reads code for bad patterns. It is not a type checker, but with type information it can see more.
- CI (continuous integration) is a server that runs your checks on every change.
- Exit code is the number a program returns:
0means success.
Reading a long error: bottom up
A short error is easy. A long one looks scary because tsc prints a chain: a top line that says what failed, then indented lines that say why, each one going deeper. The top line is the symptom. The last indented line is usually the cause. Here is a nested object passed to a function:
type Order = { id: number; customer: { name: string; address: { city: string; zip: string } }; items: { sku: string; qty: number }[] };function ship(o: Order) { return o.id; }const input = { id: 1, customer: { name: "Ada", address: { city: "London", zip: 12345 } }, items: [{ sku: "a", qty: 1 }, { sku: "b", qty: "2" }],};ship(input);src/e.ts(9,6): error TS2345: Argument of type '{ id: number; customer: { name: string; address: { city: string; zip: number; }; }; items: ({ sku: string; qty: number; } | { sku: string; qty: string; })[]; }' is not assignable to parameter of type 'Order'. The types of 'customer.address.zip' are incompatible between these types. Type 'number' is not assignable to type 'string'.Read it from the bottom: "number is not assignable to string", on the property customer.address.zip. That is the fix: the zip is a number in my data and a string in the type. The long first line is only a dump of the whole argument; you do not need to read it.
Two lessons from this one. First, tsc told me about zip but not about qty: "2", which is also wrong. It reports the first difference it finds in the chain. After I fix zip, the next run shows the qty problem. Fix one, run again. Second, an error can sit far from the place you wrote the wrong thing: the mistake was in input, and the error is on the call ship(input).
A deeper chain, a Promise with a wrong item inside:
function load(): Promise<{ id: number; tags: string[] }> { return Promise.resolve({ id: 1, tags: [1, 2] });}src/e.ts(11,3): error TS2322: Type 'Promise<{ id: number; tags: number[]; }>' is not assignable to type 'Promise<{ id: number; tags: string[]; }>'. Type '{ id: number; tags: number[]; }' is not assignable to type '{ id: number; tags: string[]; }'. Types of property 'tags' are incompatible. Type 'number[]' is not assignable to type 'string[]'. Type 'number' is not assignable to type 'string'.Five levels deep. The last line is the answer: a number where a string is needed, and the line above it says it is inside tags. A third pattern is a callback with the wrong parameter, where the cause is a missing property:
error TS2322: Type '(req: { path: string; }) => Promise<string>' is not assignable to type '(req: { url: string; }) => Promise<string>'. Types of parameters 'req' and 'req' are incompatible. Property 'path' is missing in type '{ url: string; }' but required in type '{ path: string; }'.The bottom line says: the caller will give an object with url, but my function wants path.
A routine for any error. 1. Read the code TSxxxx and the first line to know the kind. 2. Jump to the last indented line for the cause. 3. Hover the variable in your editor to see the type TypeScript worked out. 4. Fix the first error in the list only, then run again, because one early mistake often causes several later ones. 5. Search for the code plus a few words from the message.
The errors you will see most
I put all of these in one file with the Lesson 0 config (strict, noUncheckedIndexedAccess) and ran tsc. Here are the real messages, with what they mean and the usual fix.
| Code | Real message | Usually means | Usual fix |
|---|---|---|---|
| TS2322 | Type 'string' is not assignable to type 'number'. | A value has the wrong type for where it goes (assignment, return, property). | Fix the value or the type. Read the lines under it. |
| TS2339 | Property 'age' does not exist on type '{ name: string; }'. | A typo, or the type is missing the field. | Fix the name, or add the field to the type. |
| TS2345 | Argument of type 'number' is not assignable to parameter of type 'string'. | A function call with the wrong kind of argument. | Pass the right type, or change the parameter type. |
| TS7006 | Parameter 'x' implicitly has an 'any' type. | A parameter with no type and nothing to infer it from. | Write the type: (x: number). |
| TS18048 | 'v' is possibly 'undefined'. | A variable that may be undefined is used. | Check first: if (v), v?.length, or ?? default. |
| TS2532 | Object is possibly 'undefined'. | The same, for an expression such as arr[0] with noUncheckedIndexedAccess. | Same fix: check, or arr[0]?.x. |
| TS2564 | Property 'name' has no initializer and is not definitely assigned in the constructor. | A class field that is never set. | Give it a value, set it in the constructor, or make it optional (name?: string). |
| TS2307 | Cannot find module './nothing.js' or its corresponding type declarations. | A wrong path, a missing package, or a missing .d.ts. | Fix the path, install the package, or declare it (Lesson 5). |
| TS7053 | Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; }'. | Using a plain string as a key on an object with fixed keys. | Type the object as Record<string, T>, or narrow the key to keyof typeof obj. |
| TS2304 | Cannot find name 'foo'. | A name that is not declared or not imported. | Import it, declare it, or fix the typo. |
| TS2741 | Property 'y' is missing in type '{ x: number; }' but required in type 'P'. | An object is missing a required property. | Add it, or make it optional. |
| TS2554 | Expected 2 arguments, but got 1. | The wrong number of arguments. | Add or remove an argument. |
| TS2353 | Object literal may only specify known properties, and 'z' does not exist in type 'P'. | An extra property in an object literal (often a typo). | Remove it, or add it to the type. |
| TS2349 | This expression is not callable. (and Type 'Number' has no call signatures.) | Calling something that is not a function. | Check what the value really is. |
| TS2367 | This comparison appears to be unintentional because the types '"a" | "b"' and '"c"' have no overlap. | An === that can never be true. | Fix the value, or the type. |
| TS2352 | Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. | An as between unrelated types. | Do the real conversion (Number(x)), or validate. |
(The table has 16 rows, more than ten; the first twelve are the ones I meet daily.) Four notes:
- TS2552 versus TS2304. For an undeclared name close to a real one,
tscsaysTS2552: Cannot find name 'undefinedVar'. Did you mean 'undefined'?. Without a near match, it saysTS2304: Cannot find name 'foo'.Same problem, different code. - TS18048 versus TS2532. Both mean "may be
undefined". In my file,v.lengthon a variablevgaveTS18048: 'v' is possibly 'undefined', anda.b.cgaveTS18048: 'a.b' is possibly 'undefined', butarr[0].toFixed()gave the olderTS2532: Object is possibly 'undefined', becausearr[0]is an expression and not a name. - TS2564 and
erasableSyntaxOnly. The shortcutconstructor(public name: string)fixes TS2564, but it is rejected byerasableSyntaxOnly(TS1294, Lesson 0). Declare the field and set it in the constructor. - Fixes that I compiled. I checked that
Record<string, string>for the object, ak is keyof typeof oguard,v?.length ?? 0, a field with a default, and an optional field all compile without errors.
any against unknown
When you do not know a type, you have two choices, and they are opposites.
function useAny(x: any) { return x.foo.bar(); }function useUnknown(x: unknown) { return x.foo; }const j = JSON.parse("{}");const k: unknown = JSON.parse("{}");const s1: string = j;const s2: string = k;src/e.ts(11,42): error TS18046: 'x' is of type 'unknown'.src/e.ts(15,7): error TS2322: Type 'unknown' is not assignable to type 'string'.useAny and s1 compiled with no error: any turns the checker off for that value, and it spreads (j is any, so everything made from it is any). unknown means "something, I do not know what": you may hold it and pass it, but you must narrow it before you use it (typeof, in, instanceof, or a Zod schema, Lesson 6). Use unknown for outside data and for catch (e). Use any almost never.
@ts-expect-error against @ts-ignore
Both comments hide the error on the next line. They differ when there is no error:
// @ts-ignoreconst a: number = "x";// @ts-expect-errorconst b: number = "x";// @ts-expect-errorconst c: number = 5;// @ts-ignoreconst d: number = 5;src/e.ts(5,1): error TS2578: Unused '@ts-expect-error' directive.That was the only message. Lines 1 to 4 were silent. @ts-ignore on line 7 (a correct line) also stayed silent forever. @ts-expect-error says "I expect an error here", and if the error goes away, TypeScript tells you to remove the comment. That makes it the safer one:
- Use
@ts-expect-errorin tests of types (a line that must fail) and for a known problem with a note:// @ts-expect-error library types are wrong, see issue #123. - Avoid
@ts-ignore. It stays after the code is fixed and may later hide a different error on that line. - Neither is a fix. Prefer a narrowing check, a better type, or
unknown.
A last pitfall, strictFunctionTypes. A handler that accepts only a mouse event cannot be used where a mouse-or-keyboard handler is wanted:
error TS2322: Type '(e: MouseEvent) => void' is not assignable to type 'Handler'. Types of parameters 'e' and 'e' are incompatible. Type 'KeyboardEvent | MouseEvent' is not assignable to type 'MouseEvent'. Type 'KeyboardEvent' is missing the following properties from type 'MouseEvent': button, buttons, clientX, clientY, and 14 more.Read the bottom: the caller may pass a keyboard event, and your function would break on it.
Migrating a loose project to strict
Do not turn on strict in a big project and look at 4000 errors. Go in steps, keep the build green at each step, and commit after each. I made a small project to show the steps: four JavaScript files (utils, user, api, main) written the way loose code is written: no types, a class that sets this.name without declaring it, a catch (e) that reads e.message, a function that uses this, a wrong .bind call and an argv[0].length.
Step 1: let TypeScript read the JavaScript (allowJs)
{ "compilerOptions": { "target": "es2022", "module": "nodenext", "allowJs": true, "checkJs": false, "noEmit": true, "types": ["node"], "strict": false }, "include": ["src"] }tsc printed nothing, exit code 0. With allowJs the JS files are part of the project (imports between .ts and .js work) and nothing is checked yet. Set strict to false explicitly here: since TypeScript 6.0, strict is on by default (Lesson 0), so a "loose" project must say so.
Running the JavaScript showed a real bug that the types would have found:
TypeError: Cannot read properties of undefined (reading 'length') at .../src/main.js:11:21It is argv[0].length when no argument is given.
Step 2: check the JavaScript (checkJs)
Set "checkJs": true. Now tsc reads the JS and infers types.
src/user.js(6,36): error TS2339: Property 'balance' does not exist on type 'Account'.One error, because with strict off most things quietly become any. You can also opt in one file at a time with // @ts-check on the first line and keep checkJs off; I checked a file with JSDoc types that way:
// @ts-check/** * @param {number} a * @param {string} [label] * @returns {string} */export function fmt(a, label = "n") { return label + a.toFixed(1); }fmt("1");src/jsdoc.js(10,5): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.JSDoc comments are the way to type a JavaScript file that you cannot rename yet. These two steps give you value without touching the code.
Step 3: rename to .ts, still strict: false
I renamed the four files to .ts (and changed the imports to .ts, with allowImportingTsExtensions). Four errors, all TS2339: the class used this.name and this.balance without declaring them. In a .ts class, fields must be declared:
export class Account { name: string; balance = 0; constructor(name) { this.name = name; }After that: 0 errors with strict: false. The code is TypeScript now, and still loose.
Step 4: the strict flags, one at a time
Now turn on the flags of strict one by one, in this order. Counts are for my small project:
| Step | Flag turned on | Errors | What they were |
|---|---|---|---|
| 4a | noImplicitAny | 10 | TS7006: parameters with no type |
| 4b | strictNullChecks | 1 | TS2345: User | undefined passed to makeLabel |
| 4c | strictFunctionTypes, strictPropertyInitialization, alwaysStrict | 0 | nothing to fix here |
| 4d | strictBindCallApply | 1 | TS2554: Expected 3 arguments, but got 4. on add.bind(null, "5", 5, 6) |
| 4e | noImplicitThis | 1 | TS2683: 'this' implicitly has type 'any' because it does not have a type annotation. |
| 4f | useUnknownInCatchVariables | 1 | TS18046: 'e' is of type 'unknown'. on e.message |
| 5 | noUncheckedIndexedAccess (not in strict) | 2 | TS2532 on first(["a"]).toUpperCase() and on argv[0].length |
Why this order: noImplicitAny first, because until the any parameters are typed, the other checks have nothing to look at. In my first try I turned on every flag in a row without fixing, and strictNullChecks added no errors at all (the count stayed at 14): the ten untyped parameters were any, and any hides null mistakes. That is why you fix noImplicitAny before moving on.
The fixes were small:
// 4a type the parameters (and give the shapes a name)export type Item = { price: number; qty: number };export type User = { id: number; name: string; email: string };export function sum(items: Item[]) { /* ... */ }export function findUser(users: User[], id: number) { return users.find((u) => u.id === id); }// 4b the undefined case was realif (hit) console.log(makeLabel(hit));// 4d the call had one argument too manyexport const add5 = add.bind(null, 5);// 4e an arrow function keeps `this` from the objectinc() { return () => this.count + 1; }// 4f narrow the unknownconsole.log("failed:", e instanceof Error ? e.message : e);// 5 the argv bug from step 1console.log(argv[0]?.length ?? 0);I also tried the same in one go: with "strict": true and noUncheckedIndexedAccess together on the untouched .ts files, tsc reported 18 errors. Doing it in steps turned 18 errors into six small, ordered jobs. The final config, with 0 errors:
{ "compilerOptions": { "target": "es2022", "module": "nodenext", "strict": true, "noUncheckedIndexedAccess": true, "noEmit": true, "skipLibCheck": true, "types": ["node"] }, "include": ["src"] }What strict does not catch in this example. const res = await fetch(url); return await res.json(); still returns any, because Response.json() is typed Promise<any>. strict cannot fix that; a schema at the edge can (Lesson 6). And load can now return any | null to every caller.
In a big project the same order works with a twist: you cannot fix all the errors at once. Options I would use, in order of preference: (1) turn a flag on for the whole project and fix all errors, if there are fewer than a few dozen; (2) move files to .ts and strict one folder at a time, with a second tsconfig for the strict folder that includes only the folders already clean (a tsc run with both configs in CI); (3) as a last resort, // @ts-expect-error with a TODO on the lines you cannot fix today, so the count can only go down. Avoid mass as any.
Type checking in CI
tsc is the type checker, so it belongs in CI. I ran a file with one error three ways:
| Command | Wrote dist/a.js? | Exit code |
|---|---|---|
tsc (emit on) | Yes, despite the error | 2 |
tsc --noEmitOnError | No | 1 |
tsc --noEmit | No (nothing is ever written) | 1 |
Any non-zero code fails a CI job. Note the first row: by default, tsc writes JavaScript even when there are errors, so a build script that ignores the exit code can ship broken code. For a project where a bundler writes the output, use tsc --noEmit as a check. A sketch (not run):
{ "scripts": { "typecheck": "tsc --noEmit", "lint": "eslint .", "test": "node --test" } }# .github/workflows/ci.yml (not run)on: [push, pull_request]jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 22 } - run: npm ci - run: npm run typecheck - run: npm run lint - run: npm testAdd "incremental": true to speed up repeat runs, and for a monorepo use tsc -b (Lesson 5). Also run the type check in your editor, so you see the error before CI does.
Lint rules that help
A linter does not replace tsc. It catches things tsc is happy with. In a separate folder (TypeScript 6.0.3, see the note at the top) I set up ESLint with typescript-eslint and its strictTypeChecked preset, which uses type information, and ran it on this file:
async function save(x: number) { return x; }export function run(input: any, maybe?: { v: string }) { save(1); const n = input.count; const s = maybe!.v; // @ts-ignore const bad: number = "x"; return [n, s, bad];}tsc printed nothing and exited 0 on it, because @ts-ignore hid the real error and the rest is legal. ESLint reported 8 problems:
1:1 error Async function 'save' has no 'await' expression @typescript-eslint/require-await3:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any4:3 error Promises must be awaited, end with a call to .catch, ... @typescript-eslint/no-floating-promises5:9 error Unsafe assignment of an `any` value @typescript-eslint/no-unsafe-assignment5:19 error Unsafe member access .count on an `any` value @typescript-eslint/no-unsafe-member-access6:13 error Forbidden non-null assertion @typescript-eslint/no-non-null-assertion7:3 error Use "@ts-expect-error" instead of "@ts-ignore", ... @typescript-eslint/ban-ts-comment9:3 error Unsafe return of a value of type `any[]` @typescript-eslint/no-unsafe-returnThe rules worth knowing:
no-floating-promises: a promise that is not awaited (save(1)). Its error is silent, becausetscdoes not mind.no-explicit-anyand theno-unsafe-*family: stopanyfrom spreading.ban-ts-comment: allows@ts-expect-error(with a description) and bans@ts-ignore.no-non-null-assertion: the!operator. It is a cast that says "this is not null"; a runtimeTypeErroris the cost when you are wrong.consistent-type-imports: keepsimport typeconsistent (Lesson 5). I did not run this one.
The config was a few lines: eslint.configs.recommended, tseslint.configs.strictTypeChecked, and parserOptions: { projectService: true } so ESLint can ask TypeScript for types. The type-aware rules are slower than plain ones.
Try it
- Write a function with a nested object argument that has two wrong fields. Run
tsc. How many errors are reported? Fix one and run again. - Make a file that triggers
TS2322,TS2339,TS2345andTS7006. Read each from the bottom line up. - Write
// @ts-expect-errorabove a correct line. What doestscsay? - Take a small JS file, add
// @ts-checkand a JSDoc@param {number}. Call it with a string. What happens? - Turn
strictoff, then on again. Which of the four errors in exercise 2 disappear when it is off? - Make
let n = JSON.parse("1")andlet m: unknown = JSON.parse("1"). Which can you pass to a function that takesstring?
Answers.
- One error, for the first mismatch (
customer.address.zipin my run). After fixing it, the next run showed the second one. - Each has the cause in the last line, such as
Type 'string' is not assignable to type 'number'. error TS2578: Unused '@ts-expect-error' directive.error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'., withcheckJsoff.TS7006(needsnoImplicitAny).TS2322,TS2339andTS2345stay. (In my Lesson 0 test,TS18048also disappeared withstrictoff.)- The
anyone (n) compiles, and it is the dangerous one.mis refused withType 'unknown' is not assignable to type 'string'.
Interview questions
How do you read a long TypeScript error? (Must know)
From the bottom. The first line says what failed, and the indented lines go deeper, so the last one is usually the cause. In my test of a nested Order object, the long first line was a dump of my argument, and the last line said Type 'number' is not assignable to type 'string' on customer.address.zip. I also know that tsc shows the first difference only, so I fix it and run again.
What is the difference between any and unknown? (Must know)
any switches off checking and spreads to everything made from it: const j = JSON.parse(...) is any, so const s: string = j compiled. unknown means "I do not know"; I can hold it but must narrow it before I use it, so x.foo gave TS18046 and const s: string = k failed. I use unknown for outside data and catch variables, and almost never any.
@ts-ignore or @ts-expect-error? (Must know)
@ts-expect-error. Both hide the next line's error, but if the line has no error, @ts-expect-error itself fails with TS2578: Unused '@ts-expect-error' directive, so a stale suppression cannot stay. @ts-ignore stayed silent on a correct line in my test. I add a short reason after it, and I prefer to fix the type instead.
What does TS2322 mean, and what do you do? (Must know)
"Type A is not assignable to type B": a value of the wrong type is put where B is expected (an assignment, a return, a property). I read the lines under it, because they say why, for example Type 'undefined' is not assignable to type 'string' for string | undefined. Then I fix the value or widen the type, but I check the value first, not the type.
What is the difference between TS18048 and TS2532? (Senior signal)
Both say a value may be undefined. In my run, TS18048: 'v' is possibly 'undefined' appeared when the thing was a named variable or property path, and the older TS2532: Object is possibly 'undefined' appeared for an expression like arr[0]. The fix is the same: check first (if, ?., ??).
How would you migrate a large JavaScript project to TypeScript? (Senior signal)
In steps with a green build at every step. I add allowJs and a tsconfig first, then checkJs (or // @ts-check per file) with JSDoc types where useful, then I rename files to .ts a folder at a time. Then I turn on the strict flags one by one, starting with noImplicitAny, because until the anys are typed, strictNullChecks shows nothing. I commit after each step. In my example project, turning all flags on at once gave 18 errors, and in steps I got ten, then one at a time.
Why is noImplicitAny the first strict flag you turn on? (Senior signal)
Because an any hides other mistakes. In my project, I turned on all flags without fixing anything, and strictNullChecks added no errors (the count stayed at 14), since ten parameters were any. After I typed them, strictNullChecks found a real User | undefined bug.
What does noUncheckedIndexedAccess do, and why is it separate from strict? (Senior signal)
It adds | undefined to array[i] and object[key] reads. It found argv[0].length, a crash I saw at runtime in step 1 of my migration. It is not in strict, I believe because it creates many errors in old code that is otherwise fine, so it is opt-in. I turn it on last in a migration and always in a new project.
What are the common causes of TS2307? (Must know)
A wrong relative path; a missing file extension under nodenext (that one has its own TS2835); a package that is not installed; or a .json import that is not set up for TypeScript. The message is Cannot find module './nothing.js' or its corresponding type declarations. I check the path, then node_modules, then add a declare module (Lesson 5).
How do you type-check in CI? (Must know)
I run tsc --noEmit (or tsc -b for project references) as its own step, plus lint and tests. It exits 1 on errors. Plain tsc with emit on exits 2 but still writes JavaScript, which I saw in my test, so a build that does not check the exit code can ship broken code. I add --noEmitOnError or a separate check step.
What do lint rules add beyond tsc? (Senior signal)
Things that are legal but risky. With typescript-eslint and type information, my test file passed tsc (exit 0) and got 8 lint errors: a floating promise, explicit any, unsafe any member access and return, a ! non-null assertion and a @ts-ignore. no-floating-promises is the one I would not drop. Honest cost: type-aware rules are slower, and typescript-eslint 8.71.1 declares a peer range of TypeScript <6.1.0, so I could not install it beside TypeScript 7.0.2.
When is as acceptable? (Trap)
When I know something the compiler cannot and I have a reason: after a check it cannot follow, or inside a small, tested helper (like the getter builder in Lesson 4). Not on outside data: JSON.parse(...) as T is a lie that Lesson 0 showed crashing at runtime. When two types do not overlap, tsc refuses with TS2352 and suggests going through unknown, which is a sign to stop and validate instead.
Key takeaways
- Read errors from the bottom: the last indented line is the cause.
tscshows the first difference, so fix one and run again. - A dozen codes cover most days:
TS2322,TS2339,TS2345,TS7006,TS18048/TS2532,TS2564,TS2307,TS7053,TS2304,TS2741,TS2554. unknownforces a check,anyspreads silently. Prefer@ts-expect-errorto@ts-ignore, with a reason.- Migrate in steps:
allowJs,checkJsor// @ts-check, rename to.ts, thennoImplicitAnyfirst,strictNullChecks, the rest, andnoUncheckedIndexedAccesslast. Keep the build green and commit at each step. - Run
tsc --noEmitin CI; plaintsccan write JavaScript even when it fails. - Add a type-aware linter for what
tscallows: floating promises,any,!,@ts-ignore.
What's next
That is the end of the series. You can run tsc and read its messages (Lesson 0), type values and functions (Lessons 1 to 3), build types (Lesson 4), set up real projects (Lesson 5) and validate outside data (Lesson 6). To practise, take a small JavaScript project of your own and migrate it with the steps above, then answer the questions in Full-Stack Q&A, Lesson 3 and Frontend Depth, Lesson 4 out loud.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Request and Response Design: JSON Shapes, Errors and Validation · API Design
- Validation at the Edge: Schemas, DTOs and Trusting Nothing · Backend in Depth
- Error Handling: Operational vs Programmer Errors · Backend in Depth
Part of the study guide
Related series