Mastering TypeScript Type Definitions
This challenge focuses on creating robust and accurate type definition files (.d.ts) for existing JavaScript code. Properly typed code enhances maintainability, reduces runtime errors, and improves developer experience by providing excellent autocompletion and static analysis. You will be tasked with defining types for a small, but representative, JavaScript library.
Problem Description
You are given a set of JavaScript files that represent a small utility library. Your goal is to create corresponding TypeScript definition files (.d.ts) that accurately describe the types of the exported functions, classes, and variables within this library. This will allow TypeScript developers to use this JavaScript library with full type safety and IntelliSense.
Key Requirements:
- Define Function Signatures: Accurately type all function parameters and return values, including complex types like unions, intersections, generics, and optional parameters.
- Define Class Structures: Type class properties, methods, constructors, and inheritance if applicable.
- Define Variable/Constant Types: Accurately type any exported variables or constants.
- Handle Overloads: If a function supports multiple signatures based on input types, define these overloads.
- Export Public API: Ensure that only the intended public API of the library is exported in the type definitions.
Expected Behavior:
A TypeScript project that imports this JavaScript library should be able to:
- Receive accurate autocompletion for exported members.
- Catch type errors at compile time when using the library incorrectly.
- Understand the expected data structures and types for parameters and return values.
Edge Cases to Consider:
- JavaScript libraries might use dynamic property assignments or less strict typing. Your
.d.tsfile should enforce stricter, intended types. - Consider how to represent JavaScript arrays with specific element types or tuples.
- Think about callback functions and their expected parameters and return types.
Examples
Let's assume we have the following JavaScript file:
utils.js
// utils.js
/**
* Adds two numbers.
* @param {number} a The first number.
* @param {number} b The second number.
* @returns {number} The sum of a and b.
*/
export function add(a, b) {
return a + b;
}
/**
* Formats a name.
* @param {string} firstName The first name.
* @param {string} [lastName] The last name (optional).
* @returns {string} The formatted name.
*/
export function formatName(firstName, lastName) {
if (lastName) {
return `${firstName} ${lastName}`;
}
return firstName;
}
/**
* Represents a simple user.
*/
export class User {
/**
* @param {string} name The user's name.
* @param {number} age The user's age.
*/
constructor(name, age) {
this.name = name;
this.age = age;
}
/**
* Greets the user.
* @returns {string} A greeting message.
*/
greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
/**
* A configuration object.
* @typedef {Object} Config
* @property {string} apiUrl - The API endpoint.
* @property {number} timeout - The request timeout in milliseconds.
* @property {boolean} [enableLogging=false] - Whether to enable logging.
*/
/**
* Processes a configuration.
* @param {Config} config - The configuration object.
* @returns {Promise<void>} A promise that resolves when processing is complete.
*/
export async function processConfig(config) {
console.log(`Processing API: ${config.apiUrl}`);
console.log(`Timeout: ${config.timeout}ms`);
if (config.enableLogging) {
console.log('Logging enabled.');
}
await new Promise(resolve => setTimeout(resolve, config.timeout));
}
export const DEFAULT_TIMEOUT = 5000;
Your Task: Create a utils.d.ts file that accurately types the above utils.js code.
Example 1:
Input:
// In a TypeScript file importing utils.js
import { add, formatName, User, processConfig, DEFAULT_TIMEOUT } from './utils';
const sum = add(5, 10);
const fullName = formatName('John', 'Doe');
const greeting = new User('Alice', 30).greet();
const config = { apiUrl: 'http://api.example.com', timeout: 2000 };
processConfig(config);
const timeoutValue = DEFAULT_TIMEOUT;
console.log(sum); // Expected: 15
console.log(fullName); // Expected: John Doe
console.log(greeting); // Expected: Hello, my name is Alice and I am 30 years old.
console.log(timeoutValue); // Expected: 5000
// Incorrect usage that should be caught by types:
// const invalidSum = add('5', 10); // Should be a type error
// const invalidName = formatName(123); // Should be a type error
// new User('Bob'); // Missing age, should be a type error
// processConfig({ apiUrl: 'test' }); // Missing timeout and enableLogging, could be type error depending on strictness
Output (for utils.d.ts):
// utils.d.ts
/**
* Adds two numbers.
*/
export function add(a: number, b: number): number;
/**
* Formats a name.
*/
export function formatName(firstName: string, lastName?: string): string;
/**
* Represents a simple user.
*/
export class User {
name: string;
age: number;
/**
* @param name The user's name.
* @param age The user's age.
*/
constructor(name: string, age: number);
/**
* Greets the user.
*/
greet(): string;
}
/**
* A configuration object.
*/
export interface Config {
apiUrl: string;
timeout: number;
enableLogging?: boolean;
}
/**
* Processes a configuration.
*/
export function processConfig(config: Config): Promise<void>;
/**
* The default timeout value in milliseconds.
*/
export const DEFAULT_TIMEOUT: number;
Explanation:
The utils.d.ts file defines the types for each exported member. add takes two numbers and returns a number. formatName takes a required string and an optional string, returning a string. The User class is defined with its properties and methods. The Config interface is created to describe the structure of the configuration object, and processConfig uses this interface, correctly specifying its return type as Promise<void>. DEFAULT_TIMEOUT is typed as a number.
Example 2 (Handling Overloads):
Assume a function find that can find an element by ID (number) or by name (string).
search.js
// search.js
export function find(idOrName) {
// ... implementation ...
if (typeof idOrName === 'number') {
return { id: idOrName, name: `Item ${idOrName}` };
} else {
return { id: Math.random(), name: idOrName };
}
}
Your Task: Create a search.d.ts file for the find function.
Output (for search.d.ts):
// search.d.ts
interface FoundItem {
id: number;
name: string;
}
/**
* Finds an item by its ID or name.
*/
export function find(id: number): FoundItem;
export function find(name: string): FoundItem;
Explanation:
The search.d.ts file uses function overloads to define the two distinct ways the find function can be called and what it returns. An interface FoundItem is defined to represent the common return type.
Constraints
- The provided JavaScript files will not contain any ES6 module syntax (e.g.,
import/export) within their internal logic, only at the top level for exporting. - The library will not use complex metaprogramming or dynamic imports that would be impossible to statically type.
- Focus on correctness and clarity of the type definitions. Performance of the type checking itself is not a primary concern for this challenge.
- You will be provided with multiple
.jsfiles, and you must create corresponding.d.tsfiles for each.
Notes
- You can use JSDoc comments in your
.d.tsfiles to provide documentation that will be picked up by IDEs. - Consider using
declare modulefor grouping related types and exports if the library becomes very large, though for this challenge, individual.d.tsfiles are expected. - Pay close attention to the difference between
interfaceandtypein TypeScript when defining structures. - Think about how to represent JavaScript arrays of specific types (e.g.,
Array<number>ornumber[]). - The challenge simulates real-world scenarios where you might need to add types to existing JavaScript codebases.