1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17export interface SerperSitelink {
18 title: string;
19 url: string;
20}
21
22export interface SerperOrganic {
23 title: string;
24 url: string;
25 description: string;
26 position: number | null;
27 sitelinks?: SerperSitelink[];
28 attributes?: Record<string, string>;
29 date?: string;
30}
31
32export interface SerperKnowledgeGraph {
33 title?: string;
34 type?: string;
35 website?: string;
36 image_url?: string;
37 description?: string;
38 description_source?: string;
39 description_link?: string;
40 attributes?: Record<string, string>;
41}
42
43export interface SerperAnswerBox {
44 answer?: string;
45 title?: string;
46 url?: string;
47 snippet?: string;
48 snippet_highlighted?: string[];
49}
50
51export interface SerperPeopleAlsoAsk {
52 question: string;
53 snippet?: string;
54 title?: string;
55 url?: string;
56}
57
58export interface SerperShoppingItem {
59 title?: string;
60 source?: string;
61 url?: string;
62 price?: string;
63 delivery?: string;
64 image_url?: string;
65 rating?: number;
66 rating_count?: number;
67 offers?: string;
68 product_id?: string;
69 position?: number;
70}
71
72export interface SerperMapped {
73 organic: SerperOrganic[];
74 ads: unknown[];
75 people_also_ask: SerperPeopleAlsoAsk[];
76 related_searches: string[];
77 knowledge_graph: SerperKnowledgeGraph | null;
78 answer_box: SerperAnswerBox | null;
79 shopping: SerperShoppingItem[];
80 ai_overview: unknown | null;
81}
82
83function str(v: unknown): string {
84 return typeof v === "string" ? v : "";
85}
86
87
88export function mapSerperResponse(raw: any): SerperMapped {
89 const organic: SerperOrganic[] = Array.isArray(raw?.organic)
90 ? raw.organic.map((r: Record<string, unknown>) => ({
91 title: str(r?.title),
92 url: str(r?.link),
93 description: str(r?.snippet),
94 position: typeof r?.position === "number" ? r.position : null,
95 ...(Array.isArray(r?.sitelinks)
96 ? {
97 sitelinks: (r.sitelinks as Record<string, unknown>[]).map((s) => ({
98 title: str(s?.title),
99 url: str(s?.link),
100 })),
101 }
102 : {}),
103 ...(r?.attributes && typeof r.attributes === "object" ? { attributes: r.attributes as Record<string, string> } : {}),
104 ...(r?.date ? { date: str(r.date) } : {}),
105 }))
106 : [];
107
108 const people_also_ask: SerperPeopleAlsoAsk[] = Array.isArray(raw?.peopleAlsoAsk)
109 ? raw.peopleAlsoAsk.map((p: Record<string, unknown>) => ({
110 question: str(p?.question),
111 ...(p?.snippet ? { snippet: str(p.snippet) } : {}),
112 ...(p?.title ? { title: str(p.title) } : {}),
113 ...(p?.link ? { url: str(p.link) } : {}),
114 }))
115 : [];
116
117 const related_searches: string[] = Array.isArray(raw?.relatedSearches)
118 ? raw.relatedSearches.map((r: Record<string, unknown>) => str(r?.query)).filter(Boolean)
119 : [];
120
121 const kg = raw?.knowledgeGraph as Record<string, unknown> | undefined;
122 const knowledge_graph: SerperKnowledgeGraph | null = kg
123 ? {
124 ...(kg.title ? { title: str(kg.title) } : {}),
125 ...(kg.type ? { type: str(kg.type) } : {}),
126 ...(kg.website ? { website: str(kg.website) } : {}),
127 ...(kg.imageUrl ? { image_url: str(kg.imageUrl) } : {}),
128 ...(kg.description ? { description: str(kg.description) } : {}),
129 ...(kg.descriptionSource ? { description_source: str(kg.descriptionSource) } : {}),
130 ...(kg.descriptionLink ? { description_link: str(kg.descriptionLink) } : {}),
131 ...(kg.attributes && typeof kg.attributes === "object" ? { attributes: kg.attributes as Record<string, string> } : {}),
132 }
133 : null;
134
135 const ab = raw?.answerBox as Record<string, unknown> | undefined;
136 const answer_box: SerperAnswerBox | null = ab
137 ? {
138 ...(ab.answer ? { answer: str(ab.answer) } : {}),
139 ...(ab.title ? { title: str(ab.title) } : {}),
140 ...(ab.link ? { url: str(ab.link) } : {}),
141 ...(ab.snippet ? { snippet: str(ab.snippet) } : {}),
142 ...(Array.isArray(ab.snippetHighlighted) ? { snippet_highlighted: ab.snippetHighlighted as string[] } : {}),
143 }
144 : null;
145
146 const shopping: SerperShoppingItem[] = Array.isArray(raw?.shopping)
147 ? raw.shopping.map((s: Record<string, unknown>) => ({
148 ...(s?.title ? { title: str(s.title) } : {}),
149 ...(s?.source ? { source: str(s.source) } : {}),
150 ...(s?.link ? { url: str(s.link) } : {}),
151 ...(s?.price ? { price: str(s.price) } : {}),
152 ...(s?.delivery ? { delivery: str(s.delivery) } : {}),
153 ...(s?.imageUrl ? { image_url: str(s.imageUrl) } : {}),
154 ...(typeof s?.rating === "number" ? { rating: s.rating } : {}),
155 ...(typeof s?.ratingCount === "number" ? { rating_count: s.ratingCount } : {}),
156 ...(s?.offers ? { offers: str(s.offers) } : {}),
157 ...(s?.productId ? { product_id: str(s.productId) } : {}),
158 ...(typeof s?.position === "number" ? { position: s.position } : {}),
159 }))
160 : [];
161
162 return {
163 organic,
164 ads: Array.isArray(raw?.ads) ? raw.ads : [],
165 people_also_ask,
166 related_searches,
167 knowledge_graph,
168 answer_box,
169 shopping,
170 ai_overview: raw?.aiOverview ?? null,
171 };
172}