Common TypeScript Beginner Mistakes
New TypeScript developers make the same mistakes: skipping parameter types, overusing any, and forgetting that types disappear at runtime. Learn to spot and fix the most frequent errors.
Common TypeScript beginner mistakes tend to repeat across nearly every new codebase, even though TypeScript itself is designed to catch mistakes early. The good news is that these patterns are well known, easy to spot, and simple to fix once you understand what the compiler is actually telling you.
Here are the seven most common mistakes new TypeScript developers make, why each one is a problem, and exactly how to fix it.
1. Forgetting to Annotate Function Parameters
This is the number one mistake. Function parameters are the one place TypeScript cannot infer types, because there is no value to look at when the function is declared.
function add(a, b) {
return a + b;
}
add(5, 10); // 15
add("5", 10); // "510" (no error, but probably a bug)Without annotations, both parameters become any, so the function accepts anything and the string-concatenation bug above slips through with no warning at all. Annotating both parameters closes the gap and gives the compiler something to check calls against:
function add(a: number, b: number): number {
return a + b;
}
add("5", 10);Calling it with a string in the first position now fails to compile instead of quietly producing the wrong result, because the annotation gives the compiler something concrete to check the argument against:
Argument of type 'string' is not assignable to parameter of type 'number'.Always annotate function parameters. Enabling the noImplicitAny compiler flag turns every untyped parameter into a compile error automatically, so the mistake cannot ship even if someone forgets.
2. Using Any as a Shortcut
When TypeScript complains about a type, the quickest fix is to annotate it as any and move on. This works, but it disables all type checking for that value and everything it touches.
function processOrder(order: any) {
console.log(order.total.toFixed(2)); // crashes if total is missing
order.sendEmail(); // crashes if sendEmail does not exist
}The any type spreads: once a value passes into another function or expression, the result becomes any too, so a single loose parameter can quietly disable checking across an entire module. Defining the actual shape closes that hole:
type Order = {
total: number;
items: string[];
};
function processOrder(order: Order) {
console.log(order.total.toFixed(2)); // safe: total is guaranteed to be a number
}If you genuinely do not know the shape of the data, use unknown instead. The compiler forces you to narrow the type before you can use it, which keeps the code safe without giving up on checking entirely. For a full comparison of the two, see any vs unknown in TypeScript.
3. Not Enabling Strict Mode
TypeScript's most valuable safety checks are opt-in. Without strict mode enabled in the compiler configuration, the compiler lets through patterns that cause runtime errors:
{
"compilerOptions": {
"strict": true
}
}The table below shows what strict mode enables and what happens without it:
| Strict flag | What it catches | Without it |
|---|---|---|
| strictNullChecks | Using a value that might be null | null silently passes as any type |
| noImplicitAny | Missing parameter types | Parameters default to any |
| strictFunctionTypes | Unsound function type checks | Weaker checks on function parameters |
| noImplicitThis | this used without a type | this is implicitly any |
Without strictNullChecks, a missing value flows straight through to a method call and only fails once the code actually runs:
let name: string = null;
console.log(name.toUpperCase());With it enabled, the exact same line never compiles in the first place, so the crash above simply cannot reach production:
Type 'null' is not assignable to type 'string'.Start new projects with strict mode on from day one. For existing JavaScript projects migrating to TypeScript, enable strict flags one at a time. For more on the strict mode flags, see strict mode in TypeScript.
4. Using Wrapper Types Instead of Primitives
JavaScript has wrapper objects String, Number, and Boolean that are different from the primitive values string, number, and boolean. Using the capitalized versions as types refers to the wrapper objects, not the primitives.
function greet(name: String): String {
return `Hello, ${name}`;
}This compiles, but capital String refers to the wrapper object type, not the primitive, and it causes subtle problems: a plain string is assignable to the wrapper type, but not the other way around, so the two are not fully interchangeable once mixed.
function greet(name: string): string {
return `Hello, ${name}`;
}Always use the lowercase primitive names for type annotations. There is never a good reason to reach for the capitalized wrapper types in ordinary application code.
5. Assuming Types Exist at Runtime
TypeScript types are erased during compilation. They do not exist when your code runs. This is the most fundamental thing to understand about TypeScript, and it trips up beginners regularly.
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function handlePet(pet: Cat | Dog) {
if (pet instanceof Cat) {
pet.meow();
}
}The instanceof operator checks a runtime value, but Cat is a compile-time type with no trace left in the compiled output, so the check itself fails before the code can even run:
'Cat' only refers to a type, but is being used as a value here.The fix is to check for a property that actually exists at runtime instead of reaching for the type itself, since only real JavaScript values survive into the compiled output:
function handlePet(pet: Cat | Dog) {
if ("meow" in pet) {
pet.meow(); // narrowed to Cat
} else {
pet.bark(); // narrowed to Dog
}
}The in operator checks for the existence of a property at runtime, and TypeScript uses that check to narrow the type.
The same rule applies to type assertions written with the as keyword: they compile cleanly but add no runtime safety, so casting a parsed API response to a specific shape can still crash if the actual response changes. For data that comes from outside your program, use runtime validation. For more on this pattern, see runtime validation in TypeScript.
6. Over-Annotating Variables
TypeScript can infer types from initial values. Adding annotations on top of inference is redundant and creates extra maintenance work.
let title: string = "Learn TypeScript";
let count: number = 0;
let items: string[] = ["a", "b", "c"];Every annotation here repeats what the initial value already says, and if the value's type ever changes, the annotation needs a manual update too. Removing the redundant annotations keeps the type accurate automatically:
let title = "Learn TypeScript"; // inferred: string
let count = 0; // inferred: number
let items = ["a", "b", "c"]; // inferred: string[]Add annotations where inference does not work: function parameters, variables declared without an initial value, empty arrays, and places where you want a narrower type than inference provides.
7. Confusing Void and Undefined Return Types
A function that returns nothing should use void, not undefined. The two types have different meanings.
function log(message: string): undefined {
console.log(message);
}Annotating undefined as a return type means the function must explicitly return that value, so a function that just logs and falls off the end fails to compile:
A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.void means the function returns nothing the caller should use, and it does not require an explicit return statement at all:
function log(message: string): void {
console.log(message); // ok: no return statement needed
}It is the standard type for event handlers, callbacks, setters, and any function whose purpose is a side effect rather than producing a value. For the full distinction, see void and never in TypeScript.
Rune AI
Key Insights
- Always annotate function parameters. TypeScript cannot infer them, and untyped parameters become
any. - Enable strict mode from the start. It catches the most dangerous patterns before they become habits.
- Use
unknowninstead ofanyfor values from external sources like API responses and user input. - Use lowercase
string,number,booleanfor type annotations, never the capitalized wrapper objects. - Types are erased at compile time. Use runtime validation for data that comes from outside your program.
- Let TypeScript infer types on initialized variables. Adding annotations where inference works is just noise.
Frequently Asked Questions
Why are my function parameters `any` when I did not annotate them?
Should I always add return type annotations to functions?
Is it bad to use `any` during development?
Conclusion
Every TypeScript beginner hits the same walls: untyped parameters, the temptation of any, confusion about compile-time versus runtime, and the wrapper type trap. The fixes are straightforward. Annotate every function parameter. Use unknown instead of any. Enable strict mode from day one. Use lowercase primitive types. And remember: types are a compile-time safety net, not a runtime guarantee. Fixing these patterns early builds the right habits and prevents countless bugs down the line.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.