Best practices for structuring TypeScript type declarations for a function module containing numerous private classes

Currently, I am in the process of creating type definitions for a library called fessonia. This task is not new to me, but this particular library has a different organization compared to others I have worked with before, presenting a unique challenge.

The main function in this library's index.js file is concise:

const getFessonia = (opts = {}) => {
  require('./lib/util/config')(opts);
  const Fessonia = {
    FFmpegCommand: require('./lib/ffmpeg_command'),
    FFmpegInput: require('./lib/ffmpeg_input'),
    FFmpegOutput: require('./lib/ffmpeg_output'),
    FilterNode: require('./lib/filter_node'),
    FilterChain: require('./lib/filter_chain')
  };
  return Fessonia;
}

module.exports = getFessonia;

This function exports an object containing classes, with getFessonia being the only public interface according to the library documentation. Despite other classes being accessible directly, it is recommended to only use getFessonia due to potential configuration issues.

  • I aim for the type definitions to advocate best practices when utilizing this library. This means excluding methods flagged as @private or not intended for external usage. For instance, proper types should enable downstream developers to declare variables with specific types:
import getFessonia from '@tedconf/fessonia';
// defining types
const config: getFessonia.ConfigOpts = {
    debug: true,
};
const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia(config);

My current approach involves creating separate .d.ts files for each required .js file to generate coherent type definitions. These files are then consolidated into index.d.ts, enhancing clarity and usability for developers.

As I progress, some concerns arise:

  • Do I need to provide a definition for the getConfig function, considering its limitation on direct usage? Should I export the Config interface directly instead and re-export it from index.d.ts? Similarly, how do I manage excessive aliasing under the getFessonia namespace?
  • Re-exporting within namespaces can be tedious and less elegant.
  • The potential nesting under getFessonia could lead to complexity in defining arguments and class shapes.
  • The organizational structure, such as using the FFmpeg namespace, might need refinement.

Your commitment to reading until the end is appreciated! While there may not be a definitive solution, I am open to exploring different approaches taken by others and welcome recommendations for improvement. Thank you!

Answer №1

@alex-wayne's insight was a game-changer for me. I am grateful.

I realized that I had been mistakenly assuming that the library's use of default exports prevented me from exporting other entities in my type definitions. Perhaps lack of sleep played a role in this oversight!

In addition to exporting the function getFessonia as a default, I also exported an interface Fessonia to define the return value and a namespace with the same name (more on TypeScript's combining behavior) to provide types for getFessonia's options and various other entities from the library. Here is how my updated index.d.ts file looks:

(import statements omitted for brevity)
/** Main function interface to the library. Returns object of classes when called. */
export default function getFessonia(opts?: Partial<Fessonia.ConfigOpts>): Fessonia;

export interface Fessonia {
  FFmpegCommand: typeof FFmpegCommand;
  FFmpegInput: typeof FFmpegInput;
  FFmpegOutput: typeof FFmpegOutput;
  FilterChain: typeof FilterChain;
  FilterNode: typeof FilterNode;
}

(import statements for types only)
export namespace Fessonia {
    export type ConfigOpts = Partial<FessoniaConfig>;

    export {
      FFmpegCommandType as FFmpegCommand,
      FFmpegError,
      FFmpegInputType as FFmpegInput,
      FFmpegOutputType as FFmpegOutput,
      FilterChainType as FilterChain,
      FilterNodeType as FilterNode,
    };
}

My approach for defining the classes within the Fessonia object involved creating individual type definitions for each class and exporting them without including private members. If a class method had parameters with complex types, I would define those types separately and put them in a namespace with the same name as the class, like so:

// abridged version of types/lib/ffmpeg_input.d.ts
export default FFmpegInput;

declare class FFmpegInput {
    constructor(url: FFmpegInput.UrlParam, options?: FFmpegInput.Options);
}

declare namespace FFmpegInput {
    export type Options = Map<string, FFmpegOption.OptionValue> | { [key: string]: FFmpegOption.OptionValue };
    export type UrlParam = string | FilterNode | FilterChain | FilterGraph;
}

By re-exporting the types at the end of index.d.ts, I was able to write code like the following downstream:

import getFessonia, { Fessonia } from '@tedconf/fessonia';

const { FFmpegCommand, FFmpegInput, FFmpegOutput } = getFessonia();
// note the type assignment
const outputOptions: Fessonia.FFmpegOutput.Options = { /* some stuff */ };
const output = new FFmpegOutput('some/path', outputOptions);
const cmd = new FFmpegCommand(commandOpts);

Although the revised structure may not be dramatically different from the original, it does feel like an enhancement. I kept the organization simple and consistent with the existing codebase, introducing the Fessonia namespace where necessary. Overall, the readability has improved.

You can view my initial attempt at typing this library on GitHub. Thank you to all who provided feedback and encouraged me to think outside the box.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Implementing Dynamic FormControl Values within FormGroup in Angular: A Step-by-Step Guide

GenerateFields(customControl, customValue): FormGroup { return this.fb.group({ customControl: new FormControl(customValue), }) } I am looking for a way to dynamically add the value of customControl from the parameter passed in the Ge ...

Creating a rootscope variable within an .ASPX file

I'm currently facing a challenge of defining an AngularJS rootscope variable within an .ASPX file for utilization in a TypeScript file. However, I'm not entirely sure on the best approach to achieve this. I am willing to explore any viable method ...

Unit Testing AngularJS Configuration in TypeScript with Jasmine

I'm in the process of Unit Testing the configuration of a newly developed AngularJS component. Our application uses ui-router for handling routing. While we had no issues testing components written in plain Javascript, we are encountering difficulties ...

typescript provides an asynchronous recursive mapping function that allows for changing parent data starting from the leaf node

I am currently facing a challenge in identifying the leaf node within a non-binary tree that requires modification. The process involves computing a blake2b hash from the leaf data, passing it to the parent node, and repeating this calculation until reachi ...

How to implement a material chiplist in Angular 8 using formGroup

Struggling to include a chip list of Angular material within an Ng form? Unable to add a new chip list upon button click and uncertain about displaying the value of the array added in the new chip list. Take a look at this example: https://stackblitz.com/e ...

How to transfer an object between sibling components in Angular 4

Being new to Angular 2+, I understand that I may not be approaching this the correct way. My issue involves two sibling components. When clicking on a "link" in one component, it redirects to another controller. My goal is to pass an object to the componen ...

Retrieve data from a table within an Angular component

Struggling with the ng2-smart-table library, I am facing challenges in passing values entered in the edit line to a custom component: Refer to the code snippet below for passing Maximum and Minimum Temperature values to the SmartTableEditorFunctionsCompon ...

Error Encountered (TypeError): Unable to access attributes of undefined (attempting to read 'appendChild')

I have been working on creating a choropleth Map of AntV using React.js with functional components. This is the code snippet for my chart: import DataSet from '@antv/data-set'; import { Chart } from '@antv/g2'; const CustomerByRegion = ...

Enter the quiz that starts counting down as soon as you open it

Is there a way to develop a web application that can send emails to specific users with a link that expires in 3 days? When the user clicks on the link before it expires, a new browser window/tab will open with personalized questions and a countdown timer ...

Bringing in a module that enhances a class

While scouring for a method to rotate markers using leaflet.js, I stumbled upon the module leaflet-rotatedmarker. After installing it via npm, I find myself at a loss on how to actually implement it. According to the readme, it simply extends the existing ...

Leverage a custom server (such as NestJS) within NextJS to dynamically render targeted pages

I am experimenting with using NestJS as a custom server for NextJS, following the instructions in this article. Here is a simplified version of the code: @Controller('/') export class ViewController { @Get('*') async static(@Req() r ...

What is the best way to define the type of an object in TypeScript when passing it to a function

I am currently working on a function that accepts an object of keys with values that have specific types. The type for one field is determined by the type of another field in the same object. Here is the code: // Consider this Alpha type and echo function. ...

Issue: Map container not located when implementing a leaflet map with Angular

Here is the complete error message: core.js:6479 ERROR Error: Map container not found. at NewClass._initContainer (leaflet-src.js:4066) at NewClass.initialize (leaflet-src.js:3099) at new NewClass (leaflet-src.js:296) at Object.createMap [a ...

Preventing going back to a previous step or disabling a step within the CDK Stepper functionality

In my Angular application, there is a CdkStepper with 4 steps that functions as expected. Each step must be completed in order, but users can always go back to the previous step if needed. For more information on CdkStepper: https://material.angular.io/cd ...

Can an Angular Component be displayed using a Serverless function like Lambda on AWS?

I have a single-page application developed in JavaScript using the Angular 6 Framework, and I am interested in dynamically rendering an Angular Component that is hosted on a remote server. Currently, I am utilizing viewContainerRef to dynamically render ...

A different approach to fixing the error "Uncaught (in promise) TypeError: fs.writeFile is not a function" in TensorFlow.js when running on Chrome

I've been attempting to export a variable in the TensorFlow posenet model while it's running in the Chrome browser using the code snippet below. After going through various discussions, I discovered that exporting a variable with fswritefile in t ...

Issue with data not being assigned to TypeScript class variable

Utilizing an http call to fetch data from the backend, I am able to see the retrieved data in the browser console. However, for some reason, the data is not being assigned to the class variable as expected. The code snippet is shown below: "use strict"; ...

Firefox validation doesn't prevent users from entering invalid input

In my Angular6 project, I encountered an issue with a numeric input field. When entering a letter in Chrome, it does not allow the insertion of the letter. However, in Firefox, letters can be typed inside the input field. While the validation function work ...

Filter a data array using a two-dimensional filter array that may change dynamically

Currently, I am facing a challenge where I need to filter a data array of objects using a two-dimensional filter array of objects. Here is a glimpse of my data array: dataArray = [{ Filter_GroupSize: "1" Filter_Time: "5-30" title: "Tools Test ...

Styling <Link> component with styled-components: A step-by-step guide

Utilizing the Link component from @material-ui/core/Link in my TypeScript code was initially successful: <Link href="#" variant="body2"> Forgot? </Link> However, I am exploring the transition to styled-components located in a separate file. ...