diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5d4432db..656b09aa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,16 +11,45 @@ on:
jobs:
test:
runs-on: ubuntu-24.04
+ strategy:
+ matrix:
+ node-version: ['18', '20', '22']
steps:
- name: Checkout code
uses: actions/checkout@v3
- - name: Set up Node.js
+ - name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
- node-version: '22'
+ node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- name: Run lint
run: npm run lint
- name: Run tests
- run: npm run test
\ No newline at end of file
+ run: npm run test
+
+ typescript:
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '22'
+ - name: Install dependencies
+ run: npm install
+ - name: Validate TypeScript declarations
+ run: npm run validate-types
+ - name: Type check TypeScript example
+ run: npm run type-check
+ - name: Test TypeScript example compilation and execution
+ run: |
+ npx tsc example.ts
+ node example.js
+ rm example.js
+ - name: Verify TypeScript can consume the package
+ run: |
+ echo "import osrmTextInstructions = require('./index'); const compiler = osrmTextInstructions('v5'); console.log('TypeScript import works!');" > test-import.ts
+ npx tsc --noEmit test-import.ts
+ rm test-import.ts
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51c326ce..769e9bf5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
# Change Log
All notable changes to this project will be documented in this file. For change log formatting, see http://keepachangelog.com/
+## Unreleased
+- Add TypeScript support. [#321](https://github.com/Project-OSRM/osrm-text-instructions/pull/321)
+
## 0.15.0 2024-03-03
- This package now requires Node 16 and above. [#312](https://github.com/Project-OSRM/osrm-text-instructions/pull/312)
diff --git a/README.md b/README.md
index 7008df02..13a94cca 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ OSRM Text Instructions is a Node.js library that transforms route data generated
* **Customizable**: Flexible options allow you to format and tweak the results to your liking.
* **Cross-platform**: A data-driven approach facilitates implementations in other programming languages. OSRM Text Instructions is also available [in Swift and Objective-C](https://github.com/Project-OSRM/osrm-text-instructions.swift/) (for iOS, macOS, tvOS, and watchOS) and [in Java](https://github.com/Project-OSRM/osrm-text-instructions.java/) (for Android and Java SE).
* **Well-tested**: A data-driven test suite ensures compatibility across languages and platforms.
+* **TypeScript Support**: Full TypeScript type definitions are included for enhanced development experience.
## Usage
@@ -24,6 +25,32 @@ response.legs.forEach(function(leg) {
});
```
+### TypeScript Usage
+
+```typescript
+import osrmTextInstructions = require('osrm-text-instructions');
+
+const compiler = osrmTextInstructions('v5');
+
+const step: osrmTextInstructions.RouteStep = {
+ maneuver: {
+ type: 'turn',
+ modifier: 'left'
+ },
+ name: 'Main Street'
+};
+
+const options: osrmTextInstructions.CompileOptions = {
+ legCount: 2,
+ legIndex: 0,
+ formatToken: (token: string, value: string) => {
+ return token === 'way_name' ? `${value}` : value;
+ }
+};
+
+const instruction = compiler.compile('en', step, options);
+```
+
If you are unsure if the user's locale is supported by osrm-text-inustrctions, use [@mapbox/locale-utils](https://github.com/mapbox/locale-utils) for finding the best fitting language.
#### Parameters `require('osrm-text-instructions')(version)`
diff --git a/example.ts b/example.ts
new file mode 100644
index 00000000..fb702884
--- /dev/null
+++ b/example.ts
@@ -0,0 +1,107 @@
+// Example usage of osrm-text-instructions with TypeScript
+
+import osrmTextInstructions = require('./index');
+
+// Initialize the compiler for OSRM v5
+const compiler = osrmTextInstructions('v5');
+
+// Example route step data
+const step: osrmTextInstructions.RouteStep = {
+ maneuver: {
+ type: 'turn',
+ modifier: 'left',
+ bearing_after: 90
+ },
+ name: 'Main Street',
+ ref: 'A1',
+ mode: 'driving',
+ intersections: [
+ {
+ lanes: [
+ { valid: false },
+ { valid: true }
+ ]
+ }
+ ]
+};
+
+// Compile options
+const options: osrmTextInstructions.CompileOptions = {
+ legCount: 2,
+ legIndex: 0,
+ classes: ['primary'],
+ formatToken: (token: string, value: string) => {
+ if (token === 'way_name') {
+ return `${value}`;
+ }
+ return value;
+ }
+};
+
+try {
+ // Generate instruction
+ const instruction = compiler.compile('en', step, options);
+ console.log('Instruction:', instruction);
+
+ // Get way name
+ const wayName = compiler.getWayName('en', step, options);
+ console.log('Way name:', wayName);
+
+ // Get direction from degree
+ const direction = compiler.directionFromDegree('en', 90);
+ console.log('Direction:', direction);
+
+ // Ordinalize number
+ const ordinal = compiler.ordinalize('en', 1);
+ console.log('Ordinal:', ordinal);
+
+ // Generate lane configuration
+ const laneConfig = compiler.laneConfig(step);
+ console.log('Lane config:', laneConfig);
+
+ // Capitalize first letter
+ const capitalized = compiler.capitalizeFirstLetter('en', 'hello world');
+ console.log('Capitalized:', capitalized);
+
+ // Access abbreviations
+ const abbreviations = compiler.abbreviations;
+ console.log('Available abbreviations:', Object.keys(abbreviations));
+
+} catch (error) {
+ console.error('Error:', error);
+}
+
+// Example with different maneuver types
+const departStep: osrmTextInstructions.RouteStep = {
+ maneuver: {
+ type: 'depart',
+ bearing_after: 0
+ },
+ name: 'Start Street'
+};
+
+const arriveStep: osrmTextInstructions.RouteStep = {
+ maneuver: {
+ type: 'arrive',
+ modifier: 'right'
+ },
+ name: 'Destination Avenue'
+};
+
+const roundaboutStep: osrmTextInstructions.RouteStep = {
+ maneuver: {
+ type: 'roundabout',
+ modifier: 'right',
+ exit: 2
+ },
+ rotary_name: 'Main Roundabout'
+};
+
+// Compile different instruction types
+const departInstruction = compiler.compile('en', departStep);
+const arriveInstruction = compiler.compile('en', arriveStep, { waypointName: 'Home' });
+const roundaboutInstruction = compiler.compile('en', roundaboutStep);
+
+console.log('Depart:', departInstruction);
+console.log('Arrive:', arriveInstruction);
+console.log('Roundabout:', roundaboutInstruction);
\ No newline at end of file
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 00000000..be4349ba
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,318 @@
+// Type definitions for osrm-text-instructions
+// Project: https://github.com/Project-OSRM/osrm-text-instructions
+// Definitions by: Generated
+
+export = osrmTextInstructions;
+
+declare function osrmTextInstructions(version: string): osrmTextInstructions.OSRMTextInstructions;
+
+declare namespace osrmTextInstructions {
+ /**
+ * Main interface for OSRM Text Instructions compiler
+ */
+ interface OSRMTextInstructions {
+ /**
+ * Capitalizes the first letter of a string according to language rules
+ * @param language - Language code (e.g., 'en', 'fr', 'de')
+ * @param string - String to capitalize
+ * @returns Capitalized string
+ */
+ capitalizeFirstLetter(language: string, string: string): string;
+
+ /**
+ * Converts a number to its ordinalized form in the given language
+ * @param language - Language code
+ * @param number - Number to ordinalize
+ * @returns Ordinalized string (e.g., "1st", "2nd", "3rd")
+ */
+ ordinalize(language: string, number: number): string;
+
+ /**
+ * Converts degrees to compass direction in the given language
+ * @param language - Language code
+ * @param degree - Bearing in degrees (0-360)
+ * @returns Compass direction string
+ */
+ directionFromDegree(language: string, degree: number): string;
+
+ /**
+ * Generates a lane configuration string from step data
+ * @param step - Route step with intersection data
+ * @returns Lane configuration string (e.g., "xo", "ox")
+ */
+ laneConfig(step: RouteStep): string;
+
+ /**
+ * Extracts and formats way name from step data
+ * @param language - Language code
+ * @param step - Route step data
+ * @param options - Formatting options
+ * @returns Formatted way name
+ */
+ getWayName(language: string, step: RouteStep, options?: CompileOptions): string;
+
+ /**
+ * Compiles a localized text instruction from a route step
+ * @param language - Language code
+ * @param step - Route step including maneuver property
+ * @param options - Additional compilation options
+ * @returns Localized text instruction
+ */
+ compile(language: string, step: RouteStep, options?: CompileOptions): string;
+
+ /**
+ * Applies grammar rules to a name based on language-specific rules
+ * @param language - Language code
+ * @param name - Name to grammaticalize
+ * @param grammar - Grammar rule identifier
+ * @returns Grammaticalized name
+ */
+ grammarize(language: string, name: string, grammar?: string): string;
+
+ /**
+ * Tokenizes an instruction string by replacing tokens with values
+ * @param language - Language code
+ * @param instruction - Instruction template with tokens
+ * @param tokens - Token values to replace
+ * @param options - Tokenization options
+ * @returns Processed instruction string
+ */
+ tokenize(language: string, instruction: string, tokens: TokenValues, options?: CompileOptions): string;
+
+ /**
+ * Available abbreviations for all supported languages
+ */
+ abbreviations: LanguageAbbreviations;
+ }
+
+ /**
+ * Options for compiling instructions
+ */
+ interface CompileOptions {
+ /**
+ * Number of legs in the route
+ */
+ legCount?: number;
+
+ /**
+ * Zero-based index of the leg containing the step
+ */
+ legIndex?: number;
+
+ /**
+ * List of road classes (e.g., ['motorway'])
+ */
+ classes?: string[];
+
+ /**
+ * Custom name for the leg's destination
+ */
+ waypointName?: string;
+
+ /**
+ * Function to format token values before insertion
+ * @param token - Token type (e.g., 'way_name', 'direction')
+ * @param value - Token value after grammaticalization
+ * @returns Formatted token value
+ */
+ formatToken?: (token: string, value: string) => string;
+ }
+
+ /**
+ * Route step object as defined by OSRM
+ */
+ interface RouteStep {
+ /**
+ * Maneuver instruction
+ */
+ maneuver: StepManeuver;
+
+ /**
+ * Travel mode (e.g., 'driving', 'walking')
+ */
+ mode?: string;
+
+ /**
+ * Driving side ('left' or 'right')
+ */
+ driving_side?: 'left' | 'right';
+
+ /**
+ * Way name
+ */
+ name?: string;
+
+ /**
+ * Way reference
+ */
+ ref?: string;
+
+ /**
+ * Destinations (semicolon-separated)
+ */
+ destinations?: string;
+
+ /**
+ * Exits (semicolon-separated)
+ */
+ exits?: string;
+
+ /**
+ * Rotary/roundabout name
+ */
+ rotary_name?: string;
+
+ /**
+ * Junction name
+ */
+ junction_name?: string;
+
+ /**
+ * Intersections array
+ */
+ intersections?: Intersection[];
+ }
+
+ /**
+ * Step maneuver information
+ */
+ interface StepManeuver {
+ /**
+ * Maneuver type
+ */
+ type: ManeuverType;
+
+ /**
+ * Maneuver modifier
+ */
+ modifier?: ManeuverModifier;
+
+ /**
+ * Exit number for roundabouts
+ */
+ exit?: number;
+
+ /**
+ * Bearing after the maneuver in degrees
+ */
+ bearing_after?: number;
+ }
+
+ /**
+ * Intersection with lane information
+ */
+ interface Intersection {
+ /**
+ * Lane information
+ */
+ lanes?: Lane[];
+ }
+
+ /**
+ * Individual lane information
+ */
+ interface Lane {
+ /**
+ * Whether the lane is valid for the maneuver
+ */
+ valid: boolean;
+ }
+
+ /**
+ * Token values for instruction templating
+ */
+ interface TokenValues {
+ way_name?: string;
+ destination?: string;
+ exit?: string;
+ exit_number?: string;
+ rotary_name?: string;
+ lane_instruction?: string;
+ modifier?: string;
+ direction?: string;
+ nth?: string;
+ waypoint_name?: string;
+ junction_name?: string;
+ [key: string]: string | undefined;
+ }
+
+ /**
+ * Supported maneuver types
+ */
+ type ManeuverType =
+ | 'turn'
+ | 'new name'
+ | 'depart'
+ | 'arrive'
+ | 'merge'
+ | 'on ramp'
+ | 'off ramp'
+ | 'fork'
+ | 'end of road'
+ | 'continue'
+ | 'roundabout'
+ | 'rotary'
+ | 'roundabout turn'
+ | 'notification'
+ | 'exit roundabout'
+ | 'exit rotary'
+ | 'use lane';
+
+ /**
+ * Supported maneuver modifiers
+ */
+ type ManeuverModifier =
+ | 'uturn'
+ | 'sharp right'
+ | 'right'
+ | 'slight right'
+ | 'straight'
+ | 'slight left'
+ | 'left'
+ | 'sharp left';
+
+ /**
+ * Supported language codes
+ */
+ type SupportedLanguage =
+ | 'ar'
+ | 'ca'
+ | 'da'
+ | 'de'
+ | 'en'
+ | 'eo'
+ | 'es'
+ | 'es-ES'
+ | 'fi'
+ | 'fr'
+ | 'he'
+ | 'hu'
+ | 'id'
+ | 'it'
+ | 'ja'
+ | 'ko'
+ | 'my'
+ | 'nl'
+ | 'no'
+ | 'pl'
+ | 'pt-BR'
+ | 'pt-PT'
+ | 'ro'
+ | 'ru'
+ | 'sl'
+ | 'sv'
+ | 'tr'
+ | 'uk'
+ | 'vi'
+ | 'yo'
+ | 'zh-Hans';
+
+ /**
+ * Language abbreviations mapping
+ */
+ interface LanguageAbbreviations {
+ [languageCode: string]: {
+ [term: string]: string;
+ };
+ }
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 0e713c15..730f09f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,10 +10,12 @@
"license": "BSD-2-Clause",
"devDependencies": {
"@transifex/api": "^7.1.0",
+ "@types/node": "^20.0.0",
"eslint": "^8.57.0",
"mkdirp": "^0.5.1",
"request": "^2.79.0",
- "tape": "^4.9.2"
+ "tape": "^4.9.2",
+ "typescript": "^5.0.0"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -161,6 +163,16 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@types/node": {
+ "version": "20.17.57",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
+ "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
"node_modules/@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -1754,6 +1766,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -1924,6 +1957,15 @@
"core-js": "^3.35.0"
}
},
+ "@types/node": {
+ "version": "20.17.57",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz",
+ "integrity": "sha512-f3T4y6VU4fVQDKVqJV4Uppy8c1p/sVvS3peyqxyWnzkqXFJLRU7Y1Bl7rMS1Qe9z0v4M6McY0Fp9yBsgHJUsWQ==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~6.19.2"
+ }
+ },
"@ungap/structured-clone": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -3144,6 +3186,18 @@
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true
},
+ "typescript": {
+ "version": "5.8.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "dev": true
+ },
+ "undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true
+ },
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
diff --git a/package.json b/package.json
index 55c023e2..17ecbedc 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"homepage": "http://project-osrm.org",
"version": "0.15.0",
"main": "./index.js",
+ "types": "./index.d.ts",
"license": "BSD-2-Clause",
"bugs": {
"url": "https://github.com/Project-OSRM/osrm-text-instructions.js/issues"
@@ -15,15 +16,19 @@
},
"devDependencies": {
"@transifex/api": "^7.1.0",
+ "@types/node": "^20.0.0",
"eslint": "^8.57.0",
"mkdirp": "^0.5.1",
"request": "^2.79.0",
- "tape": "^4.9.2"
+ "tape": "^4.9.2",
+ "typescript": "^5.0.0"
},
"scripts": {
"lint": "eslint *.js test/*.js scripts/*.js",
"pretest": "npm run lint",
"test": "tape test/*_test.js",
- "transifex": "node scripts/transifex.js"
+ "transifex": "node scripts/transifex.js",
+ "type-check": "tsc --noEmit example.ts",
+ "validate-types": "tsc --noEmit index.d.ts"
}
}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 00000000..b6344a72
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es2017",
+ "module": "commonjs",
+ "lib": ["es2017"],
+ "types": ["node"],
+ "declaration": true,
+ "outDir": "./dist",
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true
+ },
+ "include": [
+ "*.ts",
+ "*.d.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "test"
+ ]
+}
\ No newline at end of file