New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
incorrect TypeScript definitions for TelephoneNumber, Extension, PhoneCode #170
Comments
good job |
fix #170 - declare PhoneCode (and TelephoneNumber and Extension) using type not interface
thanks for merged PR and maintaining this library :) |
The assignment needs a cast: The problem with Typescript's 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 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 |
…er and Extension) using `export type XXX = string` instead of `export interface PhoneCode extends String { }`" This reverts commit 42a4a6c.
@RonNewcomb, good point - it is a matter of trade-offs. According to my change it was possible to just document the type, allowing to assign it to string without casting, but the downside is that it won't prevent me from assigning one kind of string to different kind of string either. I prepared a PR that reverts my previous change. |
@catamphetamine, sorry for the inconvenience - please merge my PR that reverts my previous change related to this issue |
…es.revert Revert "fix #170 - declare PhoneCode (and TelephoneNumber and Extensi…
Released |
@atsu85 Thx for trying |
…xtension) using `export type XXX = string` instead of `export interface PhoneCode extends String { }`
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. |
I'm happy to make the changes if we can agree |
As String objects aren't exactly the same as string primitives, treating it as such could introduce subtle unexpected type-unsafe behaviors to user programs potentially in the form of a runtime error which TypeScript could otherwise have detected. For example, the following compiles because number is typed as subtype of String object but throws in runtime: const number = parsePhoneNumber(phoneNumber).number
const m = new WeakMap()
m.set(number, '1') // a runtime error because WeakMap#set only accepts objects and not primitives It's possible users are not aware of this subtle type unsafeness introduced by this type definition because as far as I know it's not a common practice to use String object subtype as nominal types. I think this issue could be reopened for more discussion. |
@catamphetamine Since I only use a few functions from this library, I simply decided to re-export functions with a more strict typing. import parsePhoneNumber_, { CountryCode } from 'libphonenumber-js'
export const parsePhoneNumber = parsePhoneNumber_ as unknown as (
text: string,
defaultCountry?:
| CountryCode
| {
defaultCountry?: CountryCode
defaultCallingCode?: string
extract?: boolean
}
) => string | undefined |
@javascripter Oh sh*t, you're Japanese. I forgot Japanese are obsessively polite. Well, yeah, as long as the solution works for you. export function parsePhoneNumberFromString(
text: string,
defaultCountry?: CountryCode | {
defaultCountry?: CountryCode,
defaultCallingCode?: string,
extract?: boolean
}
): PhoneNumber | undefined; It seems like you're attempting to use the returned value as a key in a Lines 58 to 75 in 303f0f3
|
Yes, my concern is with the below type definitions extending String (notice
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 const s: String = new String('a') // valid
const s: string = new String('a') // invalid, Type 'String' is not assignable to type 'string'.
const stringObj = new String('a')
stringObj instanceof Object // true
stringObj instanceof String // ture
'a' instanceof Object // false
'a' instanceof String // false 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: export type E164Number = string // keep these types for compatibility and for internal code clarity
export class PhoneNumber {
/** an E164 number **/ // add some JSDoc comments to help IDE users
number: E164Number;
} Users can keep using string alias names, but they will b used for cosmetic reasons and doesn't affect TypeScript const phoneNumber: E164Number // this will be the same as const number: string |
@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 And you're proposing we change all I can see that comprehensive comment by @RonNewcomb above where he describes why did he choose I don't have the qualification to make any decision there. |
Yes. String wrapper objects inherit from Object and are objects, so they are accepted in functions and variables that expect an object. interface Str extends String {}
function fn(value: object) {
}
const s:Str = 'a' as Str
fn(s) // no error
fn('a') // Type 'string' is not assignable to type 'object'.ts(2322)
const s1: object = s // no error
const s2: object = 'a' // Type 'string' is not assignable to type 'object'.ts(2322)
Understood. Thanks anyway for consideration and maintaining a great library! |
Is it possible to use The variables |
FYI: I'm about to merge this new pull request that replaces the declaration for all "basic string" types like Was: export interface E164Number extends String { }
export interface NationalNumber extends String { }
export interface Extension extends String { }
export interface CarrierCode extends String { }
export interface CountryCallingCode extends String { } Will become: type Tagged<A, T> = A & { __tag: T };
type E164Number = Tagged<string, "E164Number">;
type NationalNumber = Tagged<string, "NationalNumber">;
type Extension = Tagged<string, "Extension">;
type CarrierCode = Tagged<string, "CarrierCode">;
type CountryCallingCode = Tagged<string, "CountryCallingCode">; If there're any objections then post them here. |
If we don't ever assign a |
No one should be assigning a "national (significant) number" to an E.164 because those're two different concepts. |
Published |
@catamphetamine, cool workaround! type Tagged<A, T> = A & { __tag: T }; better option would be this: type Tagged<A, T> = A & { __tag?: T }; as former doesn't give TS compiler error when trying to assign var e164: E164Number = "e164" as any
var tag: string = nationalNumber.__tag but latter would, as expeted, because |
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:
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 @RonNewcombThe text was updated successfully, but these errors were encountered: