7

For application code, is it advisable to put Redux store types in state-slice.d.ts files?

I inherited a codebase where all the core types have been placed in .d.ts definition files, but this is contrary to how I’ve come to understand the role of definition files.

My understanding is that definition files are for when you’re writing a library (which is a convenient place for authors to centralize an interface to define our library's API that we're exposing to consumers), but is ill-suited for application code since data can change rapidly.

That said, I’ve not been able to come up with a good argument against .d.ts files per se - from a functional standpoint, I don’t see the difference of referencing a type

declare namespace Domain {
  interface State {
    // ...
  }
} 

from a definition file, or explicitly importing a file from a file that has

export interface ReduxState {}

To add bit more context, since this is a bit of a peculiar question - I’m currently working on a new project that needs to share these core types from the existing codebase I inherited, and I’m trying to move these types into a separate package.

Unfortunately I’ve been running into issues with importing and exporting these types - it seems that I have to (note the exports)

/* package A */
export declare namespace StateA { }

/* package B */
import { StateA } from 'packageA/StateA' 

const stateA: StateA = {};

instead of TypeScript automatically picking it up.

/* package B (originally) */

// stateA.d.ts
declare namespace StateA {}

// test.ts
const stateA: StateA = {};

I’ve tried changing the typeRoots field in the tsconfig.json file to see if I can avoid having to insert import statements everywhere, to no avail.

I was thinking that this might a good opportunity to convert all type definition files to ordinary type files, but I just can’t come up with a good reason apart from "I don’t think we should be using type definition files" and "I can’t get the types in the definition types to work like they do now".

So my questions are

  • What are the benefits of using type definition files over defining types in an ordinary .ts file?
  • Should we be writing our own definition files when working with consumer facing application code?
  • Am I thinking too much and it really doesn't matter?

I do apologize in advance if this question is a bit open-ended, I hope this isn't ill-suited for the StackOverflow community (or if it is, what would be a good forum to ask?). Thanks!

Baggio Wong
  • 357
  • 1
  • 8

1 Answers1

4

Have you seen this answer How do I use namespaces with TypeScript external modules?

I don't totally agree with it, but I think it's worth it to take a look. I recently asked me all those questions too.e

Here is what I've been doing:

For types that are only used within one file or a few files, I'm defining and exporting them from regular ts files.

For example:

store.ts

This file defines an APP_THUNK type that are only used by a few files that handles the creation of redux thunks. So, I'm defining and exporting from this file, since it uses the ROOT_STATE type that is also defined here.

import { configureStore } from "@reduxjs/toolkit";
import { rootReducer } from "./rootReducer";
import { Action } from "@reduxjs/toolkit";
import { ThunkAction } from "redux-thunk";

export interface ROOT_STATE {
  ...
}

export type APP_THUNK = ThunkAction<
  Promise<void> | void,
  ROOT_STATE,
  unknown,
  Action<string>
>

export const store = configureStore({
  reducer: rootReducer
});

Then, when I need to use those types, I import them: import { APP_THUNK } from "@redux/store";

But I also have some types that are used across my whole project, in lots of files. And for those kind of types I'm keeping d.ts files and making them accessible by using declare namespace SOME_NAMESPACE.

For example:

BLOGPOST.d.ts

This file defines all types related to blogPost objects.

declare namespace PROJECT {

  interface BLOGPOST {
    id: string,
    slug: string,
    images: BLOGPOST_IMAGE[]
  }

  interface BLOGPOST_IMAGE {
    id:        string,
    src:       string,
    alt:       string,
    caption:   string,
  }

  // AND SO ON...
  // THERE ARE MANY MORE TYPES HERE RELATED TO BLOGPOSTS  

}

I also merge the PROJECT namespace from different files. For example: PRODUCTS.d.ts also declares namespace PROJECT and it's automatically merged with the one on BLOGPOSTS.d.ts.

And using those types is very convenient:

interface Props {
  blogPost: PROJECT.BLOGPOST
}

// OR MAYBE

const blogPost = await API.getBlogPost() as PROJECT.BLOGPOST;

And the auto complete becomes instantly grouped under that namespace. Which I think is also very nice.

enter image description here

This works for me. Please consider that I work alone and this types are only used across this one project. Don't know if this pattern would work for multi-person teams or across different projects.

Also note that in Typescript offical docs, we get that:

Using Modules

Modules can contain both code and declarations.

Modules also have a dependency on a module loader (such as CommonJs/Require.js) or a runtime which supports ES Modules. Modules provide for better code reuse, stronger isolation and better tooling support for bundling.

It is also worth noting that, for Node.js applications, modules are the default and we recommended modules over namespaces in modern code.

Starting with ECMAScript 2015, modules are native part of the language, and should be supported by all compliant engine implementations. Thus, for new projects modules would be the recommended code organization mechanism.

But I still think that when you are consuming those types, it's a nicer dev experience when you are using namespaces over modules. But be aware of namespace collisions, given the fact they are global.

cbdeveloper
  • 27,898
  • 37
  • 155
  • 336
  • Thanks for sharing all of this! That's actually the same way we're approaching types - I just don't think `namespace`s offer anything that ES6 modules don't already do, except maybe we can save an `import` statement and get autocomplete, which aren't good reasons to choose one over the other. I think we'll stick with this strategy for now and revise as needed, but perhaps to avoid `namespace`s when adding new types. Thanks again! – Baggio Wong Oct 01 '20 at 20:39