libphonenumber-js: incorrect TypeScript definitions for TelephoneNumber, Extension, PhoneCode
EDIT: it turned there was serious downside with this change i proposed bellow
Problem
Currently PhoneCode (and TelephoneNumber and Extension) are defined using interface, like:
export interface PhoneCode extends String { }
meaning, that variables/fields of these types are treated as objects, not string primitives
(even tough typeof phoneCode === "string").
As a result variable of type PhoneCode can’t be assigned to variables/fields with string type:
import { getPhoneCode } from 'libphonenumber-js';
const phoneCode = getPhoneCode('EE');
const phoneCodeAsString: string = phoneCode;
// ERROR:^^^^^^^^^^^ TS2322: Type 'PhoneCode' is not assignable to type 'string'.
Expected behaviour
Should be able to asign PhoneCode (and TelephoneNumber and Extension) to ´string´.
Solution
Declare PhoneCode (and TelephoneNumber and Extension) using
export type XXX = string
instead of
export interface PhoneCode extends String { }
Just in case, I’m mentioning @RonNewcomb, because he created the initial TypeScript definitions file, and also @catamphetamine who has created Extension type by following example of @RonNewcomb
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Reactions: 2
- Comments: 21 (13 by maintainers)
Commits related to this issue
- fix #170 - declare PhoneCode (and TelephoneNumber and Extension) using `export type XXX = string` instead of `export interface PhoneCode extends String { }` — committed to atsu85/libphonenumber-js by deleted user 6 years ago
- Merge pull request #171 from atsu85/issue-170-incompatible-string-types fix #170 - declare PhoneCode (and TelephoneNumber and Extension) using type not interface — committed to catamphetamine/libphonenumber-js by catamphetamine 6 years ago
- Revert "fix #170 - declare PhoneCode (and TelephoneNumber and Extension) using `export type XXX = string` instead of `export interface PhoneCode extends String { }`" This reverts commit 42a4a6c043d4a... — committed to atsu85/libphonenumber-js by deleted user 6 years ago
- Merge pull request #172 from atsu85/issue-170-incompatible-string-types.revert Revert "fix #170 - declare PhoneCode (and TelephoneNumber and Extensi… — committed to catamphetamine/libphonenumber-js by catamphetamine 6 years ago
- fix #170 - declare PhoneCode (and TelephoneNumber and Extension) using `export type XXX = string` instead of `export interface PhoneCode extends String { }` — committed to atsu85/libphonenumber-js by deleted user 6 years ago
I would like to suggest potentially reopening this issue. Idiomatic Typescript favors just using the “string” type instead of extending “String”. Most people using Typescript will expect the simple string type. Extending String should be left to the user and not required for everyone having to use the library. In most cases, the rest of the program will expect to consume the value as a regular string type. You are making a big assumption that people need to know the difference between CountryCode and TelephoneNumber at the type level. If they do, it should be on them to enforce that.
The assignment needs a cast:
const phoneCodeAsString: string = <string>phoneCode;The problem with Typescript’s
export type XXX = stringsyntax is that the Intellisense of every IDE I’ve ever seen will show a var/property of type XXX as being of type string, not XXX. That largely defeats the purpose of typing it at all. (I believe the syntax doesn’t actually make a new type; it’s just a shortcut to save typing. Hence it doesn’t throw type mismatch errors.)The reason that requiring a cast is (IMHO) a good thing is such: If you have an Address1 type descended from string and a CustomerName type descended from string, and you assign a value of one to a property/var of the other, you want to get the compiler error that requires a cast, because they’re different types despite both being represented as string.
This problem is particularly acute with string manipulation libraries like this one. Strings in, strings out, did this string already go through the library or not, does the phone# format that that string is in match the format that this string function accepts… everything’s a damn string. You’re effectively working with untyped data since
stringis about as helpful in this context asany.Making subtypes that appear in intellisense helps a lot with simple usability, and the occasional required cast, albeit annoying, can save a you a bug hunt or two next week.
But yeah I really wish Typescript would realize that
var supertypeVar = subtypeValueis a valid no-cast-required assignment by OO rules.No one should be assigning a “national (significant) number” to an E.164 because those’re two different concepts. Ok, thx.
If we don’t ever assign a
NationalNumbervariable to anE164Numberone, then these opaque types are actually the better way, so no objections from me.@catamphetamine
Yes, my concern is with the below type definitions extending String (notice
.numberin my code which is E164Number):The Map example was meant to illustrate how extending String is different from having some types strict (the Map code itself doesn’t really represent my actual usage).
The reason the current type definition extends String is because it’s not possible to extend
stringprimitive. However, String wrapper object type andstringprimitive type are, strictly speaking, different types and do behave differently in some cases.So, if some function really expects an object and you give it a E164Number, TypeScript doesn’t complain but behaves differently from what TypeScript thinks it will. This behavior is what I think is not a good trade-off because it can be hard for users to understand why it happens.
So, my proposal is: Make the above types string and maybe add some JSDoc to help IDE users:
Users can keep using string alias names, but they will b used for cosmetic reasons and doesn’t affect TypeScript
Published
libphonenumber-js@1.9.48.Yes. String wrapper objects inherit from Object and are objects, so they are accepted in functions and variables that expect an object.
Understood. Thanks anyway for consideration and maintaining a great library!
@javascripter Thanks for the detailed explanation.
I’d probably agree that “cosmetic” typing would be a better way than strict typing if I was a TypeScript user myself. But I’m not. So it’s not for me to decide.
To clarify, any type defined as
export interface XXX extends String { }is also accepted in functions that expect an object, without raising any warnings or errors? That could be considered a bug, I guess.And you’re proposing we change all
extends String { }things to= string.I can see that comprehensive comment by @RonNewcomb above where he describes why did he choose
String {}overstring: https://github.com/catamphetamine/libphonenumber-js/issues/170#issuecomment-363156068I don’t have the qualification to make any decision there. I can see that comment having 6 upvotes from which I conclude that other people agree with that approach. And @atsu85 's original comment doesn’t seem to have any upvotes from which I could conclude that no other people actually are strongly supportive of his proposition. So, it seems logical for me to leave the current typings as is.
@catamphetamine I’m sorry. I should have described my opinion in a more relaxed and neutral way like you suggested. I edited my comment above not to offend others.
Since I only use a few functions from this library, I simply decided to re-export functions with a more strict typing.
thanks for merged PR and maintaining this library 😃
good job