Skip to content
Educora
Advanced20 min8 / 8

Modern patterns and declaration files

Learn what strict mode catches, get precise types with `as const` and `satisfies`, and type third-party JavaScript libraries with `.d.ts` declaration files.

Check yourself
In this lesson you will learn
  • Explain which checks the strict mode turns on
  • Get exact literal types from values with as const
  • Compare satisfies with a type annotation and with as
  • Use @types packages and write a simple .d.ts file

You already know types, functions, interfaces, unions, classes and generics. This last lesson is about four tools professionals use every day: strict mode, which catches most mistakes; as const, for getting exact types from values; satisfies, which checks a type without widening it; and declaration files, which introduce untyped JavaScript libraries to TypeScript.

Strict mode

"strict": true in tsconfig.json is not one option but a whole set of checks. Without it TypeScript lets many things slide, and the most common JavaScript bugs slip through. These are the most important members of the set:

  • strictNullChecks: null and undefined are separate types; a value that is “possibly undefined” can't be used without a check.
  • noImplicitAny: a parameter whose type can't be worked out doesn't silently become any — you get an error.
  • strictPropertyInitialization: every class field must get a value in its declaration or in the constructor.
  • useUnknownInCatchVariables: in catch (error), error has the type unknown rather than any.
TypeScript
const users = [
  { name: 'Aysel', age: 15 },
  { name: 'Murad', age: 17 },
];

const found = users.find((u) => u.name === 'Leyla');
console.log(found.age);
Text
users.ts:7:13 - error TS18048: 'found' is possibly 'undefined'.

7 console.log(found.age);
              ~~~~~
find may find nothing. Without strict mode the code would compile cleanly and then crash at runtime with TypeError: Cannot read properties of undefined (reading 'age').
TypeScript
const users = [
  { name: 'Aysel', age: 15 },
  { name: 'Murad', age: 17 },
];

const found = users.find((u) => u.name === 'Leyla');
console.log(found?.age ?? 'not found');
Expected output
not found
The proper fix: handle the “not found” case with ?. and ??.

as const

as const asks TypeScript to treat a value as “frozen”: strings become exact literal types instead of string, an array becomes a read-only tuple, and an object's properties become readonly. This is ideal for fixed lists such as roles, statuses and settings, and it neatly replaces the enum we talked about earlier:

TypeScript
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number];

function canEdit(role: Role): boolean {
  return role !== 'viewer';
}

const Status = { Active: 'active', Blocked: 'blocked' } as const;
type Status = (typeof Status)[keyof typeof Status];

const s: Status = Status.Blocked;
console.log(ROLES.join(', '), canEdit('editor'), s);
Expected output
admin, editor, viewer true blocked
The Role type is 'admin' | 'editor' | 'viewer', and the Status type is 'active' | 'blocked'. The list also exists in the program, so you can print it and loop over it.
TypeScript
const ROLES = ['admin', 'editor', 'viewer'] as const;
ROLES.push('guest');
Text
roles.ts:2:7 - error TS2339: Property 'push' does not exist on type 'readonly ["admin", "editor", "viewer"]'.

2 ROLES.push('guest');
        ~~~~
as const turns the array into a read-only tuple, so it has no push method.

The satisfies operator

A type annotation (const x: T = ...) checks the value but widens the variable's type to T, and exact information about the value is lost. satisfies T only checks: the value must fit T, while the variable keeps the precise type that TypeScript inferred. In a colour palette some colours are strings and some are tuples of three numbers:

TypeScript
type ColorName = 'red' | 'green' | 'blue';
type Color = string | [number, number, number];

const palette = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
} satisfies Record<ColorName, Color>;

console.log(palette.green.toUpperCase());
console.log(palette.red[0]);
Expected output
#00FF00
255
TypeScript knows that green is a string and red is a tuple, because satisfies keeps the precise type.
TypeScript
type ColorName = 'red' | 'green' | 'blue';
type Color = string | [number, number, number];

const annotated: Record<ColorName, Color> = {
  red: [255, 0, 0],
  green: '#00ff00',
  blue: [0, 0, 255],
};
annotated.green.toUpperCase();

const checked = {
  red: [255, 0, 0],
  green: '#00ff00',
  bleu: [0, 0, 255],
} satisfies Record<ColorName, Color>;
Text
palette.ts:9:17 - error TS2339: Property 'toUpperCase' does not exist on type 'Color'.
  Property 'toUpperCase' does not exist on type '[number, number, number]'.

9 annotated.green.toUpperCase();
                  ~~~~~~~~~~~

palette.ts:14:3 - error TS2353: Object literal may only specify known properties, and 'bleu' does not exist in type 'Record<ColorName, Color>'.

14   bleu: [0, 0, 255],
     ~~~~
First error: after the annotation, green is just a Color. Second error: satisfies still checks the value and catches the typo bleu.
SyntaxChecks?Variable's type
const x: T = valueyesT (widened)
const x = value satisfies Tyesthe value's precise type
const x = value as Thardly at allT

Declaration files and third-party libraries

Definition
Declaration file (.d.ts)

A file that contains only types and no executable code. It is a “manual” for JavaScript code, written for TypeScript: which functions exist, which parameters they take and what they return.

Types for npm packages arrive in one of three ways. Many modern packages ship their own .d.ts files, and there is nothing to do. For others, the community publishes types in separate @types/... packages. If neither exists, you write the declaration file yourself.

Terminal
npm install lodash
npm install --save-dev @types/lodash
The library itself goes into dependencies, and its types go into devDependencies because they are needed only during development.

Suppose the project uses an old library called old-slug that has no types — it turns text into a short name for a URL. When you import it, TypeScript warns that it can't find the module's types:

Text
src/index.ts:1:21 - error TS7016: Could not find a declaration file for module 'old-slug'. '/home/leyla/blog/node_modules/old-slug/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/old-slug` if it exists or add a new declaration (.d.ts) file containing `declare module 'old-slug';`

1 import slugify from 'old-slug';
                      ~~~~~~~~~~

If there is no @types/old-slug package, we create the file types/old-slug.d.ts and describe the module with declare module. The word declare says that this code already exists elsewhere; we are only stating its type:

TypeScript
declare module 'old-slug' {
  export default function slugify(text: string, separator?: string): string;
}
types/old-slug.d.ts: a function signature with no body, types only.
TypeScript
import slugify from 'old-slug';

const title = 'Hello from Baku';
console.log(slugify(title));
console.log(slugify(title, '_'));
Expected output
hello-from-baku
hello_from_baku
src/index.ts now type-checks cleanly, the editor suggests the parameters, and a call like slugify(42) would give error TS2345.

When you publish your own library, you don't have to write .d.ts files by hand. With the option "declaration": true in tsconfig.json, tsc creates a .d.ts file next to every .js file:

TypeScript
export interface User {
    name: string;
    age: number;
}
export declare function greet(user: User): string;
greet.d.ts generated from greet.ts: the interface and the function's signature remain, while the body lives in greet.js.

Key points

  • strict: true turns on strictNullChecks, noImplicitAny and other checks; always keep it on.
  • as and ! hide the error message but don't fix the problem.
  • as const turns a value into exact literal, read-only types; (typeof LIST)[number] derives a union type from a list.
  • satisfies T checks a value against T but keeps the variable's precise type.
  • .d.ts files contain only types; types come from the package itself, from @types/... packages, or from a file you write with declare module.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What does "strict": true in tsconfig.json do?