Now that you have a firm grasp of TypeScript's basic language concepts, you probably want to know how to migrate existing code in JavaScript to TypeScript, and what to look for while doing that. This is incredibly valuable if you already possess good experience with JavaScript, but you want to migrate some projects to TypeScript and you don't know how. Therefore, it's important to understand where existing JavaScript programs stand when translating them into TypeScript.
Let's move on to the next section to learn how JavaScript compares to TypeScript.
How does JavaScript compare to TypeScript?
If you are from a JavaScript background, you will find that learning TypeScript is not very far away from what you were doing. TypeScript adds types to JavaScript and, in reality, it wraps all JavaScript programs so that they are valid TypeScript programs by default. However, adding additional compiler checks may cause those programs not to compile as they did previously.
Therefore, you need to recognize the following concepts. Some JavaScript projects compile successfully. However, the same JavaScript projects may not type check. Those that type check represent a subset of all JavaScript programs. If you add more compiler checks, then this subset becomes smaller as the compiler will reject programs that do not pass this phase.
As a straightforward example, the following JavaScript program is also a valid TypeScript program by default, although no types are declared in the parameter name or the return type:
const isArray = (arr) => {
return Array.isArray(a);
};
This program type checks correctly, so long as the noImplicitAny
compiler flag is false.
Note
Although it is valid, it is not recommended in the long run as the compiler will infer the parameters as any
type, which means that it will not type check them. When working on large-scale TypeScript projects, you should avoid those cases when you have implicit any
types. If you don't, you lose many of the benefits of type safety.
Transitioning from JavaScript to TypeScript
A reasonable question you may have to answer when attempting to translate existing JavaScript code into TypeScript is this: How can you do this efficiently and how can you write correct types?
There are several techniques that you can use to perform that body of work, but in most cases, we can summarize it in a few words: divide and conquer:
- To begin with, you can start by dividing large pieces of JavaScript into smaller packages and files. This is to ensure you don't spend time only in one package.
- Then, start by renaming
.js
files as .ts
files. Depending on the tsconfig
flags, you will have some compilation errors, which is expected. Most of the compiler errors are for missing parameter types. For example, the following is a function that checks if the parameter is an object. You can easily use it in TypeScript, so long as the noImplicitAny
compiler flag is unset:export const isObject = (o) => {
return o === Object(o) && !Array.isArray(o) &&
typeof o !== "function";
};
- You may also want to enable the
allowJs
flag, which allows you to import regular JavaScript files in TypeScript programs, with no complaints from the compiler. For example, if you maintain a file named utilities.js
, you can import it into TypeScript like so:import { isObject } from "./utilities";
If you have imported from external libraries such as lodash
or Rxjs
, you may be prompted to download types for them. Usually, TypeScript will reference where those types are located. For example, for lodash
, you should install it this way:
npm install --save @types/lodash
In any other cases, you will have to follow the compiler leads and suggestions. Hopefully, if you have structured your programs so that they're in small and manageable pieces, then this process won't take much of your time.
Next, we will see whether design patterns can be used in JavaScript or whether it makes more sense to leave them as a typed language such as TypeScript.
Design patterns in JavaScript
When studying TypeScript design patterns and best practices, you may find yourself writing equivalent code in JavaScript for those examples. Although you can technically implement those patterns in JavaScript, the lack of types and abstractions makes learning those concepts less appealing.
For example, while using interfaces as parameters, we can change the implementation logic at runtime, without changing the function signature. This is how the strategy design pattern works, as will be explained in Chapter 5, Behavioral Design Patterns.
With JavaScript, we cannot use interfaces, so you may have to rely more on Duck Typing, property checks, or runtime assertions to verify that a particular method exists in an object.
Duck Typing is a concept where we are only interested in the shape of an object (property names or runtime type information) when we try to use it for a particular operation. This is because, in a dynamic environment such as JavaScript, there are only runtime checks to ensure the validity of operations. For example, let's say we have a function that accepts a logger object, which logs events into a stream, and an emailClient
object by name and checks if certain methods are available before calling them:
function triggerNotification(emailClient, logger) {
if (logger && typeof logger.log === 'function') {
logger.log('Sending email');
}
if (emailClient && typeof emailClient.send ===
'function') {
emailClient.send("Message Sent")
}
}
So long as the log
and send
properties exist in those objects and they are functions, then this operation will succeed. There are many ways that this can go wrong, though. Look at the following call to this function:
triggerNotification({ log: () => console.log("Logger call") }, { send: (msg) => console.log(msg) });
When you call the function this way, nothing happens. This is because the order of the parameters has changed (swapped) and log
or send
are not available as properties. When you provide the right shape of objects, then the call succeeds:
triggerNotification({ send: (msg) => console.log(msg) }, { log: () => console.log("Logger call") });
This is the correct output of this program:
> Logger call
> Message Sent
With the correct arguments passed into the triggerNotification
function, you will see the aforementioned output of the console.log
command.
Duck Typing has a similar counterpart to TypeScript, and it's called structural typing.
This is what is enforced during static analysis, and it means that when we have two types (A and B), then we can assign B to A if B is a subset of A. For example, look at the following logger
object assignment:
interface Logger {
log: (msg: string) => void;
}
let logger: Logger;
let cat = { log: (msg: string) => console.log(msg) };
logger = cat;
Here, A is logger of the Logger
type and B is of the {log: (string) => void}
type. Because type B is equivalent to A, this assignment is valid. Structural typing is a very important concept when learning TypeScript. Wewill see more examples throughout this book.
TypeScript and JavaScript have a close relationship, and Typescript will continue to be a superset of JavaScript for the time being. Now, let's learn how to use the code examples in this book.