34

I'm working on a project with a Node.js, Express.js & TypeScript backend (REST API) and a React, Redux & TypeScript frontend.

In the backend I have created some types for example:

models/Product.ts

export type Product = {
    title: string
    description: string
    price: number
}

So the REST API is sending a response like the following:

{
  "data": [
     {"title": "Shoe", "Description": "This is a shoe.", "price": 20},
     {...}
   ] // inside here all the Products[]
}

On the client I want to cast the data to the type Product array. But since the frontend and backend are different code bases and separated, I still want to take the advantage of using types on the frontend. But I want to achieve this without duplicating code. So I don't want to update 2 places when modifying or adding models.

Does someone know what is the best way to share types between client and server?

I was thinking of something like creating an endpoint on the backend which the client can hit, then it writes all the models in a single file for example models.ts on the client. So I think I need to loop through every file inside /models/ on the backend then parsing it in a new file which is written on the client as models.ts. But is this really a good way? Does someone know a better way of achieving this structure?

Steven Soekha
  • 721
  • 1
  • 9
  • 20
  • This could be answered here https://stackoverflow.com/questions/66529067/sharing-typescript-type-declarations-via-npm-package/66533749#66533749 – Leonardo Viada Mar 11 '21 at 14:27

3 Answers3

41

You can use TypeScript path mapping.

Example from a project I'm the author of:
Backend defined types inside SampleTypes.ts are reused in the client project to avoid duplicating the code.

client/tsconfig.json:

{
  "compilerOptions": {
    "paths": { "@backend/*": ["../server/src/api/*"], },
  }
}

../server/src/api/ -> https://github.com/winwiz1/crisp-react/tree/master/server/src/api

client/....ts:

import { SampleRequest } from "@backend/types/SampleTypes";
plswork04
  • 589
  • 6
  • 11
winwiz1
  • 2,906
  • 11
  • 24
  • Nice, that worked like a charm for me. Much better than just copy paste it everytime there is a change. – Raqha Apr 23 '22 at 23:06
  • 6
    This works, but does mean the api and client src trees can't be used as isolated docker contexts since a context of `client` can't look at `../server`. (For now I just moved context up a level, but that's not ideal?) – plswork04 Apr 25 '22 at 23:28
  • @Raqha Glad it worked for you. – winwiz1 Apr 27 '22 at 09:03
  • 1
    @plswork04 Suggest to use Docker multi-staged build to cut image size and improve security by reducing the attack surface. Stages: (1) Copy client and server code, install client dependencies and build client. (2) Start afresh, copy server code only, install server dependencies and build server. (3) Create the final image by starting again afresh with fresh OS image, install server run-time dependencies only (no dev dependencies including TypeScript, no source code), copy from previous stages the build artifacts (client's - script bundles and .html files, server's - transpiled JS files). – winwiz1 Apr 27 '22 at 09:15
  • 3
    with create-react-app it's impossible to include outside the client directory. maybe the server could include stuff in the *client* but i've seen the tsc command then build both client and server projects... – dcsan May 02 '22 at 22:23
  • 1
    also remember your paths are relative to `'baseUrl': './src'`. but with this setting tsc creates `build/client` and `build/server/src/...js` ie including anything from client also compiles the whole client dir – dcsan May 02 '22 at 22:36
  • So I tried this and it seems to compile both the client and server, but my react-native client throws an Unresolved module error - it can't find the module I'm referencing. I'm using Expo to do my builds and testing – Ira Klein Jun 09 '22 at 12:06
  • @IraKlein If TypeScript compiler (TSC) happily transpiles .ts file into .js file but at run-time the JS code cannot find the referenced (I assume the imported) module, then the two most likely issues are: 1. The bundler doesn't support Path Mapping and cannot understand the import. 2. The bundler/builder has not been configured to support Path Mapping so it doesn't process the import correctly. I'm not familiar enough with Expo but it appears it can use either TSC or Metro to transpile TS into JS so the problem could potentially be with Metro. E.g. transpiling and/or bundling incorrectly. – winwiz1 Jun 12 '22 at 13:28
2

You are essentially looking to share code (in your case type definitions) between multiple packages. Yarn has developed workspaces for this purpose, and it does not rely on tsconfig/Typescript as the other answer does.

It's a dive into a rabbit hole, with a bit of work of your Yarn configuration and possibly the use of tools like Lerna. It does make sense however, when you have tightly coupled packages that share types, but perhaps also validation logic.

Roy Prins
  • 2,790
  • 2
  • 28
  • 47
  • On top of this, I would say this is basically the premise of a monorepo. `lerna` is also now under `nrwl` (or `nx`) who maintain a good set of tools for monorepos. It is indeed a rabbit hole and requires a significant time investment and is only relevant based on your team's priorities. – Murali Varma Jun 30 '23 at 18:55
0

You can structure your Node.js code in modules where each module's types are in type.ts file.

For example:

src/modules/devices/types.ts

export interface Route {
  timestamp: string
  name: string
  geojsonString: string
}

export type AddRouteBodyParams = Pick<Route, 'name' | 'geojsonString'>

export interface DeleteRouteBodyParams {
  deviceId: string
  timestamp: string
}

export interface EditRouteBodyParams {
  deviceId: string
  timestamp: string
  name: string
  geojsonString: string
}

src/modules/devices/controllerDevice.ts

import type { Request, Response } from 'express'
import type { ParsedQs } from 'qs'

import type { ResponseError, ResponseSuccess } from '../../api-types'

import { editRoute } from './serviceRoutes'
import type { EditRouteBodyParams } from './types'

export const deviceController = {
  editRoute: async (
    req: Request<any, any, EditRouteBodyParams, ParsedQs, Record<string, any>>,
    res: Response<ResponseSuccess | ResponseError>
  ) => {
    const editResponse = await editRoute({ ...req.body })
      if (editResponse instanceof Error) {
        return res.status(500).json({ error: editResponse.message })
      }
      return res.send({ message: `Route with name: ${req.body.name} was updated` })
  },
} as const

Then, you can export all api types into a single file and copy it into front-end project by running npm command:

"scripts": {
    "generate-types": "tsc -p tsconfig-export.json && cp ./dist/export-api-types.d.ts ../client_map_app/src/types"
}

You can check more details here.

Andrej Gajdos
  • 915
  • 7
  • 10