|$ curl https://forge-ai.dev/api/markdown?path=docs/js/typescript
$cat docs/javascript-—-typescript-integration.md
updated This week·25 min read·published

JavaScript — TypeScript Integration

JavaScriptTypeScriptIntermediateIntermediate
Introduction

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It adds static type checking, interfaces, generics, and advanced tooling to the JavaScript ecosystem. TypeScript does not change how JavaScript runs — it catches type-related errors at compile time rather than runtime, shifting bug discovery earlier in the development cycle.

The relationship between JavaScript and TypeScript is pragmatic: any valid JavaScript is valid TypeScript (with some caveats). TypeScript infers types where possible, and you can opt in to stricter checking gradually. This makes adoption incremental — you can add TypeScript to a JavaScript project file by file, or use JSDoc annotations for type checking without changing file extensions.

TypeScript's value proposition extends beyond type safety: it enables better IDE support (autocomplete, refactoring, navigation), serves as living documentation for APIs, and makes codebases more maintainable at scale. This guide covers the practical integration of TypeScript into JavaScript projects, from basic annotations to advanced patterns.

typescript-intro.ts
TypeScript
1// Basic TypeScript — type annotations
2function greet(name: string): string {
3 return `Hello, ${name}!`;
4}
5
6// TypeScript infers the return type
7const message = greet('World'); // inferred as string
8
9// TypeScript catches errors at compile time
10// greet(42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'
11
12// Interfaces define the shape of objects
13interface User {
14 id: number;
15 name: string;
16 email: string;
17 role?: 'admin' | 'user'; // optional with union type
18}
19
20function createUser(data: User): User {
21 return {
22 id: data.id,
23 name: data.name,
24 email: data.email,
25 role: data.role ?? 'user',
26 };
27}
28
29// Generics — reusable type-safe components
30function identity<T>(value: T): T {
31 return value;
32}
33
34const num = identity<number>(42);
35const str = identity('hello'); // type inferred as string
Type Annotations

Type annotations are the core of TypeScript. They declare the expected types of variables, function parameters, return values, and properties. TypeScript's type system is structural (not nominal) — compatibility is determined by the shape of types, not their declared names. The compiler uses type inference extensively, so annotations are only required where the type cannot be automatically determined.

TypeExampleDescription
stringlet name: stringText values
numberlet age: numberAll numeric values (int, float, NaN)
booleanlet isActive: booleantrue / false
T[]let items: number[]Array of a given type
[T, U]let pair: [string, number]Tuple of fixed length
T | Ulet id: string | numberUnion type (either/or)
T & Ulet obj: A & BIntersection type (both)
T?let name?: stringOptional (can be undefined)
T | nulllet data: T | nullNullable type
unknownlet val: unknownType-safe any (must narrow before use)
type-annotations.ts
TypeScript
1// Type inference — TypeScript figures out types automatically
2let name = 'Alice'; // inferred as string
3let count = 42; // inferred as number
4let isReady = true; // inferred as boolean
5
6// Explicit annotations — when inference isn't enough
7let data: string | null = null;
8let ids: number[] = [];
9let pair: [string, number] = ['age', 30];
10let callback: (x: number) => void;
11
12// Union types — value can be one of several types
13function formatId(id: string | number): string {
14 return `ID: ${id}`;
15}
16
17// Type narrowing — refine union types with control flow
18function process(value: string | number | null) {
19 if (value === null) {
20 console.log('No value provided');
21 } else if (typeof value === 'string') {
22 console.log(`String: ${value.toUpperCase()}`);
23 } else {
24 console.log(`Number: ${value.toFixed(2)}`);
25 }
26}
27
28// Literal types — exact values as types
29type Direction = 'up' | 'down' | 'left' | 'right';
30function move(dir: Direction) {
31 console.log(`Moving ${dir}`);
32}
33move('up'); // OK
34// move('diagonal'); // Error!
35
36// Type assertions — when you know more than the compiler
37const element = document.getElementById('app') as HTMLDivElement;
38// or: const element = <HTMLDivElement>document.getElementById('app');
39
40// Non-null assertion
41const input = document.querySelector('input')!;
42// ! tells TS: "I know this is not null"
Interfaces & Types

Interfaces and type aliases are the primary ways to define object shapes in TypeScript. Interfaces are extensible (declaration merging, extends), while type aliases can represent unions, intersections, primitives, and tuples. Choose interfaces for public API contracts and object definitions; use types for unions, computed properties, and complex compositions.

interfaces-types.ts
TypeScript
1// Interfaces — preferred for object shapes
2interface Person {
3 name: string;
4 age: number;
5 email?: string; // optional
6 readonly id: number; // cannot be modified after creation
7}
8
9// Interface extension
10interface Employee extends Person {
11 department: string;
12 salary: number;
13}
14
15// Declaration merging — interfaces can be augmented
16interface Person {
17 phone?: string;
18}
19// Person now has: name, age, email?, id, phone?
20
21// Type aliases — more flexible
22type Point = {
23 x: number;
24 y: number;
25};
26
27// Types can represent unions
28type Status = 'active' | 'inactive' | 'pending';
29type Result<T> = { success: true; data: T } | { success: false; error: string };
30
31// Types can use computed properties
32const key = 'role' as const;
33type Role = { [key]: string };
34
35// Types for function signatures
36type MathFn = (a: number, b: number) => number;
37const add: MathFn = (a, b) => a + b;
38
39// Generic interface
40interface Repository<T> {
41 get(id: string): Promise<T | null>;
42 getAll(): Promise<T[]>;
43 create(item: T): Promise<T>;
44 update(id: string, item: Partial<T>): Promise<T>;
45 delete(id: string): Promise<boolean>;
46}
47
48// Index signature — dynamic keys
49interface Dictionary<T> {
50 [key: string]: T | undefined;
51}
52
53const cache: Dictionary<number> = {};
54cache['visits'] = 42;
55
56// extends vs intersection
57interface Animal { name: string }
58interface Bear extends Animal { honey: boolean }
59// vs
60type BearType = Animal & { honey: boolean };
61// Both work for objects, but interfaces are more performant and debuggable

best practice

Use interface for public APIs and object definitions that may be extended. Use type for unions, intersections, mapped types, and any composition that interface cannot express. The rule is pragmatic: interface extends by declaration merging and inheritance; type creates aliases for any type expression.
Generics

Generics create reusable components that work with multiple types while maintaining type safety. Instead of specifying a concrete type, you use a type parameter (commonly T, U, V) that is resolved when the component is used. Generics are the backbone of TypeScript's standard library — Array<T>, Promise<T>, Map<K, V> are all generic.

generics.ts
TypeScript
1// Basic generic function
2function firstElement<T>(arr: T[]): T | undefined {
3 return arr[0];
4}
5
6const first = firstElement([1, 2, 3]); // type: number | undefined
7const str = firstElement(['a', 'b']); // type: string | undefined
8
9// Generic constraints — limit what types are accepted
10interface HasLength {
11 length: number;
12}
13
14function logLength<T extends HasLength>(item: T): T {
15 console.log(item.length);
16 return item;
17}
18
19logLength('hello'); // OK (string has length)
20logLength([1, 2, 3]); // OK (array has length)
21// logLength(42); // Error: number doesn't have length
22
23// Multiple type parameters
24function pair<T, U>(first: T, second: U): [T, U] {
25 return [first, second];
26}
27
28const result = pair('key', 42); // type: [string, number]
29
30// Generic class
31class Stack<T> {
32 private items: T[] = [];
33
34 push(item: T): void {
35 this.items.push(item);
36 }
37
38 pop(): T | undefined {
39 return this.items.pop();
40 }
41
42 peek(): T | undefined {
43 return this.items[this.items.length - 1];
44 }
45
46 get size(): number {
47 return this.items.length;
48 }
49}
50
51const numberStack = new Stack<number>();
52numberStack.push(1);
53numberStack.push(2);
54console.log(numberStack.pop()); // 2
55
56// Generic constraint with keyof
57function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
58 return obj[key];
59}
60
61const user = { name: 'Alice', age: 30, role: 'admin' };
62getProperty(user, 'name'); // type: string
63getProperty(user, 'age'); // type: number
64// getProperty(user, 'invalid'); // Error!
65
66// Generic mapped type
67type Readonly_<T> = {
68 readonly [P in keyof T]: T[P];
69};
70
71type Optional<T> = {
72 [P in keyof T]?: T[P];
73};

info

TypeScript can often infer generic type parameters from usage — you rarely need to explicitly specify them. Write const result = pair('key', 42) instead of pair<string, number>('key', 42). However, be explicit when inference gives unexpected results or when the generic has many parameters and you want to override inference for some.
preview
Utility Types

TypeScript provides a set of built-in utility types that transform existing types. These enable common type manipulations without writing complex generic logic. Utility types are the TypeScript equivalent of lodash for types — they compose and derive new types from existing ones, reducing boilerplate and improving consistency.

UtilityDescriptionExample
Partial<T>Makes all properties optionalPartial<User>
Required<T>Makes all properties requiredRequired<Config>
Readonly<T>Makes all properties readonlyReadonly<State>
Pick<T, K>Selects specific propertiesPick<User, 'id' | 'name'>
Omit<T, K>Removes specific propertiesOmit<User, 'password'>
Record<K, T>Creates object type with keys K and values TRecord<string, number>
Exclude<T, U>Excludes types from unionExclude<'a'|'b', 'a'>
ReturnType<T>Extracts return type of function typeReturnType<typeof fn>
Awaited<T>Unwraps promise typeAwaited<Promise<string>>
utility-types.ts
TypeScript
1// Partial — useful for updates
2interface User {
3 id: number;
4 name: string;
5 email: string;
6 role: string;
7}
8
9function updateUser(id: number, changes: Partial<User>): void {
10 // changes can have any subset of User properties
11}
12
13updateUser(1, { name: 'Alice' }); // OK — only updating name
14updateUser(2, { email: 'b@b.com', role: 'admin' }); // OK
15
16// Pick and Omit
17type UserPublic = Omit<User, 'email' | 'role'>; // only id, name
18type UserCredentials = Pick<User, 'email'>; // only email
19
20// Record — object with consistent value types
21const scores: Record<string, number> = {
22 alice: 95,
23 bob: 87,
24 charlie: 92,
25};
26
27// Record with union keys
28type Page = 'home' | 'about' | 'contact';
29const pages: Record<Page, { title: string; path: string }> = {
30 home: { title: 'Home', path: '/' },
31 about: { title: 'About', path: '/about' },
32 contact: { title: 'Contact', path: '/contact' },
33};
34
35// ReturnType — extract function return type
36function createUser(name: string, age: number) {
37 return { id: crypto.randomUUID(), name, age, createdAt: new Date() };
38}
39type CreatedUser = ReturnType<typeof createUser>;
40// { id: string; name: string; age: number; createdAt: Date }
41
42// Awaited — unwrap promises
43async function fetchData(): Promise<{ items: string[] }> {
44 return { items: ['a', 'b'] };
45}
46type Data = Awaited<ReturnType<typeof fetchData>>;
47// { items: string[] }
48
49// Combination pattern
50type UpdatePayload<Entity> = {
51 id: string;
52 changes: Partial<Entity>;
53 timestamp: number;
54};
55
56type UserUpdate = UpdatePayload<User>;
🔥

pro tip

Master the combination of utility types with mapped types: Partial<Pick<User, 'name' | 'email'>> creates a type with optional name and email properties from User. For conditional types, explore Extract and Exclude for filtering union types, and Parameters/ConstructorParameters for extracting function and constructor types.
TypeScript with React

TypeScript and React are a powerful combination. TypeScript provides type safety for props, state, hooks, events, and refs. React's type definitions (@types/react) include comprehensive types for every React API. With TypeScript, many React bugs — passing wrong props, accessing state incorrectly, mishandling event types — are caught at compile time.

typescript-react.tsx
TSX
1// Typed React component (function component)
2import React, { useState, useEffect, useCallback } from 'react';
3
4interface ButtonProps {
5 label: string;
6 variant?: 'primary' | 'secondary' | 'danger';
7 disabled?: boolean;
8 onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
9 children?: React.ReactNode;
10}
11
12const Button: React.FC<ButtonProps> = ({
13 label,
14 variant = 'primary',
15 disabled = false,
16 onClick,
17 children,
18}) => {
19 return (
20 <button
21 className={variant}
22 disabled={disabled}
23 onClick={onClick}
24 >
25 {label}
26 {children}
27 </button>
28 );
29};
30
31// Typed useState
32const [count, setCount] = useState<number>(0);
33const [user, setUser] = useState<User | null>(null);
34const [items, setItems] = useState<string[]>([]);
35
36// Typed useEffect
37useEffect(() => {
38 const controller = new AbortController();
39 fetch('/api/data', { signal: controller.signal })
40 .then(res => res.json())
41 .then((data: User[]) => setUser(data[0]));
42 return () => controller.abort();
43}, []);
44
45// Typed event handlers
46const handleChange = useCallback(
47 (e: React.ChangeEvent<HTMLInputElement>) => {
48 console.log(e.target.value);
49 },
50 []
51);
52
53const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
54 e.preventDefault();
55 // handle form
56};
57
58// Typed refs
59const inputRef = useRef<HTMLInputElement>(null);
60const divRef = useRef<HTMLDivElement>(null);
61
62// useReducer with typed actions
63type Action =
64 | { type: 'increment'; amount: number }
65 | { type: 'decrement'; amount: number }
66 | { type: 'reset' };
67
68interface CounterState {
69 count: number;
70}
71
72function counterReducer(state: CounterState, action: Action): CounterState {
73 switch (action.type) {
74 case 'increment':
75 return { count: state.count + action.amount };
76 case 'decrement':
77 return { count: state.count - action.amount };
78 case 'reset':
79 return { count: 0 };
80 }
81}
82
83const [state, dispatch] = useReducer(counterReducer, { count: 0 });

info

For React with TypeScript, always prefer interface for props and state shapes (they have better performance and clearer error messages). Use React.FC sparingly — it implicitly includes children, which is often not what you want. Consider typing children?: React.ReactNode explicitly in your props interface instead.
Migration Guide

Migrating an existing JavaScript project to TypeScript should be incremental. TypeScript is designed for gradual adoption — you can start with a single file and expand. The key is setting up the right configuration, enabling strict checking gradually, and using any and // @ts-ignore as temporary bridges while you add types.

StepActionConfig
1Install TypeScriptnpm i -D typescript @types/node
2Create tsconfig.jsonnpx tsc --init
3Rename .js to .tsStart with utility files and helpers
4Enable allowJsallowJs: true
5Add types graduallyUse JSDoc for .js, strict for .ts
6Enable strict modestrict: true
migration-config.ts
TypeScript
1// tsconfig.json — recommended starter config
2{
3 "compilerOptions": {
4 "target": "ES2020",
5 "module": "ESNext",
6 "moduleResolution": "bundler",
7 "lib": ["ES2020", "DOM", "DOM.Iterable"],
8 "jsx": "react-jsx",
9 "strict": true,
10 "noUncheckedIndexedAccess": true,
11 "noImplicitOverride": true,
12 "allowJs": true, // allow .js files alongside .ts
13 "checkJs": false, // set to true to check .js files too
14 "declaration": true, // generate .d.ts files
15 "declarationMap": true,
16 "sourceMap": true,
17 "outDir": "./dist",
18 "rootDir": "./src",
19 "skipLibCheck": true // faster compilation
20 },
21 "include": ["src/**/*"],
22 "exclude": ["node_modules", "dist"]
23}
24
25// JSDoc — add types to plain JavaScript before migration
26/**
27 * @param {string} name
28 * @param {number} age
29 * @returns {{ name: string; age: number; isAdult: boolean }}
30 */
31function createPerson(name, age) {
32 return { name, age, isAdult: age >= 18 };
33}
34
35// // @ts-expect-error — suppress expected errors during migration
36// @ts-expect-error — better than @ts-ignore
37// Use when TypeScript flags a line that you intend to fix later
38const oldApiResult = legacyFunction(); // @ts-expect-error
39
40// Incremental strictness — start lenient, tighten over time
41// 1. noImplicitAny: false → true
42// 2. strictNullChecks: false → true
43// 3. strict: true (enables all strict flags)

best practice

The safest migration strategy: enable allowJs: true so TypeScript and JavaScript coexist, rename files to .ts incrementally starting with leaf modules (no dependencies), add types to the most stable parts of your codebase first (shared utilities, API clients), and only enable strict: true once all files pass type checking. Use // @ts-expect-error for expected errors during migration — it forces you to address them when the signature changes.
Best Practices
Enable strict mode from the start — strictNullChecks catches the most runtime errors
Prefer interfaces for public APIs and object shapes; use types for unions and computed types
Avoid any — use unknown if you truly don't know the type, then narrow it with type guards
Use discriminated unions (tagged unions) for state machines and complex async states
Leverage const assertions (as const) for literal types and readonly arrays
Use satisfies operator to type-check inferred types without widening them (TS 4.9+)
Prefer generics over casting — generics maintain type relationships, casting destroys them
Use branded types for type-safe IDs and domain primitives
Write isolated type tests with expectTypeOf (vitest) to verify complex type logic
Keep your tsconfig strict but don't over-engineer types — practical safety wins over perfection
🔥

pro tip

TypeScript's most underused feature: branded types. Create type-safe wrappers for primitive values that represent domain concepts: type UserId = string & { readonly __brand: "UserId" }. This prevents accidentally passing a raw string where a UserId is expected. Combined with zod or io-ts for runtime validation, branded types create a robust boundary between your system and external data.
$Blueprint — Engineering Documentation·Section ID: JS-TYPESCRIPT·Revision: 1.0