When a JavaScript project grows, some errors only become apparent during page execution or when receiving an unexpected response from an API. TypeScript adds static type checking before runtime, which helps detect many cases early and provides documentation within the code. However, it does not guarantee the correctness of logic and does not verify data coming from the network at runtime on its own.
What you will take away from the article
- Using types to describe contracts.
- Dealing with null and API data in an explicit manner.
- Runtime verification of external data.
What does TypeScript add?
TypeScript adds a type system that is checked before runtime and then converts the code into JavaScript. The editor can detect passing a string instead of a number or accessing an unknown property. A type contract clarifies the shape of the data and the allowed operations. Start with basic types, objects, and functions, and unions of types, and then learn generics when you need them; do not make complexity a goal in itself.
Write types that express working conditions
A request status can be represented as a union like pending | paid | cancelled instead of a random string, so the cases to be dealt with when modifying appear. Avoid any because it disables checking, and use unknown when receiving an unknown value and then verify it. Remember that as SomeType does not convert data and does not prove its correctness; it is telling the compiler to assume a certain type.
Verify data at runtime
If the server sends JSON, the response does not become reliable just because an interface is added; types disappear from the resulting JavaScript. Verify the required fields, types, and allowed values at runtime using a schema validator or explicit checking. Separate the shape coming from the network from the domain model if their rules differ, and then convert it to a relatively trusted internal object after checking.
Enable strict settings and integrate tests
Options like strict and strictNullChecks make dealing with potentially absent values more explicit. In an old project, enable them gradually and handle errors instead of silencing them with any. Run type checking and tests in CI, and use the compiler during refactoring to find places that need updating. The success of type checking does not prove the correctness of program behavior; combine types with tests that measure results.
A union of types clarifies request conditions
type OrderStatus =
| "pending"
| "paid"
| "cancelled";
type Order = {
id: string;
status: OrderStatus;
total: number;
};
function canShip(order: Order): boolean {
return order.status === "paid";
}
The type clarifies allowed conditions
When you write OrderStatus as a limited union of values, the editor can detect a spelling error in the status or a missing branch in the logic before runtime. This is especially useful when actions differ according to the request status; a paid request can be shipped, but a cancelled request should not go through the same path.
However, TypeScript does not automatically verify data coming from an API, model, or JSON file at runtime. The type tells the compiler what the code assumes; external data must be actually verified and then converted to a trusted shape. This difference between static verification and runtime verification of data is one of the most important rules for using TypeScript safely.
Detailed case study: from appearance to root cause
The practical case here is: an API response arrives with an unexpected field despite TypeScript describing it as a guaranteed number. It is not enough to fix the apparent symptom; one must draw the data path or request from its beginning to the result and find out where the difference originated. Start by logging what happened, what was expected to happen, and the step that can be proven by measurement, log, or test. After that, change one clear factor and then redo the same scenario to verify that the root cause was addressed. If several components changed at once, it will be difficult to know which one made the difference.
Link the outcome of the case to the principle of the article: what does TypeScript add? and write types that express working conditions. These two concepts define what should be checked first and what can be postponed. Also, record the cases that were not tested, so that the success of one scenario does not turn into a claim that the entire system is error-free.
Choosing types without overcomplicating
Use unknown when the data is untrusted, and then narrow the type through conceptual verification instead of quickly resorting to any or assertions. Generic types are useful when they describe a real relationship between input and output, but they become a burden if they become more complex than the function itself. Make types reflect the domain model, not easily changeable implementation details.
Distinct unions help represent mutually exclusive conditions like loading, success, and failure, instead of a set of Boolean values that can produce illogical combinations. When changing an interface, use compiler errors to identify places that need to be updated, and then test the actual behavior; the success of building TypeScript does not prove the correctness of business rules.
From example to implementation in a real project
Start from the code example presented in the article: "A union of types clarifies request conditions". Do not transfer it literally to production before you identify its inputs, outputs, and assumptions. Ask what happens when a value is missing, or unexpected data arrives, or the request is repeated, or an external dependency is delayed. After that, implement a small version within an independent branch and add a test for the basic case and another for a failure case.
Use these steps as a starting point: start with the parts most prone to data errors. Then define types for common contracts. Then verify JSON and inputs at runtime. Make each step reviewable, and do not combine real project settings and secrets within a tutorial example. If your environment differs from the example, document the difference and modify the implementation based on official documentation rather than copying a solution without understanding.
Understanding type errors and processing external data
If a function becomes full of as and ! to silence the compiler, this is a sign that the data definition or the boundaries between conditions are not clear. Start from the source: is the response type accurate? Can the value be null? Is there an uncovered case? Fix the model instead of spreading type conversions that force the compiler to accept assumptions it does not know are correct.
When receiving JSON, verify the required fields, their type, and their ranges at runtime, either with a schema library or with suitable manual verification. After verification, pass the resulting type to the rest of the application. This makes the system boundaries clear and prevents an incorrect value from infiltrating logic that relies on unrealistic guarantees.
A systematic diagnosis approach when failing
When a solution does not work, do not start by adding another condition before identifying the failure indicator. Record the input, the expected outcome, the actual outcome, and the message or measurement that supports the conclusion. One of the repeated mistakes in this topic is: believing that an interface verifies JSON at runtime. Treat this point as a hypothesis to be tested, not as a preconceived diagnosis; the cause of the problem may be in a neighboring layer or in an incorrect assumption in testing.
Divide the system into clear boundaries: inputs, processing, external dependency, and outcome. Test each boundary separately as much as possible, and then implement an integrated scenario. If you fix the problem, add a test that prevents it from returning or an operational note that helps to detect it early. The goal is for diagnosis to become repeatable by another team member, not to remain tied to the memory of the person who discovered the fault.
Plan to introduce TypeScript into an existing project
If the project is written in JavaScript, it is not necessary to convert all files at once. Start with independent units or the most error-prone parts, and add settings gradually to clarify the files included in the verification. Do not hide many errors using any just to finish the transition quickly, because that reduces the expected value of the conversion.
Enable strict options gradually according to the project size, and write types for API boundaries and basic models, and set clear rules for the team to deal with exceptions. Review tool versions and the build method in CI, so that developers and the repository use the same verification rules.
Advanced considerations before adopting the solution
If a function becomes full of as and ! to silence the compiler, this is a sign that the data definition or the boundaries between conditions are not clear. Start from the source: is the response type accurate? Can the value be null? Is there an uncovered case? Fix the model instead of spreading type conversions that force the compiler to accept assumptions it does not know are correct.
When receiving JSON, verify the required fields, their type, and their ranges at runtime, either with a schema library or with suitable manual verification. After verification, pass the resulting type to the rest of the application. This makes the system boundaries clear and prevents an incorrect value from infiltrating logic that relies on unrealistic guarantees.
In a real project, the solution is not evaluated on the success of the happy case alone. Review the behavior under load, failure cases, future changes, and relevant security boundaries. It is important here not to consider the static type as a validation of JSON; inspect the data at system boundaries and minimize unjustified explicit conversions. Do not treat these points as a later addendum; the design decision may be difficult to change when data accumulates or multiple clients depend on it.
Define metrics or acceptance tests before merging, and clearly document the solution's boundaries. The solution may be suitable for a small site but may need a queue or cache or a more rigorous structure as usage grows. Do not add complexity prematurely without evidence, but do not ignore a clear indication that the requirements exceed the simple version. A good decision balances current simplicity with the expected cost of change and maintains system clarity for the team.
How do you know that types have improved the project?
Monitor the amount of repeated manual verification that disappeared, the number of impossible cases that the compiler now reveals, and the ease of modifying the model across files. The goal is not to produce the largest number of types, but to reduce hidden assumptions and improve readability and maintainability.
Review a complex type with another developer: if they cannot understand it without following several layers of generics, you may need to split it into simpler definitions. Test basic behavior because the static type does not cover timing errors, network errors, or unrepresented business rules.
Practical application and acceptance criteria
Try this exercise on a test project or a separate branch: verify invalid JSON at runtime, then narrow the type before using the value. Write the expected outcome before implementation, record what actually happened, and fix the difference if it appears. Also, try a boundary case or an intentional safe failure. Do not use real data or a production system to cause a test failure.
Consider the task complete when the re-testing steps are documented, the test proves basic behavior and failure case, and there are no secrets or sensitive information in the log, and another developer can understand the reason for the solution. If this is not achieved, what is required is not to add more code directly; rephrase the problem and clarify the assumption that was not correct. This approach makes the article a starting point for technical practice, not just theoretical reading.
Frequently asked questions about TypeScript
Does TypeScript prevent all JavaScript errors? No; it helps to detect a class of static errors but does not replace tests and runtime verification.
Is any always forbidden? Not necessarily, but it should be a limited and justified exception.
Do I need a validation library? When data comes from external boundaries, you need runtime verification, and a schema library can facilitate that.
Practical implementation plan
- Start with the parts most prone to data errors.
- Define types for shared contracts.
- Verify JSON and inputs at runtime.
- Enable type checking in CI.
- Handle warnings without hiding them randomly.
Common mistakes to watch out for
- Believing that an interface verifies JSON at runtime.
- Using
asto bypass every error. - Writing complex types that do not add clarity.
Conclusion
TypeScript makes assumptions visible and helps to discover compatibility errors early. Use it to describe contracts and cases, verify external data at runtime, and keep tests responsible for actual behavior.