Skip to content

Instantly share code, notes, and snippets.

@mmazzarolo
Created May 6, 2023 12:09
Show Gist options
  • Save mmazzarolo/20dcfeff4b12a6b2e908c74abad59cfa to your computer and use it in GitHub Desktop.
Save mmazzarolo/20dcfeff4b12a6b2e908c74abad59cfa to your computer and use it in GitHub Desktop.
WIP - Type-safe TypeScript ISO 3166 country codes (names, two-letters codes, and three-letters codes)
// ==========================================
// (WIP) TYPE-SAFE ISO 3166 COUNTRY CODES
// ==========================================
/**
* Countries data.
* This is the source that should be updated if you want to add/remove new country codes.
*/
const countriesData = [
["Afghanistan", "AF", "AFG"],
["AlandIslands", "AX", "ALA"],
["Albania", "AL", "ALB"],
["Algeria", "DZ", "DZA"],
["AmericanSamoa", "AS", "ASM"],
["Andorra", "AD", "AND"],
["Angola", "AO", "AGO"],
["Anguilla", "AI", "AIA"],
["Antarctica", "AQ", "ATA"],
["AntiguaAndBarbuda", "AG", "ATG"],
["Argentina", "AR", "ARG"],
["Armenia", "AM", "ARM"],
["Aruba", "AW", "ABW"],
["Australia", "AU", "AUS"],
["Austria", "AT", "AUT"],
["Azerbaijan", "AZ", "AZE"],
["Bahamas", "BS", "BHS"],
["Bahrain", "BH", "BHR"],
["Bangladesh", "BD", "BGD"],
["Barbados", "BB", "BRB"],
["Belarus", "BY", "BLR"],
["Belgium", "BE", "BEL"],
["Belize", "BZ", "BLZ"],
["Benin", "BJ", "BEN"],
["Bermuda", "BM", "BMU"],
["Bhutan", "BT", "BTN"],
["Bolivia", "BO", "BOL"],
["BonaireSintEustatiusSaba", "BQ", "BES"],
["BosniaAndHerzegovina", "BA", "BIH"],
["Botswana", "BW", "BWA"],
["BouvetIsland", "BV", "BVT"],
["Brazil", "BR", "BRA"],
["BritishIndianOceanTerritory", "IO", "IOT"],
["BruneiDarussalam", "BN", "BRN"],
["Bulgaria", "BG", "BGR"],
["BurkinaFaso", "BF", "BFA"],
["Burundi", "BI", "BDI"],
["Cambodia", "KH", "KHM"],
["Cameroon", "CM", "CMR"],
["Canada", "CA", "CAN"],
["CapeVerde", "CV", "CPV"],
["CaymanIslands", "KY", "CYM"],
["CentralAfricanRepublic", "CF", "CAF"],
["Chad", "TD", "TCD"],
["Chile", "CL", "CHL"],
["China", "CN", "CHN"],
["ChristmasIsland", "CX", "HKG"],
["CocosKeelingIslands", "CC", "MAC"],
["Colombia", "CO", "CXR"],
["Comoros", "KM", "CCK"],
["Congo", "CG", "COL"],
["CongoDemocraticRepublic", "CD", "COM"],
["CookIslands", "CK", "COG"],
["CostaRica", "CR", "COD"],
["CoteDIvoire", "CI", "COK"],
["Croatia", "HR", "CRI"],
["Cuba", "CU", "CIV"],
["Curaçao", "CW", "HRV"],
["Cyprus", "CY", "CUB"],
["CzechRepublic", "CZ", "CUW"],
["Denmark", "DK", "CYP"],
["Djibouti", "DJ", "CZE"],
["Dominica", "DM", "DNK"],
["DominicanRepublic", "DO", "DJI"],
["Ecuador", "EC", "DMA"],
["Egypt", "EG", "DOM"],
["ElSalvador", "SV", "ECU"],
["EquatorialGuinea", "GQ", "EGY"],
["Eritrea", "ER", "SLV"],
["Estonia", "EE", "GNQ"],
["Ethiopia", "ET", "ERI"],
["FalklandIslands", "FK", "EST"],
["FaroeIslands", "FO", "ETH"],
["Fiji", "FJ", "FLK"],
["Finland", "FI", "FRO"],
["France", "FR", "FJI"],
["FrenchGuiana", "GF", "FIN"],
["FrenchPolynesia", "PF", "FRA"],
["FrenchSouthernTerritories", "TF", "GUF"],
["Gabon", "GA", "PYF"],
["Gambia", "GM", "ATF"],
["Georgia", "GE", "GAB"],
["Germany", "DE", "GMB"],
["Ghana", "GH", "GEO"],
["Gibraltar", "GI", "DEU"],
["Greece", "GR", "GHA"],
["Greenland", "GL", "GIB"],
["Grenada", "GD", "GRC"],
["Guadeloupe", "GP", "GRL"],
["Guam", "GU", "GRD"],
["Guatemala", "GT", "GLP"],
["Guernsey", "GG", "GUM"],
["Guinea", "GN", "GTM"],
["GuineaBissau", "GW", "GGY"],
["Guyana", "GY", "GIN"],
["Haiti", "HT", "GNB"],
["HeardIslandMcdonaldIslands", "HM", "GUY"],
["HolySeeVaticanCityState", "VA", "HTI"],
["Honduras", "HN", "HMD"],
["HongKong", "HK", "VAT"],
["Hungary", "HU", "HND"],
["Iceland", "IS", "HUN"],
["India", "IN", "ISL"],
["Indonesia", "ID", "IND"],
["Iran", "IR", "IDN"],
["Iraq", "IQ", "IRN"],
["Ireland", "IE", "IRQ"],
["IsleOfMan", "IM", "IRL"],
["Israel", "IL", "IMN"],
["Italy", "IT", "ISR"],
["Jamaica", "JM", "ITA"],
["Japan", "JP", "JAM"],
["Jersey", "JE", "JPN"],
["Jordan", "JO", "JEY"],
["Kazakhstan", "KZ", "JOR"],
["Kenya", "KE", "KAZ"],
["Kiribati", "KI", "KEN"],
["Korea", "KR", "KIR"],
["KoreaDemocraticPeoplesRepublic", "KP", "PRK"],
["Kuwait", "KW", "KOR"],
["Kyrgyzstan", "KG", "KWT"],
["LaoPeoplesDemocraticRepublic", "LA", "KGZ"],
["Latvia", "LV", "LAO"],
["Lebanon", "LB", "LVA"],
["Lesotho", "LS", "LBN"],
["Liberia", "LR", "LSO"],
["LibyanArabJamahiriya", "LY", "LBR"],
["Liechtenstein", "LI", "LBY"],
["Lithuania", "LT", "LIE"],
["Luxembourg", "LU", "LTU"],
["Macao", "MO", "LUX"],
["Macedonia", "MK", "MKD"],
["Madagascar", "MG", "MDG"],
["Malawi", "MW", "MWI"],
["Malaysia", "MY", "MYS"],
["Maldives", "MV", "MDV"],
["Mali", "ML", "MLI"],
["Malta", "MT", "MLT"],
["MarshallIslands", "MH", "MHL"],
["Martinique", "MQ", "MTQ"],
["Mauritania", "MR", "MRT"],
["Mauritius", "MU", "MUS"],
["Mayotte", "YT", "MYT"],
["Mexico", "MX", "MEX"],
["Micronesia", "FM", "FSM"],
["Moldova", "MD", "MDA"],
["Monaco", "MC", "MCO"],
["Mongolia", "MN", "MNG"],
["Montenegro", "ME", "MNE"],
["Montserrat", "MS", "MSR"],
["Morocco", "MA", "MAR"],
["Mozambique", "MZ", "MOZ"],
["Myanmar", "MM", "MMR"],
["Namibia", "NA", "NAM"],
["Nauru", "NR", "NRU"],
["Nepal", "NP", "NPL"],
["Netherlands", "NL", "NLD"],
["NewCaledonia", "NC", "ANT"],
["NewZealand", "NZ", "NCL"],
["Nicaragua", "NI", "NZL"],
["Niger", "NE", "NIC"],
["Nigeria", "NG", "NER"],
["Niue", "NU", "NGA"],
["NorfolkIsland", "NF", "NIU"],
["NorthernMarianaIslands", "MP", "NFK"],
["Norway", "NO", "MNP"],
["Oman", "OM", "NOR"],
["Pakistan", "PK", "OMN"],
["Palau", "PW", "PAK"],
["PalestinianTerritory", "PS", "PLW"],
["Panama", "PA", "PSE"],
["PapuaNewGuinea", "PG", "PAN"],
["Paraguay", "PY", "PNG"],
["Peru", "PE", "PRY"],
["Philippines", "PH", "PER"],
["Pitcairn", "PN", "PHL"],
["Poland", "PL", "PCN"],
["Portugal", "PT", "POL"],
["PuertoRico", "PR", "PRT"],
["Qatar", "QA", "PRI"],
["Reunion", "RE", "QAT"],
["Romania", "RO", "REU"],
["RussianFederation", "RU", "ROU"],
["Rwanda", "RW", "RUS"],
["SaintBarthelemy", "BL", "RWA"],
["SaintHelena", "SH", "BLM"],
["SaintKittsAndNevis", "KN", "SHN"],
["SaintLucia", "LC", "KNA"],
["SaintMartin", "MF", "LCA"],
["SaintPierreAndMiquelon", "PM", "MAF"],
["SaintVincentAndGrenadines", "VC", "SPM"],
["Samoa", "WS", "VCT"],
["SanMarino", "SM", "WSM"],
["SaoTomeAndPrincipe", "ST", "SMR"],
["SaudiArabia", "SA", "STP"],
["Senegal", "SN", "SAU"],
["Serbia", "RS", "SEN"],
["Seychelles", "SC", "SRB"],
["SierraLeone", "SL", "SYC"],
["Singapore", "SG", "SLE"],
["SintMaarten", "SX", "SGP"],
["Slovakia", "SK", "SXM"],
["Slovenia", "SI", "SVK"],
["SolomonIslands", "SB", "SVN"],
["Somalia", "SO", "SLB"],
["SouthAfrica", "ZA", "SOM"],
["SouthGeorgiaAndSandwichIsl", "GS", "ZAF"],
["SouthSudan", "SS", "SGS"],
["Spain", "ES", "SSD"],
["SriLanka", "LK", "ESP"],
["Sudan", "SD", "LKA"],
["Suriname", "SR", "SDN"],
["SvalbardAndJanMayen", "SJ", "SUR"],
["Swaziland", "SZ", "SJM"],
["Sweden", "SE", "SWZ"],
["Switzerland", "CH", "SWE"],
["SyrianArabRepublic", "SY", "CHE"],
["Taiwan", "TW", "SYR"],
["Tajikistan", "TJ", "TWN"],
["Tanzania", "TZ", "TJK"],
["Thailand", "TH", "TZA"],
["TimorLeste", "TL", "THA"],
["Togo", "TG", "TLS"],
["Tokelau", "TK", "TGO"],
["Tonga", "TO", "TKL"],
["TrinidadAndTobago", "TT", "TON"],
["Tunisia", "TN", "TTO"],
["Turkey", "TR", "TUN"],
["Turkmenistan", "TM", "TUR"],
["TurksAndCaicosIslands", "TC", "TKM"],
["Tuvalu", "TV", "TCA"],
["Uganda", "UG", "TUV"],
["Ukraine", "UA", "UGA"],
["UnitedArabEmirates", "AE", "UKR"],
["UnitedKingdom", "GB", "ARE"],
["UnitedStates", "US", "GBR"],
["UnitedStatesOutlyingIslands", "UM", "USA"],
["Uruguay", "UY", "UMI"],
["Uzbekistan", "UZ", "URY"],
["Vanuatu", "VU", "UZB"],
["Venezuela", "VE", "VUT"],
["Vietnam", "VN", "VEN"],
["VirginIslandsBritish", "VG", "VNM"],
["VirginIslandsUS", "VI", "VIR"],
["WallisAndFutuna", "WF", "WLF"],
["WesternSahara", "EH", "ESH"],
["Yemen", "YE", "YEM"],
["Zambia", "ZM", "ZMB"],
["Zimbabwe", "ZW", "ZWE"],
] as const;
/**
* Utility type to prettify the type displayed on IDE by unwrapping it.
*/
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
/**
* Utility. Build a const object (a pseudo Enum) given a read-only array.
* We use it to expose country names and codes allowing autocomplete.
*/
const constArrayToConstObject = <T extends readonly any[], K extends T[number]>(
arr: T
): Prettify<Readonly<{ [P in K]: P }>> => {
return arr.reduce((acc, elem) => {
acc[elem] = elem;
return acc;
}, {});
};
/**
* Extract a list & type union of country names, two-letter codes, and three-letter codes.
*/
const countryNames = countriesData.map((data) => data[0]);
type CountryName = (typeof countryNames)[number];
const twoLetterCountryCodes = countriesData.map((data) => data[1]);
type TwoLetterCountryCode = (typeof twoLetterCountryCodes)[number];
const threeLetterCountryCodes = countriesData.map((data) => data[2]);
type ThreeLetterCountryCode = (typeof threeLetterCountryCodes)[number];
/**
* Country data.
*/
interface Country {
name: CountryName;
twoLetterCode: TwoLetterCountryCode;
threeLetterCode: ThreeLetterCountryCode;
}
/**
* Build support maps to make the country retrival O(1).
* TODO: Lazy load them when they're requested for the first time.
*/
const countryByName = new Map<CountryName, Country>();
const countryByTwoLetterCode = new Map<TwoLetterCountryCode, Country>();
const countryByThreeLetterCode = new Map<ThreeLetterCountryCode, Country>();
Array.from({ length: countriesData.length }).forEach((_, i) => {
const name = countryNames[i];
const twoLetterCode = twoLetterCountryCodes[i];
const threeLetterCode = threeLetterCountryCodes[i];
const country: Country = { name, twoLetterCode, threeLetterCode };
countryByName.set(name, country);
countryByTwoLetterCode.set(twoLetterCode, country);
countryByThreeLetterCode.set(threeLetterCode, country);
});
/**
* Any country name, two-letter code, or three-letter code is a valid input.
*/
type CountryInput = CountryName | TwoLetterCountryCode | ThreeLetterCountryCode;
/**
* Type guards to distinguish between the various input types.
* Luckily, all country names are > 2 characters long, so we can can use the input lenght as our discriminating factor.
*/
function isCountryName(input: CountryInput): input is CountryName {
return input.length > 2;
}
function isTwoLetterCountryCode(input: CountryInput): input is TwoLetterCountryCode {
return input.length === 2;
}
function isThreeLetterCountryCode(input: CountryInput): input is ThreeLetterCountryCode {
return input.length === 3;
}
/**
* Returns a country data given any of its identifiers (name, two-letter code, or three-letter code).
* @param input A country name, two-letter code, or three-letter code.
* @returns The country data.
*/
export function getCountry(input: CountryInput): Country {
if (isCountryName(input)) {
return countryByName.get(input)!;
} else if (isTwoLetterCountryCode(input)) {
return countryByTwoLetterCode.get(input)!;
} else if (isThreeLetterCountryCode(input)) {
return countryByThreeLetterCode.get(input)!;
}
throw new Error(`getCountry() - Invalid input: "${input}"`);
}
/**
* Returns a three-letter country-code given any of its identifiers (name, two-letter code, or three-letter code).
* @param input A country name, two-letter code, or three-letter code.
* @returns The three-letter country-code.
*/
export function getThreeLetterCountryCode(
input: CountryInput | CountryInput[]
): ThreeLetterCountryCode | ThreeLetterCountryCode[] {
return Array.isArray(input) ? input.map((x) => getCountry(x).threeLetterCode) : getCountry(input).threeLetterCode;
}
/**
* An enum-like object with all the country names — helpful for IDE autocomplete.
*/
export const countryName = constArrayToConstObject(countryNames);
/**
* An enum-like object with all the country two-letter codes — helpful for IDE autocomplete.
*/
export const twoLetterCountryCode = constArrayToConstObject(twoLetterCountryCodes);
/**
* An enum-like object with all the country three-letter codes — helpful for IDE autocomplete.
*/
export const threeLetterCountryCode = constArrayToConstObject(threeLetterCountryCodes);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment