Hand-Write a .d.ts for a Small JS Helper
Write a declaration file for a JavaScript helper with a generic function and an overload, then test it with a short file. After this, calls to the helper are checked and the JavaScript stays as it is.
Skip ahead: If you have written a .d.ts before and only want the part about generics and overloads, skip to "Step 3: generics", then continue.
You have a small JavaScript helper that your TypeScript code imports, and every call to it is any. This page writes the declaration file for it, with one generic function and one function that takes two forms of input. Total time is about fifteen minutes. The lesson on declaration files in the course explains what a .d.ts promises. This is the recipe for a helper.
All the code runs as ES modules.
npm pkg set type=module
Step 1: the helper and the error
export function chunk(items, size) {
const out = [];
for (let i = 0; i < items.length; i += size) {
out.push(items.slice(i, i + size));
}
return out;
}
export function pick(value, key) {
if (Array.isArray(value)) {
return value.map((item) => item[key]);
}
return value[key];
}
import { chunk, pick } from "./array_utils.js";
console.log(chunk([1, 2, 3, 4, 5], 2));
console.log(pick({ name: "Ana", age: 31 }, "name"));
console.log(pick([{ name: "Ana" }, { name: "Ben" }], "name"));
use.ts(1,29): error TS7016: Could not find a declaration file for module './array_utils.js'. '/path/to/project/array_utils.js' implicitly has an 'any' type.
The compiler found the file and has no types for it. Without a declaration, every import is any. Create array_utils.d.ts next to the JavaScript.
Step 2: the plain function
export function chunk(items: readonly unknown[], size: number): unknown[][];
That is already better, and it is not good enough. chunk([1, 2, 3], 2) returns unknown[][], so the caller loses the number type and has to cast. The helper returns the same kind of thing it receives, which is what a generic is for.
Step 3: generics
export function chunk<T>(items: readonly T[], size: number): T[][];
import { chunk } from "./array_utils.js";
const pages = chunk([1, 2, 3, 4, 5], 2);
const first: string[] = pages[0];
chunk_use.ts(4,7): error TS2322: Type 'number[]' is not assignable to type 'string[]'.
Type 'number' is not assignable to type 'string'.
T is number here, so the result is number[][], and the compiler refuses to treat a number array as strings. The parameter is readonly T[] because the function does not change its input, and a read-only parameter accepts both ordinary and read-only arrays. Always write it that way for input you only read.
Step 4: overloads for two forms of input
pick does two different things. Given one object, it returns one value. Given an array of objects, it returns an array. Describe each form on its own line. These are overloads.
export function chunk<T>(items: readonly T[], size: number): T[][];
export function pick<T, K extends keyof T>(value: T, key: K): T[K];
export function pick<T, K extends keyof T>(value: readonly T[], key: K): T[K][];
import { pick } from "./array_utils.js";
const person = { name: "Ana", age: 31 };
const people = [person, { name: "Ben", age: 28 }];
const name: string = pick(person, "name");
const names: string[] = pick(people, "name");
const ages: number[] = pick(people, "age");
console.log(name, names, ages);
Ana [ 'Ana', 'Ben' ] [ 31, 28 ]
The compiler tries the overloads from the top and uses the first that fits. Order matters: put the more specific signature first. Here an array is not a plain T with a name key, so the object form fails for arrays and the second line matches.
The K extends keyof T part gives you the key check for free.
import { pick } from "./array_utils.js";
const person = { name: "Ana", age: 31 };
pick(person, "nmae");
pick_errors.ts(5,6): error TS2769: No overload matches this call.
The last overload gave the following error.
Type '{ name: string; age: number; }' is missing the following properties from type 'readonly unknown[]': length, concat, join, slice, and 26 more.
With overloads the message is about the last overload the compiler tried, which is why it talks about arrays. The real mistake is the misspelled key nmae, so check your own argument before you read the long text.
Step 5: test the declaration
The compiler does not compare the .d.ts with the JavaScript. If the declaration lies, it is trusted. A short test file is how you find out. Calls that should work and calls that should fail, and tsc --noEmit runs it.
import { chunk, pick } from "./array_utils.js";
const nums: number[][] = chunk([1, 2, 3], 2);
const name: string = pick({ name: "Ana" }, "name");
const names: string[] = pick([{ name: "Ana" }], "name");
// @ts-expect-error the key must exist on the object
pick({ name: "Ana" }, "age");
console.log(nums, name, names);
Keep this file in the repository and run it in CI. If the .d.ts changes in a way that breaks the examples, the test fails. If the JavaScript changes and the examples still compile, the test cannot tell you, so also run the helper's real tests.
⚠ Warning: Do not write any where a type is hard. A hole in a declaration spreads to every caller. If you do not know yet, write unknown and let callers narrow it, or leave that export out and import it with an explicit cast until you do.
Common mistakes
- Naming the file
array_utils.d.tsbut importing./array_utilswithout the extension under ES modules. The import path ends in.js, matching the output. - Putting the declaration in a
typesfolder that is not ininclude. A.d.tsnext to its.jsis found by name. - Writing a body in a
.d.ts. Functions end with a semicolon.
Related course: TypeScript Advanced.
About
Write a declaration file for a JavaScript helper with a generic function and an overload, then test it with a short file. After this, calls to the helper are checked and the JavaScript stays as it is.
- Responsible
- Mary Q
- Last Update
- 10/22/2026
- Reading Time
- 5 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.