Dealing with untyped code
Chapter 4: TypeScript Architecture
Chapters - Table of contents
This page will introduce the many ways to deal with assets files, modules external to your applications and how to interact and migrate legacy JavaScript code.

Dealing with npm modules

When installing a new npm package (or migrating your codebase to TypeScript), you will face 3 scenarios:
  • The package is shipped with its own type definitions, no action is required on your side!
    (ex: date-fns
  • The package has a corresponding @type/ package that need to be installed
    (ex: lodash)
  • The package is not providing any type definitions.
Let's see how to deal with those 3 scenarios.

date-fns: a package published with typings

date-fns is a modern alternative to moment.js to deal with time and dates.
When installing date-fns:
1npm i -S date-fns
2
npm will install the package which is published with its TypeScript typings, in one or many .d.ts files.
What is a .d.ts file?
A .d.ts file is a TypeScript file containing only types, it cannot contain any other expression than type expressions.
The types defined in a d.ts are globally available in the whole TypeScript project, there is no need to import them.
date-fns npm package is shipped with a typings.d.ts file containing all the types of the functions exposed by the library.
Let's take a look at the typing of the previously seen compareDesc(a, b) function, as written in date-fns's typings.d.ts file:
1function compareDesc(
2  dateLeft: Date | number,
3  dateRight: Date | number
4): number
5
So, how does TypeScript resolves the types of date-fns?
When doing the following:
import { compareDesc } from "date-fns"
TypeScript first looks in the package.json of date-fns for a typings property that indicates the path of the .d.ts file of the package.
If the property is not present, TypeScript will try to find the corresponding @types/date-fns package.

lodash: a package with a separated typings package

Installing lodash on a TypeScript project (npm i -S lodash) will give the following TypeScript warning:
TypeScript could not find any typing property in lodash's package.json and no @types/lodash package installed.
TypeScript could not find any typing property in lodash's package.json and no @types/lodash package installed.
Without any installation of the @types/lodash package, TypeScript will resolve all lodash's functions to any.
In order to install the types of lodash, run the following command:
1npm i -D @types/lodash
2
ℹ️Please not the -D which means "install as development dependency".
Installing a @type/* as a main dependency (npm i -S ...) makes no sense since types are not used at runtime.

A package without published typings

In some rare case, you will have to work with some packages that don't provide any typings, neither in the package itself or in a separate @types/* package.
For this scenario, you will have to write the types of the need package's functions in a d.ts file in your project.
Let's say that you need a package called identity that exposes the following function:
1export const identity = (a) = a
2
In order to add typing of identity's function, you will need to add the following identity.d.ts file to your project (the name of the .d.ts file does not matter):
1declare module "identity" {
2  export function identity<T>(a: T): T;
3}
4
5
The identity.d.ts file describes a module called identity that exports a function named identity, which allow TypeScript to resolve the following:
1import { identity } from 'identity'
2
3const result = identity(1)
4// `result` is of type `number` 🎉
5

Dealing with legacy JavaScript code

When dealing with migrating JavaScript code to TypeScript, or have TypeScript work with JavaScript code, 3 main strategies will be possible:

1. Ask TypeScript to work with the JavaScript code

The TypeScript provide an option to consume JavaScript code, which needs to be enabled in the tsconfig.json file:
  • allowJs: will allow you to import .js/.jsx files from .ts/.tsx files with TypeScript doing its best to infer the types.
  • checkJs: ask TypeScript to check the typings in JavaScript code, raising errors if some are found.
What is a tsconfig.json file?
Every TypeScript project must have a tsconfig.json file at its root to indicate to TypeScript which compiler options should be applied.
Running tsc --init (tsc is the TypeScript CLI, which can be installed as follows: npm i -g typescript) will create a tsconfig.json file for you.
More on the tsconfig.json options in the next page.
This strategy can be applied to projects having a set of simple helpers written in JavaScript.
However, please know that such approach has its limitations as show below:
1// one.js
2export const one = () = 1
3
4// identity.js
5export const identity = (a) = a
6
7// index.ts
8import { one } from './one'
9// `one()` typing is `() => number`
10import { identity } from './identity'
11// `identity(a)` typing is `(a: any) => any`
12
13const result = identity(1) // `result` is of type `any`
14
As showcased in the introduction of this Chapter, TypeScript inference powers have their limitations.
For this reason, TypeScript compiler's options allowJs and checkJs should only be used in association of the other strategies below, otherwise, you'll end up having weak TypeScript types.

2. Write the TypeScript types definition

When enabling TypeScript's compiler allowJs option doesn't help (for example: with complex code which always resolve to any), another approach is to provide your own .d.ts files.
To do so, the TypeScript convention is to provide a .d.ts file having the same location and base filename as the corresponding .js file:
1// Example of project structure:
2
3- node_modules/
4- package.json
5- tsconfig.json
6- src/
7  - identity.js
8  - identity.d.ts
9
Given our identity.js definition:
1export const identity = (a) = a
2
We would write the following identity.d.ts file:
1export const identity = <T>(a: T) => T;
2
Which would help TypeScript provide complete type inference:
Thanks to the .d.ts file, TypeScript has a complete typing of identity(a)
Thanks to the .d.ts file, TypeScript has a complete typing of identity(a)
Of course, doing so for many files and, especially, for long files would take much time.
Another approach would be to get the help of the TypeScript CLI (tsc) to bootstrap those .d.ts files from the JavaScript files.
Then, you'll just have to fix the types that resolved to any!

Leverage TypeScript CLI to bootstrap some .d.ts files

Back to our identity.js file, we can run the following command at the same location:
1tsc *.js --declaration --allowJs --emitDeclarationOnly --outDir .
2
which will produce the following identity.d.ts file:
1export function identity(a: any): any;
2
Which we can easily fix to the following definition:
1export function identity<T>(a: T): T;
2

3. Gradually migrate the JavaScript files to TypeScript

The two previous approaches combined are great, however, they only provide temporary solutions to the problem of dealing with legacy JavaScript code.
You don't want to end up to maintain .js files that provide weak typing or maintaining both a set of .js and .d.ts files that you will have to keep "in-sync".
The following strategy is a "rule of thumb" to apply whenever you face JavaScript code in a TypeScript project:
  1. If the JavaScript code is an external module (cf: npm package)
    1. Try to install its corresponding @types/* package
    2. If no such package exists, write your own .d.ts definition file
      (see next section "Augment globals")
  2. If the JavaScript code is in your project and will only be used by files migrated to TypeScript
    1. If the JavaScript code is a set of multiple and complex files that cannot be rewritten in a day
      -> generate the .d.ts files with the TypeScript CLI (tsc) while planning this refactoring with your team
    2. If the JavaScript code can be rewritten in a day
      -> rewrite it in TypeScript
  3. If the JavaScript code is in your project and will be used by both JavaScript and TypeScript file
    1. If the JavaScript code is small
      -> duplicate it and migrate it to TypeScript (marking the .js file as deprecated)
    2. Otherwise -> generate the .d.ts files with the TypeScript CLI (tsc) while planning this refactoring with your team
As you can see, migrating a project to TypeScript requires to:
  1. identify all the logic parts of your projects (components, helpers, libraires, etc..) and assign them a priority (example: an upcoming revamp of a feature might be a good timing to rewrite all its corresponding components and helpers).
  2. For each parts, draw the dependency to prioritize the order of migration and identify low hanging fruits.

Misc: Typing assets and augment globals

TypeScript and asset imports (JSON, SVG)

Working with JSON files

With TypeScript's compiler default configuration, importing a JSON file will result in the following:
Updating the tsconfig.json to put resolveJsonModule to true will solve the issue and provide typing of JSON files!
__

Working with SVG files

The same issue goes when trying to import SVG files:
TypeScript does not know how to find the type declarations of *.svg modules.
TypeScript does not know how to find the type declarations of *.svg modules.
__
In order to solve this issue, we will need to tell to TypeScript how to resolve the type of imports made from *.svg modules.
To achieve this, we create a svg.d.ts at the root of our src/ folder containing the following declaration:
1declare module "*.svg" {
2  const content: any;
3  export default content;
4}
5
The above TypeScript declaration file indicates that, in the current project, any import from a "*.svg" module would resolve with a default export of type any.
In the specific case of SVG files import, resolving to any is fine since, most of the time, we just instantiate the given SVG component, without props:
1import GithubLogo from "./Footer/Github.svg";
2
3function Footer() {
4
5  return (
6     <>
7       // ...
8       <GithubLogo />
9       // ...
10     </>
11  )
12}
13
__

Augmenting globals (ex: window)

As exposed in the Chapter 2, interfaces are closed by default, meaning that the following snippet raise some TypeScript errors:
However, in most projects, we often need to add custom properties on window (or process with webpack).
To solve this issue, we are gonna need to augment existing interfaces, in a similar way we allowed the import of SVG files.
In order to add our custom IS_PRODUCTION flag to the Window type, we will create a new augments.d.ts file at our src/ root as follows:
1interface Window {
2  IS_PRODUCTION: boolean
3}
4
which would solve our issue:
How's that possible?
Interfaces are closed by default but open to modification, which means that they can be reopened and updated as follows:
1interface ButtonProps {
2   name: string
3}
4
5interface ButtonProps {
6   disabled: boolean
7}
8
9// `ButtonProps` is:
10// {
11//   name: string
12//   disabled: boolean
13// }
14
When the same interface is met by TypeScript, all its definitions get concatenated.
This is particularly handy when it comes to augmenting native types or types from external packages.
We use cookies to collect statistics through Google Analytics.
Do not track
 
Allow cookies