简体   繁体   中英

"Stringly typed" function vs. many redundant functions vs TypeScript's string enum

I have an object in the following structure:

const geo = {
    europe: {
      germany: ['berlin', 'hamburg', 'cologne'],
      france: ['toulouse', 'paris', 'limoges'],
      italy: ['rome', 'venice', 'genoa'],
    },
    asia: {
      india: ['mumbai', 'rajkot', 'pune'],
      china: ['shenzhen', 'beijing', 'shanghai'],
      nepal: ['kohalpur', 'ghorahi', 'hetauda'],
    },
  };

And I want to write a simple function that randomly selects a city, given continent and country. Although it seems to me that a straightforward implementation would be

const randomElement = (arr) => arr[Math.floor(Math.random() * arr.length)];
const pickCity = (continent, country) => randomElement(geo[continent][country])

// calling `pickCity()`
pickCity("europe", "france") // => we get one city at random, as requested

I nevertheless wonder whether such implementation is falling under the category of a "stringly typed" function, which is said to be a sub-optimal practice.

On the other hand, if I attempt to avoid those string parameters, then the alternative would be more verbose and not respecting the DRY principle:

const pickCityGermany = () => randomElement(geo.europe.germany)
const pickCityFrance = () => randomElement(geo.europe.france)
const pickCityItaly = () => randomElement(geo.europe.italy)
const pickCityIndia = () => randomElement(geo.asia.india)
const pickCityChina = () => randomElement(geo.asia.china)
const pickCityNepal = () => randomElement(geo.asia.nepal)

Is pickCity() indeed "stringly typed", or am I misinterpreting the concept?


UPDATE


Following @Bergie's answer below, I've realized that TypeScript 's enum is likely a proper solution to typing pickCity() 's parameters. So I took a stub at this, but see below, this is still not quite a complete solution.

I started with creating two enum s, one for continent and one for country:

// typescript
enum Continent {
    Europe = "europe",
    Asia = "asia"
}

enum Country {
    Germany = "germany",
    France = "france",
    Italy = "italy",
    India = "india",
    China = "china",
    Nepal = "nepal"
}

However, as @Bergi wrote, they are dependent on each other because the way geo data is structured. So the only way I could solve it right now is by typing any , which is certainly not a solution I want:

type Geo = Record<Continent, any> // <~-~-~-~-~ `any` is :(

const geo: Geo = {
    europe: {
      germany: ['berlin', 'hamburg', 'cologne'],
      france: ['toulouse', 'paris', 'limoges'],
      italy: ['rome', 'venice', 'genoa'],
    },
    asia: {
      india: ['mumbai', 'rajkot', 'pune'],
      china: ['shenzhen', 'beijing', 'shanghai'],
      nepal: ['kohalpur', 'ghorahi', 'hetauda'],
    },
  };

const randomElement = (arr) => arr[Math.floor(Math.random() * arr.length)];
const pickCity2 = (continent: Continent, country: Country) => randomElement(geo[continent][country])

// calling pickCity2()
console.log(pickCity2(Continent.Europe, Country.Germany)) // => one German city at random

why can't we simply untangle the dependency by changing geo ?

@Bergi asked this, because it's seems like an unneeded complication. So had we have the following geoSimpler instead, life would have been easier:

type GeoSimpler = Record<Country, string[]>;

const geoSimpler: GeoSimpler = {
  germany: ['berlin', 'hamburg', 'cologne'],
  france: ['toulouse', 'paris', 'limoges'],
  italy: ['rome', 'venice', 'genoa'],
  india: ['mumbai', 'rajkot', 'pune'],
  china: ['shenzhen', 'beijing', 'shanghai'],
  nepal: ['kohalpur', 'ghorahi', 'hetauda'],
};
const pickCity3 = (country: Country) => randomElement(geoSimpler[country])
console.log(pickCity3(Country.France)) // 👍

However!

I still want to account for data structures that could not be simplified. For example, consider geoTwinCitiesAttractions :

const geoTwinCitiesAttractions = {
  us: {
    cairo: ['U.S. Custom House', 'Cairo Public LIbrary', 'Magnolia Manor'],
    memphis: ['Graceland', 'National Civil Rights Museum', 'Beale Street'],
    saintPetersburg: ['The Dali Museum', 'Sunken Gardens', 'Chihuly Collection'],
    moscow: [],
    athens: ['Pleasant Hill Vineyards', 'Little Fish Brewing', 'Athens Farmers Market'],
  },
  greece: {
    athens: ['Acropolis of Athens', 'Parthenon', 'Mount Lycabettus'],
  },
  france: {
    paris: ['Eiffel Tower', 'Louvre Museum', 'Arc de Triomphe'],
  },
  russia: {
    saintPetersburg: ['State Hermitage Museum', 'Savior on the Spilled Blood', 'Winter Palace',
    ],
    moscow: ['Red Square', 'Bolshoi Theatre', 'Cathedral of Christ the Saviour'],
  },
  egypt: {
    cairo: ['Giza Necropolis', 'Mosque of Muhammad Ali', 'Salah Al-Din Al-Ayoubi Castle'],
    memphis: ['foo', 'bar', 'baz'],
  },
};

Here we must specify both country and city, because there are same-name cities in different countries. So how could we use enum to avoid the "stringly typed" pickAttraction() :

const pickAttraction = (country: string, city: string) => randomElement(geoTwinCitiesAttractions[country][city])

Yes, pickCity is "stringly typed" if you really consider passing arbitrary strings to it . pickCity('africa', 'sudan') fails with your current data structure. And worse, pickCity('sun', 'earth') or pickCity('major:Scholz', 'population:>10000') or pickCity('','ivqu ioqwe h') just don't make sense at all.

But it's fine if you don't pass arbitrary strings to it, if you pass only continent names to the first parameter and only state names to the first parameter. Using enum types would be more appropriate to type these parameters. However, since you're asking about JavaScript specifically, there are no enum types (nor type declarations at all), and at runtime using strings is the most viable solution if you need dynamic choice between the options. But your mental model (and also annotations and documentation) must treat these as enums, or as a set of allowed strings.

(A further problem, unrelated to the "stringly typed", is that the parameters are dependent on each other. pickCity('america', 'germany') doesn't work since Germany is not in America. Do you need the continent at all?)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM