(4) Start formalizing type information relating to your codebase. Make some
interfaces andtypealiases.
The next step we’ll take in this project involves finding the right “home” for type information of various kinds. Two we’ll focus on in particular are
- Types that relate to our own code, and are part of our public API surface
- Fixes and customizations of types for dependencies, that are not part of our public API surface
Formalizing interfaces for our domain model
The models that a slack-like app needs to deal with include concepts like
jsTEAMS USERS┃ ┃have ┃many ┃┗━━ CHANNELS relate┃ tohave ┃many ┃┗━━ MESSAGES ━┛
We could create a bunch of interfaces that represent these concepts
tsTry/*** A user participating in a chat*/export interfaceIUser {id : number;username : string;name : string;iconUrl : string;}/*** A chat message*/export interfaceIMessage {id : number;teamId : string;channelId : string;userId : string;createdAt : string;user :IUser ;body : string;}/*** A team, containing one or more chat channels*/export interfaceITeam {iconUrl : string;name : string;id : string;channels :IChannel [];}/*** A chat channel, containing many chat messages*/export interfaceIChannel {teamId : string;name : string;description : string;id : string;messages :IMessage [];}
Create a new file packages/chat/src/types.ts
shtouch src/types.ts
and put the above code snippet in it and save. Start going through the other TypeScript modules, from the lowest level and moving upward, replacing some of the explicit object types with types derived from these interfaces.
Keep in mind that if we use these interfaces directly, everywhere their respective concepts is needed, we’ll end up exposing components to far more data (e.g. all of IChannels fields, when perhaps just name would suffice).
Use Pick<T> to choose specific properties where only one or two are needed. For example
tsTrytypeChannelNameAndId =Pick <IChannel , 'name'|'id' >
Don’t worry about being too complete in this pass. When we start tightening up rules, we can opportunistically catch more occurrences
Other kinds of type information
These interfaces are a great example of pure type information. Another is, adjustments to type information for dependencies. If you’ve ever dealt with a library that doesn’t have any types yet, you probably know where we’re going with this.
Even if libraries do provide their own types, sometimes they clash with what you have going on in your app. For example, they might use new TS language syntax that you’re not ready to adopt yet.
A local type roots folder gives you the flexibility to “patch” or overwrite types where necessary.
Create a new folder for these kinds of types to live
shellmkdir types
Some relevant parts of tsconfig
typeRootsallows us to tell TypeScript about top-level folders which may contain type informationpathsallows us to instruct the compiler to look for type information for specific modules in specific locationstypesallows us to specify which types can affect the global scope, and which appear in vscode auto-imports
Local type overrides
and in your top-level tsconfig add a paths and a baseUrl property
diff--- a/tsconfig.json+++ b/tsconfig.json@@ -10,7 +10,10 @@"outDir": "dist","declaration": true,"jsx": "react",- "moduleResolution": "Node"+ "moduleResolution": "Node",+ "paths": {+ "*": ["types/*"]+ }},"include": ["src"]
In this folder we can place our type overrides for dependencies. We’ll talk more about this in our discussion of ambient type information later. For now, let’s convince ourselves that it works.
in chat/src/utils/networking.ts make the following change near the top of the file
diff- import { HTTPError } from './http-error.cjs'- import { HTTPError } from 'http-error'
and delete chat/src/utils/http-error.cjs.
You should see that TypeScript is not happy about the 'http-error' module.
Could not find a declaration file for module 'http-error'
We’re now depending on a library for the HTTPError class, but it doesn’t provide any type information! This library is located within the workshop repo (we’ll learn more about how to do this later) at packages/http-error/index.js.
Let’s use this opportunity to “patch” the type information within our chat project
Create a new file
shtouch types/http-error.d.ts
put the following code snippet in it, and save
tsTryexport classHTTPError {}
Look back at networking.ts and you should see that the import resolves, but instantiation of HTTPErrors are now lighting up with errors like
Expected 0 arguments, but got 2
Look at the source code for the library in packages/http-error/index.js and consider what the constructor wants to accept as arguments.
Add this constructor declaration within the class declaration, and the errors should go away
tsconstructor(resp: Response, message: string)
Note that this isn’t a construct signature, it’s something a bit different. This is how we would denote the right shape of object in a declaration file using class syntax. We could also express this same type a different way
tsinterface HTTPErrorInstance extends Error {}export const HTTPError: {new(resp: Response, message: string): HTTPErrorInstance}
But this has a couple of disadvantages (e.g. it doesn’t result in the interface name being the same as the class name)
With either of these type declarations in place, you should be able to run
shyarn typecheck
and see no type-checking errors