Loading Thucde.dev
Preparing the next view.
@googlemaps/places Node.js client library recently, you may have noticed it’s not keeping up with the latest Google Places API features—particularly the Text Search endpoint. While it’s convenient to rely on official libraries, sometimes they lag behind the actual REST API (Web Service). That’s exactly what happened to me.
"Generate TypeScript classes for the Google Places Text Search (New) response."

import axios, { AxiosInstance, AxiosError } from "axios"; import dotenv from "dotenv"; dotenv.config(); // Load environment variables // --- Common Interfaces --- /** * Represents a geographical point using latitude and longitude. */ interface LatLng { latitude: number; longitude: number; } /** * Defines a circle for location bias or restriction. */ interface LocationCircle { center: LatLng; radius: number; // in meters } /** * Defines a rectangle for location bias or restriction. */ interface LocationRectangle { low: LatLng; high: LatLng; } /** * Specifies a geographical area for biasing or restricting search results. */ interface LocationBias { circle?: LocationCircle; rectangle?: LocationRectangle; } /** * Specifies a geographical area to which search results are restricted. */ interface LocationRestriction { circle?: LocationCircle; rectangle?: LocationRectangle; } /** * Defines possible price levels for a place. */ enum PriceLevel { PRICE_LEVEL_UNSPECIFIED = "PRICE_LEVEL_UNSPECIFIED", PRICE_LEVEL_FREE = "PRICE_LEVEL_FREE", PRICE_LEVEL_INEXPENSIVE = "PRICE_LEVEL_INEXPENSIVE", PRICE_LEVEL_MODERATE = "PRICE_LEVEL_MODERATE", PRICE_LEVEL_EXPENSIVE = "PRICE_LEVEL_EXPENSIVE", PRICE_LEVEL_VERY_EXPENSIVE = "PRICE_LEVEL_VERY_EXPENSIVE", } /** * Defines preference for ranking results. */ type RankPreference = "RELEVANCE" | "DISTANCE" | "RANK_PREFERENCE_UNSPECIFIED"; // --- Request and Response Interfaces --- /** * Represents the request body for a Text Search (New) API call. * This interface defines the parameters that can be sent to Google's Places API * to search for places based on a text query. * For more details, refer to the official Google Places API documentation: * @see https://developers.google.com/maps/documentation/places/web-service/search-text-new */ export interface TextSearchRequestBody { /** * The text string on which to search, for example: "restaurant", "123 Main Street", * or "best place to visit in San Francisco". The API returns candidate matches * based on this string and orders the results based on their perceived relevance. * This field is required. */ textQuery: string; /** * The language code, indicating the language in which to return results. * If `languageCode` is not supplied, the API attempts to use the preferred language * as specified in the `Accept-Language` header, or the service's default language. * Results may be in a mixed language if the query is not in the specified language. * See: https://developers.google.com/maps/faq#languagesupport * @example 'en' (English), 'vi' (Vietnamese) */ languageCode?: string; /** * A bias for the search results toward a specified geographical region. * This field can be used to influence the results to be closer to a certain point * (e.g., a city center) without strictly limiting them to that area. * Can be either a circle (center point and radius) or a rectangle (bounding box). */ locationBias?: LocationBias; // Assumes LocationBias is defined elsewhere /** * A restriction for the search results to a specified geographical region. * This field strictly limits the results to places within the defined area. * If a `locationRestriction` is set, only results within that region will be returned. * Can be either a circle (center point and radius) or a rectangle (bounding box). */ locationRestriction?: LocationRestriction; // Assumes LocationRestriction is defined elsewhere /** * The maximum number of results to return. * The API returns up to 20 results by default. You can set this to any value * from 1 to 20. If you request more than 20 results, the API will return 20 results. * This field is new in Text Search (New). */ maxResultCount?: number; /** * Restricts the results to places that have a user rating of at least this value. * The value must be a floating-point number between 0.0 and 5.0 (inclusive). */ minRating?: number; /** * If set to `true`, only returns places that are open now (or at the time specified * by `rankPreference=DISTANCE` which implies future time). * Default is `false`. */ openNow?: boolean; /** * Restricts the results to places that are at one of the specified price levels. * Multiple price levels can be specified in an array. * @example [PriceLevel.PRICE_LEVEL_MODERATE, PriceLevel.PRICE_LEVEL_EXPENSIVE] */ priceLevels?: PriceLevel[]; // Assumes PriceLevel enum is defined elsewhere /** * Specifies the ranking method to use when returning results. * - `RELEVANCE`: (Default) Ranks results by their relevance to the query. * - `DISTANCE`: Ranks results by distance from the `locationBias` origin. * If `rankPreference` is `DISTANCE`, a `locationBias` must be set. */ rankPreference?: RankPreference; // Assumes RankPreference type is defined elsewhere /** * The Unicode country/region code (CLDR) of the region where the request comes from. * This can affect the display of results (e.g., road names in a specific regional variant). * @example 'vn' for Vietnam, 'us' for United States * @see https://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html */ regionCode?: string; /** * If set to `true`, the search results will be restricted to places that belong to * the type(s) specified by `includedType` or `includedPrimaryTypes`. * If no types are specified, this field has no effect. */ strictTypeFiltering?: boolean; /** * Restricts the results to places matching the specified type. * Only one type may be specified. For example: `"includedType":"bar"`. * The value should be from [Table A](https://developers.google.com/maps/documentation/places/web-service/supported-types#table-a). * Note: The values in Table B are only returned in the response; they cannot be used as a filter. */ includedType?: string; /** * If set to `true`, the search includes pure service area businesses in the results. * A pure service area business is a business that does not have a physical customer-facing location. * Defaults to `false`. */ includePureServiceAreaBusinesses?: boolean; /** * The number of results to return per page. * The default is 20, and the maximum is 20. This field is for pagination if the API * ever returns a `nextPageToken` for Text Search. * Note: As of current documentation, Text Search (New) does not explicitly return `nextPageToken`. * This field might be more relevant for other Places API methods like Nearby Search (New). */ pageSize?: number; /** * A token that is returned by a previous search request to get the next page of results. * Use this token in a subsequent request to retrieve additional results when `maxResultCount` * limits the number of results in the first response. * Note: As of current documentation, Text Search (New) does not explicitly return `nextPageToken`. * This field might be more relevant for other Places API methods like Nearby Search (New). */ pageToken?: string; } // Define placeholder interfaces for complex types if they are not yet fully defined // You should expand these based on the detailed Google Places API documentation interface AccessibilityOptions { wheelchairAccessibleEntrance?: boolean; // ... other accessibility options } interface AddressComponent { longText?: string; shortText?: string; types?: string[]; } interface PlaceDisplayName { text: string; languageCode: string; } interface GoogleMapsLink { uri: string; // ... other link properties } interface Photo { name: string; // Resource name for the photo widthPx: number; heightPx: number; authorAttributions: { displayName: string; uri?: string; photoUri?: string; }[]; } interface PlusCode { globalCode?: string; compoundCode?: string; } interface PostalAddress { regionCode?: string; languageCode?: string; postalCode?: string; sortingCode?: string; administrativeArea?: string; locality?: string; sublocality?: string; addressLines?: string[]; recipients?: string[]; organization?: string; } interface PrimaryTypeDisplayName { text: string; languageCode: string; } interface Viewport { low: LatLng; high: LatLng; } interface OpeningHoursPeriod { open: { day: number; // Day of the week (0 for Sunday, 1 for Monday, etc.) time: string; // Time in HHMM format }; close?: { day: number; time: string; }; } interface OpeningHours { openNow: boolean; periods: OpeningHoursPeriod[]; weekdayDescriptions: string[]; // e.g., "Monday: 9:00 AM – 5:00 PM" } interface PriceRange { minPrice: string; maxPrice: string; } interface EditorialSummary { text: string; languageCode?: string; } interface EvChargeAmenitySummary { // Define properties based on EV charging specifics } interface EvChargeOptions { // Define properties based on EV charging specifics } interface FuelOptions { // Define properties based on fuel specifics } interface GenerativeSummary { description: string; // ... other generative summary properties } interface NeighborhoodSummary { // Define properties based on neighborhood summary } interface ParkingOptions { // Define properties based on parking specifics } interface PaymentOptions { acceptsCreditCards?: boolean; acceptsDebitCards?: boolean; acceptsCash?: boolean; acceptsNfc?: boolean; acceptsQrCode?: boolean; } interface Review { name?: string; // Resource name of the review authorAttribution: { displayName: string; uri?: string; photoUri?: string; }; rating: number; // 1-5 star rating text: { text: string; languageCode?: string; }; publishTime: string; // ISO 8601 timestamp originalText?: { text: string; languageCode?: string; }; } interface ReviewSummary { // Define properties based on review summary specifics } interface RoutingSummary { // Define properties based on routing summary specifics } /** * Represents a single Place object returned in the Text Search (New) response. * The fields included in this object depend on the 'X-Goog-FieldMask' specified in the request. * Requesting more fields incurs higher costs. * For a comprehensive list of all possible fields and their detailed types, * refer to the official Google Places API documentation: * @see https://developers.google.com/maps/documentation/places/web-service/search-text#request-body0 */ export interface Place { // ----------------------------------------------------------- // The following fields trigger the Text Search Essentials ID Only SKU: // ----------------------------------------------------------- /** * Contains a list of attributions for this place that must be displayed to the user. * For example, this may include the name of the data provider. */ attributions?: string[]; /** * The unique identifier of the place, used to retrieve more details about the place. * This is a stable identifier for a place across Google's services. */ id: string; // Make this required as it's typically always returned /** * The resource name of the place in the format: `places/PLACE_ID`. * This is primarily used for internal API calls. */ name?: string; // ----------------------------------------------------------- // The following fields trigger the Text Search Pro SKU: // ----------------------------------------------------------- /** * Provides information about the accessibility options of the place. * @remarks Define a more specific type for `AccessibilityOptions` based on documentation. */ accessibilityOptions?: AccessibilityOptions; /** * A breakdown of the place's address into its individual components. * @remarks Define a more specific type for `AddressComponent` based on documentation. */ addressComponents?: AddressComponent[]; /** * A short descriptor of the address. Generally available for customers in India and are experimental elsewhere. */ addressDescriptor?: string; /** * The adr address for the place, formatted according to the HTML standard. * This address is suitable for display to the user. */ adrFormatAddress?: string; /** * The operational status of the business, such as 'OPERATIONAL', 'CLOSED_PERMANENTLY', etc. */ businessStatus?: string; /** * A list of places that are contained by this place (e.g., a shop within a mall). * @remarks Define a more specific type for `ContainingPlaces` based on documentation. */ containingPlaces?: any; // Placeholder for more specific type /** * The localized display name of the place. This is the name suitable for displaying to the user. */ displayName: PlaceDisplayName; /** * The full, human-readable address of the place, formatted according to * the local country's formatting rules. */ formattedAddress?: string; /** * A list of links to Google Maps. Pre-GA Preview. * @remarks Define a more specific type for `GoogleMapsLink` based on documentation. */ googleMapsLinks?: GoogleMapsLink[]; /** * The URI for the place on Google Maps. This link is useful for deep linking to the place on Google Maps. */ googleMapsUri?: string; /** * The background color for the icon associated with this place, in a 6-digit hexadecimal format. */ iconBackgroundColor?: string; /** * The base URI for the icon associated with this place. Concatenate this with a size and color * to create a full icon URL. */ iconMaskBaseUri?: string; /** * The geographical coordinates (latitude and longitude) of the place. */ location?: LatLng; /** * An array of `Photo` objects representing images of the place. * Each photo object provides details for fetching the image. * @remarks Define a more specific type for `Photo` based on documentation. */ photos?: Photo[]; /** * The plus code for the place, which is a short geocode that provides a simple * way to refer to a location. * @remarks Define a more specific type for `PlusCode` based on documentation. */ plusCode?: PlusCode; /** * The postal address for the place, structured into its components. * @remarks Define a more specific type for `PostalAddress` based on documentation. */ postalAddress?: PostalAddress; /** * The primary type of the place (e.g., "restaurant", "cafe"). * Values are from Table A. */ primaryType?: string; /** * The localized display name of the primary type. * @remarks Define a more specific type for `PrimaryTypeDisplayName` based on documentation. */ primaryTypeDisplayName?: PrimaryTypeDisplayName; /** * Indicates if the place is a pure service area business, meaning it does not * have a physical customer-facing location. */ pureServiceAreaBusiness?: boolean; /** * The short, formatted address of the place. */ shortFormattedAddress?: string; /** * A list of sub-destinations (e.g., individual stores within a shopping mall). * @remarks Define a more specific type for `SubDestinations` based on documentation. */ subDestinations?: any; // Placeholder for more specific type /** * An array of types for the place, indicating its classification (e.g., "cafe", "point_of_interest"). * Values are from Table A and Table B. */ types?: string[]; /** * The offset in minutes from UTC for the place's timezone. */ utcOffsetMinutes?: number; /** * The rectangular viewport that encompasses the place. * @remarks Define a more specific type for `Viewport` based on documentation. */ viewport?: Viewport; // ----------------------------------------------------------- // The following fields trigger the Text Search Enterprise SKU: // ----------------------------------------------------------- /** * The current opening hours of the place, taking into account public holidays and temporary closures. * @remarks Define a more specific type for `OpeningHours` based on documentation. */ currentOpeningHours?: OpeningHours; /** * The current secondary opening hours of the place, for specific services or departments. * @remarks Define a more specific type for `OpeningHours` based on documentation. */ currentSecondaryOpeningHours?: OpeningHours; /** * The international phone number for the place, formatted in the E.164 standard. * Includes the country code. */ internationalPhoneNumber?: string; /** * The national phone number for the place, formatted according to local conventions. * Does not include the country code. */ nationalPhoneNumber?: string; /** * The price level of the place, indicating its general price range. * @remarks Assumes `PriceLevel` enum is defined elsewhere. */ priceLevel?: PriceLevel; /** * The price range of the place, if available. * @remarks Define a more specific type for `PriceRange` based on documentation. */ priceRange?: PriceRange; /** * The average user rating of the place, on a scale of 1.0 to 5.0. */ rating?: number; /** * The regular opening hours of the place, excluding public holidays and temporary closures. * @remarks Define a more specific type for `OpeningHours` based on documentation. */ regularOpeningHours?: OpeningHours; /** * The regular secondary opening hours of the place. * @remarks Define a more specific type for `OpeningHours` based on documentation. */ regularSecondaryOpeningHours?: OpeningHours; /** * The total number of user ratings for the place. */ userRatingCount?: number; /** * The URI of the place's official website. */ websiteUri?: string; // ----------------------------------------------------------- // The following fields trigger the Text Search Enterprise + Atmosphere SKU: // ----------------------------------------------------------- /** * Indicates if dogs are allowed at the place. */ allowsDogs?: boolean; /** * Indicates if curbside pickup is available at the place. */ curbsidePickup?: boolean; /** * Indicates if delivery services are available from the place. */ delivery?: boolean; /** * Indicates if dine-in service is available at the place. */ dineIn?: boolean; /** * A summary of editorial content about the place. * @remarks Define a more specific type for `EditorialSummary` based on documentation. */ editorialSummary?: EditorialSummary; /** * Summary of EV charging amenities available. * @remarks Define a more specific type for `EvChargeAmenitySummary` based on documentation. */ evChargeAmenitySummary?: EvChargeAmenitySummary; /** * Options for electric vehicle charging at the place. * @remarks Define a more specific type for `EvChargeOptions` based on documentation. */ evChargeOptions?: EvChargeOptions; /** * Fuel options available at the place (e.g., for gas stations). * @remarks Define a more specific type for `FuelOptions` based on documentation. */ fuelOptions?: FuelOptions; /** * A summary generated by AI about the place's attributes. * @remarks Define a more specific type for `GenerativeSummary` based on documentation. */ generativeSummary?: GenerativeSummary; /** * Indicates if the place is suitable for children. */ goodForChildren?: boolean; /** * Indicates if the place is good for groups. */ goodForGroups?: boolean; /** * Indicates if the place is good for watching sports. */ goodForWatchingSports?: boolean; /** * Indicates if live music is featured at the place. */ liveMusic?: boolean; /** * Indicates if the place offers a menu specifically for children. */ menuForChildren?: boolean; /** * Summary information about the neighborhood the place is located in. * @remarks Define a more specific type for `NeighborhoodSummary` based on documentation. */ neighborhoodSummary?: NeighborhoodSummary; /** * Information about parking options available at the place. * @remarks Define a more specific type for `ParkingOptions` based on documentation. */ parkingOptions?: ParkingOptions; /** * Information about payment options accepted at the place (e.g., credit cards, cash). * @remarks Define a more specific type for `PaymentOptions` based on documentation. */ paymentOptions?: PaymentOptions; /** * Indicates if outdoor seating is available at the place. */ outdoorSeating?: boolean; /** * Indicates if reservations are possible at the place. */ reservable?: boolean; /** * Indicates if a restroom is available at the place. */ restroom?: boolean; /** * An array of user reviews for the place. * @remarks Define a more specific type for `Review` based on documentation. */ reviews?: Review[]; /** * A summary of the reviews for the place. * @remarks Define a more specific type for `ReviewSummary` based on documentation. */ reviewSummary?: ReviewSummary; /** * Summaries related to routing to the place (Text Search and Nearby Search only). * @remarks Define a more specific type for `RoutingSummaries` based on documentation. */ routingSummaries?: RoutingSummary[]; /** * Indicates if the place serves beer. */ servesBeer?: boolean; /** * Indicates if the place serves breakfast. */ servesBreakfast?: boolean; /** * Indicates if the place serves brunch. */ servesBrunch?: boolean; /** * Indicates if the place serves cocktails. */ servesCocktails?: boolean; /** * Indicates if the place serves coffee. */ servesCoffee?: boolean; /** * Indicates if the place serves dessert. */ servesDessert?: boolean; /** * Indicates if the place serves dinner. */ servesDinner?: boolean; /** * Indicates if the place serves lunch. */ servesLunch?: boolean; /** * Indicates if the place serves vegetarian food. */ servesVegetarianFood?: boolean; /** * Indicates if the place serves wine. */ servesWine?: boolean; /** * Indicates if takeout service is available from the place. */ takeout?: boolean; // Note: This is not an exhaustive list of all possible fields. // You should refer to the complete documentation for the full list of fields // and their corresponding data types, especially for the complex nested objects // (e.g., `accessibilityOptions`, `addressComponents`, `photos`, `reviews`, etc.). } /** * Represents the response object for a Text Search (New) API call. */ export interface TextSearchResponse { places: Place[]; } // --- Main Service Class --- /** * A union type representing the valid field names for the Text Search (New) API FieldMask. * This provides type suggestions but does not strictly enforce the comma-separated format. */ type PlaceField = | 'places.attributions' | 'places.id' | 'places.name' | 'places.accessibilityOptions' | 'places.addressComponents' | 'places.addressDescriptor' | 'places.adrFormatAddress' | 'places.businessStatus' | 'places.containingPlaces' | 'places.displayName' | 'places.formattedAddress' | 'places.googleMapsLinks' | 'places.googleMapsUri' | 'places.iconBackgroundColor' | 'places.iconMaskBaseUri' | 'places.location' | 'places.photos' | 'places.plusCode' | 'places.postalAddress' | 'places.primaryType' | 'places.primaryTypeDisplayName' | 'places.pureServiceAreaBusiness' | 'places.shortFormattedAddress' | 'places.subDestinations' | 'places.types' | 'places.utcOffsetMinutes' | 'places.viewport' | 'places.currentOpeningHours' | 'places.currentSecondaryOpeningHours' | 'places.internationalPhoneNumber' | 'places.nationalPhoneNumber' | 'places.priceLevel' | 'places.priceRange' | 'places.rating' | 'places.regularOpeningHours' | 'places.regularSecondaryOpeningHours' | 'places.userRatingCount' | 'places.websiteUri' | 'places.allowsDogs' | 'places.curbsidePickup' | 'places.delivery' | 'places.dineIn' | 'places.editorialSummary' | 'places.evChargeAmenitySummary' | 'places.evChargeOptions' | 'places.fuelOptions' | 'places.generativeSummary' | 'places.goodForChildren' | 'places.goodForGroups' | 'places.goodForWatchingSports' | 'places.liveMusic' | 'places.menuForChildren' | 'places.neighborhoodSummary' | 'places.parkingOptions' | 'places.paymentOptions' | 'places.outdoorSeating' | 'places.reservable' | 'places.restroom' | 'places.reviews' | 'places.reviewSummary' | 'places.routingSummaries' | 'places.servesBeer' | 'places.servesBreakfast' | 'places.servesBrunch' | 'places.servesCocktails' | 'places.servesCoffee' | 'places.servesDessert' | 'places.servesDinner' | 'places.servesLunch' | 'places.servesVegetarianFood' | 'places.servesWine' | 'places.takeout' | '*'; // Include the wildcard option // could be a string comma-separated list of PlaceField values type FieldMaskArray = PlaceField[]; export type FieldMask = FieldMaskArray | string /** * A class to interact with the Google Places Text Search (New) API using Axios. */ export class PlacesTextSearchService { private apiKey: string; private readonly baseUrl: string = "https://places.googleapis.com/v1/places:searchText"; private axiosInstance: AxiosInstance; /** * @param apiKey Your Google Cloud API Key with access to the Places API. * If not provided, it will attempt to load from GOOGLE_PLACES_API_KEY environment variable. */ constructor(apiKey?: string) { this.apiKey = apiKey || process.env.GOOGLE_PLACES_API_KEY || ""; if (!this.apiKey) { throw new Error( "Google Places API Key is not provided. Set it in the constructor or GOOGLE_PLACES_API_KEY environment variable." ); } this.axiosInstance = axios.create({ baseURL: this.baseUrl, headers: { "Content-Type": "application/json", "X-Goog-Api-Key": this.apiKey, }, timeout: 15000, // 15 seconds timeout for API requests }); } /** * Performs a Text Search (New) request. * @param request The TextSearchRequestBody object containing the search parameters. * @param fieldMask A comma-separated string of fields to return in the response * (e.g., "places.displayName,places.formattedAddress,places.location,places.rating"). * **Crucially, specify only the fields you need to manage SKU costs.** * @returns A Promise that resolves with the TextSearchResponse. * @throws An error if the API request fails. */ public async searchText( request: TextSearchRequestBody, fieldMask: FieldMask ): Promise<TextSearchResponse> { if (!fieldMask) { throw new Error( 'FieldMask is required for Text Search (New) requests. Example: "places.displayName"' ); } try { const response = await this.axiosInstance.post<TextSearchResponse>( "", request, { headers: { "X-Goog-FieldMask": fieldMask, // FieldMask is set per request }, } ); return response.data; } catch (error) { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; console.error(`Google Places Text Search Error: ${axiosError.message}`); console.error(`Status: ${axiosError.response?.status}`); console.error( `Response Data: ${JSON.stringify(axiosError.response?.data, null, 2)}` ); } else { console.error( "An unexpected error occurred during Text Search (New) API request:", error ); } throw error; // Re-throw the error for the caller to handle } } /** * Helper method to create a LocationBias or LocationRestriction circle object. * @param latitude Latitude of the circle center. * @param longitude Longitude of the circle center. * @param radius Radius of the circle in meters. * @returns An object conforming to LocationCircle. */ public static createCircle( latitude: number, longitude: number, radius: number ): LocationCircle { return { center: { latitude, longitude }, radius: radius, }; } /** * Helper method to create a LocationBias or LocationRestriction rectangle object. * @param lowLat Latitude of the low corner. * @param lowLng Longitude of the low corner. * @param highLat Latitude of the high corner. * @param highLng Longitude of the high corner. * @returns An object conforming to LocationRectangle. */ public static createRectangle( lowLat: number, lowLng: number, highLat: number, highLng: number ): LocationRectangle { return { low: { latitude: lowLat, longitude: lowLng }, high: { latitude: highLat, longitude: highLng }, }; } }
Google Places Text Search (New) | TypeScript Class