Common Typescript Mistakes You Don’t Know About
In this post, I'll highlight common TypeScript mistakes found in projects. Many developers switch from JavaScript to TypeScript without learning TypeScript's fundamentals. They think the code is similar, but end up not leveraging TypeScript's benefits. Instead, they resort to hacks to solve problems.
Disabling Strictness
One common mistake is disabling strict rules in tsconfig.json
.
{
...
"strict": true
...
}
The strict
option enables various checks and combines multiple strict rules.
Setting
strict
to false makes TypeScript less safe and is not recommended.
Less strict TypeScript leads to fewer errors during compilation and more errors at runtime, which is not a safe approach. It's recommended to never disable TypeScript rules in tsconfig.json
, as it won't make your code safer.
Operator any
Another common mistake arises from a lack of familiarity with TypeScript, leading developers to resort to using the any
type as a quick fix.
interface User {
id: string
name: string
}
const user: User = {}
In this example, we attempted to define a user object, but we didn't specify the correct fields.
When faced with errors they don't understand, people often resort to using the any
type to resolve their issues without fully understanding the underlying problem.
const user: any = {}
Using any
type essentially bypasses all TypeScript checks, converting TypeScript code into JavaScript code without any type safety. While this might seem like a quick fix, it introduces the risk of runtime errors that could have been caught during compilation.
If the
any
type is used more than 20 times in a project, it indicates a significant reliance on unsafe typing practices. This can lead to decreased code quality, maintainability issues, and an increased likelihood of runtime errors.
The any
type should be used sparingly and only in exceptional cases where the type of a value cannot be known or inferred at compile time. Overuse of the any
type can undermine the benefits of TypeScript's static type checking and lead to less predictable and more error-prone code.
Unknown operator
The unknown
type is a safer alternative to any
in TypeScript. While any
effectively disables type checking, unknown
provides a way to represent values of an unknown type while still enforcing type safety when accessing or manipulating those values. It encourages developers to handle type checks explicitly, leading to more robust and maintainable code.
const getFoo = (something: any) => {
console.log(something.at(1))
}
This code won't show any errors because it's using the any
type. So, even if something
isn't a string, the at
function can still be called on it without a problem. This isn't good and can cause unexpected issues when the code runs.
const getFoo = (something: unknown) => {
console.log(something.at(1))
}
With unknown
, this code behaves differently. It will throw an error because you can't call the at
function on an unknown
variable. This is beneficial because it catches potential issues before they happen.
It means that we can't directly use any function call on unknown
before ensuring that it's safe to do so.
The main difference is that
unknown
alerts you to potential issues, whereasany
does not.
What you need to do is narrow your type before applying any operations or parameters.
Narrowing Type
The next point is that many people are unfamiliar with type narrowing and don't know how to do it properly. Type narrowing, such as using unknown
, helps refine our types to make them more specific.
const getFoo = (something: unknown) => {
if (typeof someting === 'string') {
console.log(something.at(1))
}
}
In this case, we narrowed our type by checking if we received a string. This code won't produce any errors because TypeScript is confident that the data type is correct. So, using unknown
allows us to indicate to TypeScript that we're uncertain about the actual data type, while type narrowing enables us to refine that unknown
data without explicitly converting it to a known type.
You can employ type narrowing in various scenarios, such as when narrowing a union type. Remember this useful technique as it can be quite handy in refining your types as needed.
Type Assertion
Another common issue is the overuse of the type assertion operator as
.
const getFoo = (something: unknown) => {
const str = something as string
console.log(something.at(1))
}
Instead of narrowing types properly, we used as
to forcefully treat our data as a string. But this is like using any
– it's not safe. We're telling TypeScript to treat the data as a string, even if it's not, which can lead to runtime errors.
This code isn't safe, even if TypeScript doesn't show any errors during compilation.
Similarly to any
, you should use the as
operator as sparingly as possible.
Don't Skip Types
Another important point is that people don't specify types frequently enough. While TypeScript can infer the correct data types in some cases, it's not always accurate. By explicitly defining types, we ensure that our code behaves as expected and remains resilient to changes in the future.
Even in the code we just wrote, TypeScript inferred that our function returns void
. However, in good coding practice, we should explicitly specify the return type.
const getFoo = (something: unknown): void => {
...
}
In such cases, TypeScript will ensure that the returned type is accurate. If we omit the type, TypeScript won't detect an error if we inadvertently return a different data type.
That's why it's highly advisable to explicitly specify the type for each variable to ensure the correctness of data types throughout the code.
Understading Errors
Understanding TypeScript errors is crucial for writing robust code. Instead of resorting to quick fixes like type assertions or using the any
operator, developers should take the time to learn TypeScript thoroughly and understand the errors it produces. This will lead to better code quality and fewer runtime errors.
Understanding the error messages is crucial for fixing issues effectively. In this case, the error is indicating that the function is expected to return a value of type number
, but it's returning void
instead. This suggests that the function is not returning anything when it should be returning a number. To fix this, you need to ensure that the function returns a value of type number
.
Take your time, especially if you're a beginner, to correctly address TypeScript errors.
Happy Path
Another crucial point that many people overlook is that TypeScript is a static analyzer of your code. It doesn't have a complete understanding of your entire project or the specific values it contains.
const getData = (data: string): void => {}
const data: string | undefined = undefined
getData(data)
It might be that in your application, you are certain that a value is never undefined. However, TypeScript doesn't consider the specific values in your application; it only cares about the validity of data types. If a function parameter can potentially be undefined based on its type declaration, TypeScript will throw an error if you attempt to call the function without handling the possibility of undefined.
We're attempting to provide a data type that isn't valid for the argument.
The solution here is to either check outside for the data type or allow your function to accept undefined
and handle it internally.
Using Optional Too Much
Another common issue is that people use optional types excessively.
interface User {
id: string
name?: string
age?: number
isActive?: boolean
}
This code is technically correct, but it's not ideal. The problem lies in the excessive use of optional properties. Essentially, this interface resembles any
, as every field can be undefined
. Consequently, the type validation provided by this interface is insufficient.
Consider a scenario where you have users with only an id
and no other fields. When using this user object in a function, it may lack the necessary fields. It's advisable to mark most, if not all, properties as required and limit the use of optional properties.
Union vs Enum
Another common issue is the excessive use of type unions instead of enums.
type State = 'active' | 'completed' | 'rejected'
const state: State = 'active'
This code is valid and functional, but it often becomes problematic when you need to work with specific values instead of just data types.
if (state === 'active') {
console.log('we are active')
}
Since types only exist in TypeScript and not in JavaScript, we can't use a type
as a value. However, we can rewrite this code using enums instead.
enum State {
active = 'active'
completed = 'completed'
rejected = 'rejected'
}
const state: State = State.active
We wrote the same code but used enums instead. Already, we used State
not only as a data type but also as a value when we assigned the string to it. This is one of the benefits of enums. Assigning plain strings is not valid. We can only assign values from the Enum, which is incredibly safe and allows us to reuse the code.
if (state === State.active) {
console.log('we are active')
}
Secondly, we can use enums inside conditions, which is really handy because enums are transpiled to JavaScript and exist there as values.
Exclamation mark
One more common Typescript mistake that people make is using an exclamation mark too often.
const getData = (data: string): void => {}
const data: string | undefined = undefined
getData(data!)
I used the same code as before, but to fix the issue of providing undefined
, I used an exclamation mark.
The exclamation mark tells TypeScript that we are certain the value exists.
Realistically, you can never be absolutely sure that the value is present, making this code extremely risky. It's on par with type assertion and using any
.
const data: string | undefined = undefined
if (data) {
getData(data)
}
The correct solution here would be to check if the data
property exists before passing it to the function.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!