TypeScript is getting more and more popular. Therefore, it is a great time to start using it if you haven’t already. Chances are, our project has been developed for quite some time. If that’s the case, the best approach could be to introduce TypeScript to the codebase gradually. We might encounter a case where we would like to use React components written in JavaScript within our TypeScript code.
In this article, we look into how to create TypeScript declaration files that allow us to do that. We also learn how we can create interfaces from React prop types.
Declaration files
We can distinguish a type declaration file by the .d.ts filename extension. Such files’ job is to hold the declarations of our variables and functions without their implementations.
To understand the type declaration files better, let’s create a simple function in JavaScript.
sum.js
1 2 3 4 5 |
function sum(firstNumber, secondNumber) { return firstNumber + secondNumber; } export default sum; |
We can see that we have an exported function above. Unfortunately, it does not include any types. To deal with that, let’s create a declaration file. The simplest way to do that would be to create a .d.ts in the same directory.
sum.d.ts
1 2 3 |
function sum(firstNumber: number, secondNumber: number): number; export default sum; |
Since we’ve used the default export in our sum.js file, we also need to use the default export in sum.d.ts.
Above, we declare the function along with its arguments and the return type. The implementation is still in the JavaScript file, so we don’t need to repeat it in the .d.ts file.
Thanks to doing the above, when we import the sum function in our TypeScript code, it is fully type-safe.
index.ts
1 2 3 |
import sum from './utilities/sum'; console.log(sum('1', 2)); |
Argument of type ‘”1″‘ is not assignable to parameter of type ‘number’.
We can do a similar thing with variables instead of functions. Although, keep in mind that TypeScript capable of analyzing the JavaScript files to some extent.
constants.js
1 2 3 |
export const HEADER_HEIGHT = '100px'; export const FOOTER_HEIGHT = '60px'; |
index.ts
1 2 3 4 |
import sum from './utilities/sum'; import { FOOTER_HEIGHT, HEADER_HEIGHT } from './utilities/constants'; console.log(sum(HEADER_HEIGHT, FOOTER_HEIGHT)); |
Argument of type ‘”100px”‘ is not assignable to parameter of type ‘number’.
Even though we didn’t create a declaration file for the constants.js file, TypeScript properly assigned the HEADER_HEIGHT and FOOTER_HEIGHT constants with the literal types.
Declaring modules
TypeScript is definitely popular. Therefore, all commonly used libraries should have TypeScript declarations. Even if that’s the case, you might need to use a library that does not have the declarations for some reason. For example, it might be a private library developed by your organization.
A common use case is importing image files. Webpack usually takes care of it under the hood, for example, with the file-loader. Still, TypeScript does not recognize image files as modules.
App.tsx
1 2 3 4 5 6 7 8 |
import React from 'react'; import logoUrl from './logo.png'; export const App = () => ( <div> <img src={logoUrl} alt="logo" /> </div> ) |
Cannot find module ‘./logo.png’.
We can easily deal with that by creating a file with a module declaration.
png.d.ts
1 2 3 4 |
declare module '*.png' { const url: string; export default url; } |
Above, we declare that all files with the .png extension export a URL. We could also specify a third-party library in such a way, for example.
Creating declaration files for React components
If we are introducing TypeScript to an organization, chances are there are quite a few React components already in place. To provide an example, let’s create a simple input component in JavaScript.
Input/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import React from 'react'; import PropTypes from 'prop-types'; const Input = ({ label, onChange, name, type = 'text', }) => { return ( <div> { label && <label>{label}</label> } <input name={name} type={type} onChange={onChange} /> </div> ) } Input.propTypes = { label: PropTypes.string, onChange: PropTypes.func, name: PropTypes.string.isRequired, type: PropTypes.string, } export default Input; |
If we try to import the Input component in TypeScript as it is, we do get some type safety, but not much.
1 2 3 4 5 6 7 8 9 10 11 12 |
import React, { ComponentProps } from 'react'; import Input from './components/shared/Input'; type InputProps = ComponentProps<typeof Input>; /* { label: any; onChange: any; name: any; type?: string | undefined; } */ |
ComponentProps is a utility shipped with React that can extract the types of the props of a component
Above, we can see that TypeScript can understand our Input component to some extent. Even though some props such as the label are not required, this is not shown in the above types. The type prop is described somewhat correctly because we’ve provided a default value. Although, we can improve it further.
Input/index.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { ChangeEvent, FunctionComponent } from 'react'; type InputType = 'text' | 'checkbox' | 'email' | 'date' | 'password'; interface Props { name: string; onChange: (event: ChangeEvent<HTMLInputElement>) => void; type?: InputType; label?: string; } const Input: FunctionComponent<Props>; export default Input; |
If you are writing class components, use the ComponentClass type imported from React instead of FunctionComponent.
You can see that we are free to import additional types in our declaration file above. Creating such declarations results in type-safe React components, even if they weren’t created with TypeScript.
Creating interfaces from prop types
Above, we analyze a JavaScript React component and create a TypeScript interface from scratch. Doing that has the best chance of producing good results, but it takes quite a bit of time. Chances are, the React component we are trying to use already has prop types defined.
To create a TypeScript interface from prop types, we need the InferProps utility.
Input/index.d.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import { FunctionComponent } from 'react'; import PropTypes, { InferProps } from 'prop-types'; const propTypes = { label: PropTypes.string, onChange: PropTypes.func, name: PropTypes.string.isRequired, type: PropTypes.string, } type InputProps = InferProps<typeof propTypes> const Input: FunctionComponent<InputProps>; export default Input; |
Doing the above might get us some type-safety. Let’s inspect what props the above declaration produces:
1 2 3 4 5 6 |
{ label?: string | null; onChange: (...args: any[]) => any) | null | undefined; name: string; type?: string | null, } |
We can see that we can use prop types to generate valid TypeScript interfaces. Unfortunately, they miss some of the essential details, such as the arguments of the onChange function.
Summary
In this article, we’ve looked into how to use JavaScript React components within our TypeScript code. To do that, we’ve learned the basic principles of writing declaration files. We’ve also learned how to create declarations for React components both from scratch and using prop types. Finally, we’ve compared both approaches, and it looks like it might be a better idea to write TypeScript interfaces from scratch in a lot of the cases.