Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to

### Added

- Add basic TypeScript coverage for engine names and core search parameters.
- Expose `EngineName` type.
- Expose `EngineParameters` type.
- Expose `InvalidArgumentError` error.

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ const response = await getJson({
console.log(response);
```

### TypeScript

The library exposes basic TypeScript types for engine names and core search
parameters. These types validate the `engine` value while still allowing
engine-specific query parameters that are not modeled yet.

```ts
import { type EngineParameters, getJson } from "serpapi";

const parameters: EngineParameters = {
engine: "google_light",
api_key: API_KEY,
q: "coffee",
timeout: 60000,
};

const response = await getJson(parameters);
console.log(response.search_metadata);
```

## Features

- TypeScript support.
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export {
export type {
AccountApiParameters,
BaseResponse,
EngineName,
EngineParameters,
GetBySearchIdParameters,
LocationsApiParameters,
Expand Down
21 changes: 11 additions & 10 deletions src/serpapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InvalidArgumentError } from "./errors.ts";
import {
AccountApiParameters,
BaseResponse,
EngineName,
EngineParameters,
GetBySearchIdParameters,
LocationsApiParameters,
Expand Down Expand Up @@ -45,23 +46,23 @@ export function getJson(
* getJson("google", { api_key: API_KEY, q: "coffee" }, console.log);
*/
export function getJson(
engine: string,
parameters: EngineParameters,
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (json: BaseResponse) => void,
): Promise<BaseResponse>;

export function getJson(
...args:
| [parameters: EngineParameters, callback?: (json: BaseResponse) => void]
| [
engine: string,
parameters: EngineParameters,
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (json: BaseResponse) => void,
]
): Promise<BaseResponse> {
if (typeof args[0] === "string" && typeof args[1] === "object") {
const [engine, parameters, callback] = args;
const newParameters = { ...parameters, engine } as EngineParameters;
const newParameters = { ...parameters, engine };
return _getJson(newParameters, callback);
} else if (
typeof args[0] === "object" &&
Expand Down Expand Up @@ -126,23 +127,23 @@ export function getHtml(
* getHtml({ engine: "google", api_key: API_KEY, q: "coffee" }, console.log);
*/
export function getHtml(
engine: string,
parameters: EngineParameters,
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (html: string) => void,
): Promise<string>;

export function getHtml(
...args:
| [parameters: EngineParameters, callback?: (html: string) => void]
| [
engine: string,
parameters: EngineParameters,
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (html: string) => void,
]
): Promise<string> {
if (typeof args[0] === "string" && typeof args[1] === "object") {
const [engine, parameters, callback] = args;
const newParameters = { ...parameters, engine } as EngineParameters;
const newParameters = { ...parameters, engine };
return _getHtml(newParameters, callback);
} else if (
typeof args[0] === "object" &&
Expand Down
131 changes: 129 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,132 @@
// deno-lint-ignore no-explicit-any
export type EngineParameters = Record<string, any>;
import type http from "node:http";

// Keep this list at the engine-name level. Per-engine parameter schemas are
// intentionally out of scope for this minimal TypeScript coverage layer.
export type EngineName =
| "amazon"
| "amazon_product"
| "apple_maps"
| "apple_maps_places"
| "baidu"
| "baidu_news"
| "bing"
| "bing_copilot"
| "bing_images"
| "bing_maps"
| "bing_news"
| "bing_product"
| "bing_reverse_image"
| "bing_shopping"
| "bing_videos"
| "brave_ai_mode"
| "duckduckgo"
| "duckduckgo_light"
| "duckduckgo_maps"
| "duckduckgo_news"
| "ebay"
| "ebay_product"
| "facebook_profile"
| "google"
| "google_about_this_result"
| "google_ads"
| "google_ads_transparency_center"
| "google_ads_transparency_center_ad_details"
| "google_ai_mode"
| "google_ai_overview"
| "google_autocomplete"
| "google_events"
| "google_finance"
| "google_finance_markets"
| "google_flights"
| "google_flights_autocomplete"
| "google_flights_deals"
| "google_forums"
| "google_hotels"
| "google_hotels_autocomplete"
| "google_hotels_photos"
| "google_hotels_reviews"
| "google_images"
| "google_images_light"
| "google_images_related_content"
| "google_immersive_product"
| "google_jobs"
| "google_jobs_listing"
| "google_lens"
| "google_light"
| "google_local"
| "google_local_services"
| "google_maps"
| "google_maps_autocomplete"
| "google_maps_contributor_reviews"
| "google_maps_directions"
| "google_maps_photo_meta"
| "google_maps_photos"
| "google_maps_posts"
| "google_maps_reviews"
| "google_news"
| "google_news_light"
| "google_patents"
| "google_patents_details"
| "google_play"
| "google_play_product"
| "google_related_questions"
| "google_scholar"
| "google_scholar_author"
| "google_scholar_case_law"
| "google_scholar_cite"
| "google_scholar_profiles"
| "google_shopping"
| "google_shopping_filters"
| "google_shopping_light"
| "google_short_videos"
| "google_travel_explore"
| "google_trends"
| "google_videos"
| "google_videos_light"
| "home_depot"
| "home_depot_product"
| "instagram_profile"
| "naver"
| "naver_ai_overview"
| "open_table_reviews"
| "tripadvisor"
| "tripadvisor_place"
| "tripadvisor_reviews"
| "walmart"
| "walmart_product"
| "walmart_product_reviews"
| "walmart_product_sellers"
| "yahoo"
| "yahoo_images"
| "yahoo_shopping"
| "yahoo_videos"
| "yandex"
| "yandex_images"
| "yandex_videos"
| "yelp"
| "yelp_place"
| "yelp_reviews"
| "youtube"
| "youtube_video"
| "youtube_video_transcript";

type BaseParameters = {
api_key?: string | null;
async?: boolean;
device?: "desktop" | "tablet" | "mobile";
no_cache?: boolean;
output?: "json" | "html";
q?: string;
requestOptions?: http.RequestOptions;
timeout?: number;
zero_trace?: boolean;
};

export type EngineParameters<EngineRequired = true> =
& (EngineRequired extends true ? { engine: EngineName }
: { engine?: EngineName })
& BaseParameters
& Record<string, unknown>;

// deno-lint-ignore no-explicit-any
export type BaseResponse = Record<string, any>;
Expand Down
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function getSource() {

export function buildRequestOptions(
path: string,
parameters: qs.ParsedUrlQueryInput,
parameters: Record<string, unknown>,
): http.RequestOptions {
const clonedParams = { ...parameters };
for (const k in clonedParams) {
Expand All @@ -58,7 +58,7 @@ export function buildRequestOptions(
}
const basicOptions = {
..._internals.getHostnameAndPort(),
path: `${path}?${qs.stringify(clonedParams)}`,
path: `${path}?${qs.stringify(clonedParams as qs.ParsedUrlQueryInput)}`,
method: "GET",
};

Expand All @@ -71,7 +71,7 @@ export function buildRequestOptions(

export function execute(
path: string,
parameters: qs.ParsedUrlQueryInput,
parameters: Record<string, unknown>,
timeout: number,
): Promise<string> {
const options = buildRequestOptions(path, {
Expand Down
78 changes: 78 additions & 0 deletions tests/types_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it } from "https://deno.land/std@0.170.0/testing/bdd.ts";
import { assertEquals } from "https://deno.land/std@0.170.0/testing/asserts.ts";
import { getHtml, getJson } from "../mod.ts";
import type { BaseResponse, EngineName, EngineParameters } from "../mod.ts";

function expectType<T>(value: T): T {
return value;
}

describe("types", () => {
it("allows basic typed searches", () => {
const engine = expectType<EngineName>("google_light");
const parameters = expectType<EngineParameters>({
engine,
api_key: "api_key",
q: "coffee",
location: "Austin, Texas",
async: true,
no_cache: true,
timeout: 1000,
requestOptions: {
headers: {
"User-Agent": "serpapi-types-test",
},
},
});

assertEquals(parameters.engine, "google_light");
});

it("exposes typed overloads for core search functions", () => {
const typedGetJson = expectType<{
(
parameters: EngineParameters,
callback?: (json: BaseResponse) => void,
): Promise<BaseResponse>;
(
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (json: BaseResponse) => void,
): Promise<BaseResponse>;
}>(getJson);

const typedGetHtml = expectType<{
(
parameters: EngineParameters,
callback?: (html: string) => void,
): Promise<string>;
(
engine: EngineName,
parameters: EngineParameters<false>,
callback?: (html: string) => void,
): Promise<string>;
}>(getHtml);

assertEquals(typeof typedGetJson, "function");
assertEquals(typeof typedGetHtml, "function");
});

it("rejects invalid engine names and core parameter types", () => {
// @ts-expect-error "gogle" is not a supported engine name.
const invalidEngine: EngineName = "gogle";
const invalidParameters: EngineParameters = {
// @ts-expect-error object form requires a valid engine name.
engine: "gogle",
q: "coffee",
};
// @ts-expect-error object form requires an engine parameter.
const missingEngine: EngineParameters = { q: "coffee" };
// @ts-expect-error timeout must be a number.
const badTimeout: EngineParameters = { engine: "google", timeout: "1000" };

assertEquals(
[invalidEngine, invalidParameters, missingEngine, badTimeout].length,
4,
);
});
});