Open iislucas opened 10 years ago
Just my thoughts on "discriminating objects" problem, how about an idea that "tagging" would be implemented within some global WeakMap object where all tag information will be stored in?
In object constructions and typecasting (when writting
And we could explicitly or implicitly to "tag" an object with it's type information.
e.g.
let obj = <Point>{x: 10, y: 10};
will be transpiled to:
`var object = ___tag({x: 10, y: 10}, '{some_namespace_information}.Point');``
any pros or cons?
what exactly is wrong with T | void? after all it's been in the official "what's new" for a while written by you know who: https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#improved-unionintersection-type-inference
On Sep 13, 2016 2:24 AM, "Ryan Cavanaugh" notifications@github.com wrote:
I think there are two different things being discussed here I think -- you can have branded types in TypeScript (using whatever property key you want since the property is not actually manifest at runtime) using intersection types or brand properties of an arbitrary property type. This is useful because you can brand things which can't actually have extra properties, such as strings and numbers.
A different problem is discriminating objects that are manifest, via some key. Here a discriminator property, usually with a string value, is well-supported.
I would call the first thing about a 4 on a 0-10 "hack" scale (anything 6 or above I would not even mention in this forum without being forced to; people who were trying to fake non-nullability with T | void scored an 8 or 9), the second thing scores a solid 0.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/202#issuecomment-246586720, or mute the thread https://github.com/notifications/unsubscribe-auth/AA5PzU_eDqpxxXi8WlZX3MnUU55_xkcHks5qpkGcgaJpZM4CPxZP .
@mhegazy wrote:
I would suggest you keep the discussion in technical terms, and keep the tone professional.
I would suggest all of us do the same, and that includes your comment and my comment replying to your comment. I am sure before you posted your slander and ban-bait, that you read that I wrote to @isiahmeadows that I didn't want to discuss this off-topic noise any more.
over the past few days you have had multiple posts on this forum that are antagonizing and disrespectful to other participants.
I am tired of arguing with you about who antagonized whom, as that just feeds a goal of politically labeling the outsider as the antagonist (a form of discrimination, but do not worry I don't try to use discrimination accusations as weapons). From now on, anyone who makes non-technical posts will receive a party hat emoticon in response as a big hooray for them focusing on personality instead of technical discussion.
This is not a request to explain your comments, or tone; this is rather a statement of how many others perceive them.
This is the last time others can bait me into replying with off-topic noise, by their slandering of my personality with off-topic noise.
This is absolutely the last time I will reply on any non-technical discussion in TypeScript's community (unless the party hat icon is removed as a feature). So have fun collecting party hats.
If you deride my personality (and/or don't like me because of my technical preferences and technical goals), that is a form of discrimination btw. (You can disagree with my technical issues without involving judgement of person and that is not discrimination) We are all different. Tolerance is a virtue. You might not like Kobe Byrant's oft-alleged "arrogant and rude" personality (and I am not agreeing I am arrogant and rude, it is just an example of what some here are accusing), but this isn't a discussion venue for adherence to some personality profile. I am also not going to comment on anyone's personality. Thanks for judging me and commenting on my personality over and over and over again (even you don't even know me). It ends here from my side. You may choose to collect party hat emoticons if you wish. It's all your choice. No one is forcing you to.
One person's perception of rude is another person's reality of being matter-of-fact and rushed. One person's perception of hate speech against groups is another person's reality of speaking about the reality of a particular demographic without referring to any specific person or specific group of persons who could be identified. One person's perception of antagonist, is another person's reality of being accused of "wasting time" once and "employing feelings" twice. Etc.. I am not going to argue this over and over.
If you are interested in participating in this forum, and have vested interest in future of the TypeScript community and JavaScript tooling in general, I would suggest you ...
If you want to ban me, then build your case and do it. I can't control what other people do. I will continue to try to do my best on technical aspects of discussion.
That doesn't mean I will refuse to adopt any criticism I think is valid or helpful to my goals. I am not closed-minded. But it also means I don't owe anyone anything in that regard. Constructive criticism is not a loan to a debt slave. Constructive criticism itself must normally be offered in an air of mutual appreciation and respect for it to be constructive.
I am not a religious man.
Judging Others 7 “Do not judge, or you too will be judged. 2 For in the same way you judge others, you will be judged, and with the measure you use, it will be measured to you.
3 “Why do you look at the speck of sawdust in your brother’s eye and pay no attention to the plank in your own eye? 4 How can you say to your brother, ‘Let me take the speck out of your eye,’ when all the time there is a plank in your own eye? 5 You hypocrite, first take the plank out of your own eye, and then you will see clearly to remove the speck from your brother’s eye.
6 “Do not give dogs what is sacred; do not throw your pearls to pigs. If you do, they may trample them under their feet, and turn and tear you to pieces.
@yortus party hats can be comprised of any roughly cone shaped object. That the receiver has to determine the intent, is part of my strategy around intent and the ban rules (Code of Conduct) I've read. It is most elegant the receiver has to determine his reward, not the issuer. I issued you one, and you can determine my intent (Hint: you didn't discuss personality and you did make a technical point).
anyone who makes non-technical posts will receive a party hat
@shelby3 O/T nit: github does not (yet) allow giving people party hats. But you can give them party poppers (U+1F389).
@spion wrote:
I was not referring to subclassing. That was only the mechanism via which I illustrated the problem with nominal types.
Afaics, the problem you are concerned with only applies to subclassing.
Typeclasses only solve the initial step where you have to convince the owner to implement your nominal interface; you no longer need to do that - you can write your own instance. However, you still have to convince the first author as well as existing library writers to adopt your typeclass i.e. write their code as follows:
function f(x:TheNominalInterface) { ... }
rather then write their functions against the original nominal type written by the first author:
function f(x: TheNominalOriginalPromise) { ... }
Anyone can implement the typeclass for any preexisting data type. There is no owner any more with typeclasses. The data type can still encapsulate (via modules and/or class) any facets it wishes to, so there is an owner in that respect, but anyone can implement a typeclass interface employing the public interface of any type. It is all about composability and we build new interfaces on top of existing ones. Afaics, the main difference from subclassing is that we are encouraged to invert the hierarchy of inheritance, such that we don't implement a fixed set of interfaces conflated together in one class, but rather implement an unbounded future number of interfaces separately for any class. We change the way we design and think about our interfaces.
And this allows us many benefits, including being able to add an interface to collection of instances and feed it to a function without having to manually rebuild the collection wrapping each instance in a new delegate wrapper shell instance (or in the case of a dynamic runtime, add properties manually to each instance...messing with the prototype
chain is more analogous to typeclasses afaics).
I don't deny that typeclasses are better and more flexible than subclassing-based interfaces. But the problem doesn't just apply to subclassing.
Here is an instance of the problem in Haskell where despite having typeclasses, the community still haven't been able to fix the fragmentation that resulted from String, ByteString, ByteString.Lazy, Text and Text.Lazy. Libraries simply aren't adopting one common nominal interface: most still just use Text/String/ByteString directly, others implement their own personal and incompatible typeclass, and so on.
With structural types, you can implement the entire interface of an existing type and make all libraries that utilise the exiting type compatible with the new implementation. Without those libraries doing any changes whatsoever.
@shelby3 I'm not sure if this thread is the right place for this discussion? You have a suggestion issue open with multiple requests for clarification, that deals with type classes. This thread currently seems to be about whether or not compile-time only nominal types are a good idea, and if so, what they would look like. Type classes seem an orthogonal concept, to me, though there might be some interesting links.
@spion wrote:
I don't deny that typeclasses are better and more flexible than subclassing-based interfaces. But the problem doesn't just apply to subclassing.
Here is an instance of the problem in Haskell where despite having typeclasses, the community still haven't been able to fix the fragmentation that resulted from
String
,ByteString
,ByteString.Lazy
,Text
andText.Lazy
. Libraries simply aren't adopting one common nominal interface: most still just useText
/String
/ByteString
directly, others implement their own personal and incompatible typeclass, and so on.With structural types, you can implement the entire interface of an existing type and make all libraries that utilise the exiting type compatible with the new implementation. Without those libraries doing any changes whatsoever.
Per @SimonMeskens's suggestion, please continue this discussion in the issue thread I started for discussing typeclasses. I copied this reply over there. Thanks.
The problem you refer to (as well as Haskell's inability to do first-class unions) is because Haskell has global type inference. So this means that if we implement a data type more than one way on the same typeclass target, then the inference engine can't decide which one to apply. That problem is not with typeclasses, but with Haskell's choice of a globally coherent inference. Haskell's global inference has some benefits, but also has those drawbacks.
To enable multiple implementations (each for a specific typeclass) of the same data type on the same typeclass, we can support declaring typeclasses that extend other typeclasses. TypeScript's existing interfaces can serve this dual role I think. Then at the use site, we can provide a mechanism for selecting which implementation (i.e. sub-interface) is to be used. We can get more into the details in the future.
Suffice it to say that they are just as flexible as structural typing in terms of extensibility. The differences are they provide the ability to group structure nominal at any granularity we choose as an API designer. And to distinguish between equivalent structure that has a different name (nominal type).
@SimonMeskens wrote:
I'm opposed to solutions that create a construction that works purely compile-time. Typescript should attempt to type existing javascript as precisely and concisely as possible. The problem with nominal types is that they are not nominal at run-time, unless they are tagged objects. Typescript can already type tagged objects, though obviously, we are waiting for symbol support in computed properties to really leverage this feature.
Can anyone explain to me something that nominal types can do, that tagged objects don't do?
As I think you alluded to upthread discussion, the instanceof
operator will use [Symbol.hasInstance]
instead of the prototype
chain, so we can simply tag objects with Symbols
metadata to indicate which nominal types the instance should be.
The point of typeclasses is to give us extensibility in two directions of Wadler's Expression Problem, which is orthogonal to tagging the runtime objects.
Tagging runtime objects is reification (i.e. not erasure) so that the runtime can interopt with the static typing and so the runtime can interopt with nominal types and not just structural types.
@RyanCavanaugh just a note, the pattern you proposed:
type UserId = string & { "is user id": void }
can't be used as an specialized string
. For example, this doesn't works:
type MyUsers = { [userId : UserId] : {/*...*/}} // compiles
let mu: MyUsers
let u = mu[<UserId>'sdf'] // TS7017: Element implicitly has an 'any' type because type 'MyUsers' has no index signature.
I want TypeScript to support phantom types when supporting nominal types.
@falsandtru: Nominal types are supported already, as tagged types are supported. We just haven't really decided on a good default way as a community.
@SimonMeskens
Nominal types are supported already
Really? How can I define a nominal type based on string
that I can use in an indexer?
no, still investigating: https://github.com/Microsoft/TypeScript/wiki/Roadmap
I'm not at a dev computer right now, but almost all use cases have some way to implement them, using tagged types.
@SimonMeskens You should scroll up to Sep. 13 - we both had this very discussion already. I got a bit carried away with my last question, thinking I might have missed a development in this regard, without realizing what you refer to.
While tagged types can be used to achieve the same goals as one usually want to achieve with nominal types, they're not a solution for them. My prime example is again having a nominal type for string
that can be used as an indexer type. Having a fake interface with a brand is not allowed to be used. Actually expanding an object with a brand value is a no-go and would completely falsify what nominal types try to achieve.
@MartinJohns How about implementing a dictionary type that allows indexing with tagged values:
interface Dictionary<Key, Val> {
get(k:Key): Val;
set(k:Key, v: Val);
}
it would internally use this._dict[key.toString()]
If thats not viable, why not fix this in TypeScript by allowing any subtypes of string/number to be specified as indexes?
What about a property with an enum member as its type? That is IIRC a way of doing this in a nominal, type-safe way (they're Number subtypes, and there's only one of them).
enum StatusTypes {Resolved, Pending, Rejected}
type Status<T> =
{type: StatusTypes.Resolved, value: T} |
{type: StatusTypes.Rejected, value: Error} |
{type: StatusTypes.Pending}
Use-case: implement functional programming constructs like the interfaces from fantasy-land
; many of them cannot be implemented using structural typing, e.g. Chain
.
That's an issue with the lack of higher kinded types, not lack of nominal types.
On Thu, Dec 1, 2016, 09:32 Tycho Grouwstra notifications@github.com wrote:
Use-case: implement functional programming constructs like the interfaces from fantasy-land https://github.com/fantasyland/fantasy-land/issues/140#issuecomment-263762353; many of them cannot be implemented using structural typing, e.g. Chain https://github.com/fantasyland/fantasy-land/issues/140#issuecomment-263879504 .
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/202#issuecomment-264187290, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBPmVd67GZ-EAcKiLa71pCFZAS8R9ks5rDtpngaJpZM4CPxZP .
I forget the precise issue, but it should be easily found by searching the issues. Nominal types won't fix that, BTW.
On Thu, Dec 1, 2016, 11:43 Isiah Meadows impinball@gmail.com wrote:
That's an issue with the lack of higher kinded types, not lack of nominal types.
On Thu, Dec 1, 2016, 09:32 Tycho Grouwstra notifications@github.com wrote:
Use-case: implement functional programming constructs like the interfaces from fantasy-land https://github.com/fantasyland/fantasy-land/issues/140#issuecomment-263762353; many of them cannot be implemented using structural typing, e.g. Chain https://github.com/fantasyland/fantasy-land/issues/140#issuecomment-263879504 .
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/202#issuecomment-264187290, or mute the thread https://github.com/notifications/unsubscribe-auth/AERrBPmVd67GZ-EAcKiLa71pCFZAS8R9ks5rDtpngaJpZM4CPxZP .
@isiahmeadows: thank you for correcting me; guess I'll be following #1213.
Just as a reminder, WebGL typings need this. #5855
This would be very handy for us too. (Or probably the linked #364) We are working on Android and iOS where one of the layout systems work in device independent pixels, while the other in real device pixels, in JavaScript we prefer to use device pixels but conversions do occur a lot and having a way to distinguish dip numbers from px numbers would be great!
I use a command/query/response pattern in C# that doesn't work well in TypeScript
public interface IQuery<T> {}
public class MyRequest : IQuery<MyResponse>
{
int Id { get; set; }
}
public class MyResponse
{
string Error { get; set; }
}
public interface IRequestHandler<TRequest, TResponse>
where TRequest: IQuery<TResponse>
{
TResponse Query(TRequest request);
}
public class MyRequestHandler: IRequestHandler<MyRequest, MyResponse>
{
public MyResponse Query(MyRequest request)
{
//Do some stuff
//Return response
}
}
In C# I can only pass specific combinations for Request/Response - but in TypeScript I can pass in just about any type and expect just about any type.
const request = new CountPeopleWithAncestorName('Smith', 5);
const response: CountPeopleWithName = this.apiHandler.query(request);
In the above example I have passed in the wrong request type, but it works because the request has a "name" member as does the proper request CountPeopleWithName. At runtime I would detect the combination is incorrect, but I want to see it is incorrect at compile time.
At the moment I am having to add members to my request/response classes like this
class countPeopleWithNameQuery {
'Query:CountPeopleWithName'() : {}
}
class countPeopleWithNameResponse {
'Response:CountPeopleWithName'() {}
}
and it's very ugly.
Would be nice if I could turn on some kind of nominal checking flag in the compiler options. Or if the compiler did nominal checking by default and the only place you can duck-type is when typecasting.
this.callSomeMethod( <SomeClass> { 'a': true, 'b': 42 } );
@mrpmorris You're trying to write C# in TypeScript, you should instead learn TypeScript. Your use case is not a valid reason to ask for nominal types imo, it's just code written in the wrong language.
@SimonMeskens I would be very grateful to know how to implement mrpmorris C# code in Typescript. Would you be so kind to share any possible solutions. Many thanks.
@AJamesPhillips The whole example provided reeks of object-oriented nonsense that only makes sense in a language without the power of expression that JavaScript has. I'd need to see a full example, not some snippets. My suspicion is that you don't need identity at all and it's just an artifact of trying to write OO code in a more expressive language. If you do really need object identity, I don't see any reason why you can't just use a string ID like every other JavaScript library does. TypeScript can type those just fine (unlike C# I might add).
@AJamesPhillips
In the JS world, we just use string/numeric IDs to track type identity rather than types, since when deserializing, we don't have runtime assistance to match type -> ID. Unlike C#, TypeScript supports typing based on arbitrary literal values, such as strings, numbers, symbols, booleans, or even enum constants. And for most use cases that people think they need nominal typing in TS, it's almost always solved by literal types.
My need for it is different, though: I need to properly type a plain object-based data structure whose members are discriminated by only part of a bit mask (space optimization), and making it nominal would be far easier than using some magical bit mask types with embedded enum support (both for me and the feature implementor).
@SimonMeskens
The whole example provided reeks of object-oriented nonsense that only makes sense in a language without the power of expression that JavaScript has. I'd need to see a full example, not some snippets. My suspicion is that you don't need identity at all and it's just an artifact of trying to write OO code in a more expressive language. If you do really need object identity, I don't see any reason why you can't just use a string ID like every other JavaScript library does. TypeScript can type those just fine (unlike C# I might add).
While I am not sympathetic to the idea of introducing nominal typing in TypeScript, and while I certainly agree that the approach is wrong, as transliterating code from one language into another almost always is, I do not think it is a question of language superiority or inferiority.
I think it comes down to what you said earlier: learn the language. Once one does that, it becomes natural to play to its strengths intuitively and avoid its weaknesses.
In languages like C#, one can design and even implement a program from the types down, depending on their reification, performing both static and dynamic dispatch over them and use types both as a means of specifying behavior and enforcing contracts.
In TypeScript, it is important to start with values and let the types flow from the natural structure of those values. As those structures coalesce it may or may not make sense to codify them more rigidly but the values themselves must come first.
@aluanhaddad Don't get me wrong, I LOVE C#, but prototypal inheritance is strictly a superset of object-orientation, meaning everything you can express in OO, you can in prototypal, but not the other way round. This means that on a provably, mathematical ground, JavaScript is just superior in expressiveness and it shows in day to day code. Experts that have been writing top-level JavaScript for years, like, say, Eric Elliott, constantly show code that just wouldn't be expressible in C# without bloat. TypeScript manages to type a lot of this expressiveness.
The end result is that it's easy to write C# style code in JavaScript, but you shouldn't and doing so is a clear way to shoot yourself in the foot, as demonstrated with the above code. For example, it has a class with just one function and it's trying to type the class. In TypeScript, you just have that function itself as an object and just type that. Even more important is that JavaScript doesn't even have classes in the sense that C# does, so what are we even trying to type if you just straight port it over to TypeScript?
I can think of several ways to rewrite the above code leveraging JavaScript going from storing the appropriate query with the request, to functional reactive styles and higher abstractions using pipes.
The biggest thing is though: that code is not a reason to implement nominal types in TypeScript.
@SimonMeskens For the most part, I heartily agree.
Interestingly, even in C#, single method classes are often unnecessary, although not uncommon, thanks to delegates which offer something conceptually closer to structural typing. Nothing compared to what we have in TypeScript of course, but it is interesting to think about.
SAM types were proposed for C# last year and the proposal was rejected because it simply doesn't make sense. Delegates have long allowed for higher levels of abstraction in C# compared to say Java circa 2014.
You are indeed correct that JavaScript is flexible enough to fully implement, in terms of composition, inheritance, and everything in between, all common Object Oriented Programming patterns. From straight vertical inheritance, to facades, mixins, interceptors, and delegates (design pattern), JavaScript is uniquely expressive.
Personally, however, I try to avoid inheritance where possible, not just because I've been told it is a good practice, but because I just find it amazing what can be done with simple objects and higher order functions. I don't often need it.
Sometimes I use inheritance to be sure, but ESNext features like Object Rest/Spread, something I admit I was initially skeptical of but have fallen in love with, make composition truly effortless.
That said, all of these patterns tend to feel brittle to me without the tooling that TypeScript provides. Type inference for Object Rest/Spread is really an amazing thing in this language, and I'm really glad the TypeScript team held off on implementing it until they got the type level transformations for this unique feature precisely defined and implemented.
The other day I was writing a unit test for an abstraction that wraps window.localStorage || window.sessionStorage
and provides automatic conversion to and from JSON. I realized I needed a spy for my test to ensure that values were being passed to the underlying provider in the correct format. Being able to write {...storage, set(key, value) {...})}
, and have it all typecheck, with no fancy mocking framework (just TypeScript, tape, and jspm) was a great feeling.
@aluanhaddad That's exactly what I was trying to convey, but you expressed it more clearly and beautifully than I could.
I look at the above example and I see: no higher order functions, SAM types, an empty interface to signify identity due to lack of literal types (identity types aka literal symbol types would be even better though), over-reliance on generic types due to lack of higher and combined types. TypeScript has most (not all) of these things down, why use stunted concepts from an OO language?
I'd go as far as to say that in JavaScript, you should not favor composition, you should ONLY do composition. Differential inheritance through prototypes is wider than just vertical inheritance, so I'm not saying you shouldn't do any stuff with the prototype, but I'd probably advise against any class-type inheritance.
Even though this thread is very long, I'd like to recommend looking at mypy's "unique types", it's used as NewType (also called distinct types).
For the discussion see https://github.com/python/mypy/issues/1284
I did not know about "branded types", but they seem sorta okay-ish. (They are even mentioned in this Deep Dive book.) But as others have mentioned, they are not universally useful. (For example they don't work as index types. But they work well for serious business things like differentiating between principal and rate for money related stuff.
enum _C { }
type Credit = _C & number;
let c: Credit;
let user_id: number;
c = 2;
user_id = 3;
function a(cr: Credit) {
console.log(cr);
}
a(user_id); // no compiler alert, bad :/
console.log(c + d); // no compiler alert, bad :/
interface Mooneyz extends Number {
_MoonezyBrand: string;
}
let m1: Mooneyz = 2 as any;
function b(dough: Mooneyz) {
console.log(dough);
}
b(m1);
b(d); // compiler alert, good!
)
Also, for the very simple case of differentiating between numbers and other variables/values that have the same structural type, Scala uses Value Classes.
this is the latest and most problem free candidate for being a tag-type
declare class As<S extends string> {
private as: S;
}
type Email = string & As<'email'>;
type CustomerId = number & As<'customer-id'>;
const basket = {} as Basket;
On Jun 14, 2017 5:18 AM, "devdoomari" notifications@github.com wrote:
@aleksey-bykov https://github.com/aleksey-bykov but that doesn't work for interfaces / etc:
declare class As
{ private as: S; }type Basket = { a?: number } & As<'basket'> const testBasket: Basket = {} // error: no 'as' in {}— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/202#issuecomment-308373939, or mute the thread https://github.com/notifications/unsubscribe-auth/AA5PzZ698L2V9bnzMaKFCKXZ36TY5P4Qks5sD6VmgaJpZM4CPxZP .
@aleksey-bykov I like the way you went about it... I've been experimenting with similar (but not quite as elegant) fake-property/forced-assertion patterns for added granularity, but when I took your example into the playground I eventually found my self here:
let x = <'email'>'abc.com'; // or … as Email
let y = <'customerID'>'abc.com'; // or … as CustomerID
x = y; // typescript is equally unhappy
My realization at this point was that no matter how elaborate or simple the approach used to introduce type distinction... all those guards are basically breaking the same rules that help make them feasible in the first place. So unless Typescript provides a true Symbol like concept to explicitly and irrevocably declare structurally compatible types as functionally distinct and incompatible, the only thing we can hope to achieve is clever ways to opt-out of type-checking in order to achieve a false sense of what we assume is a more granular type checking.
type assertions via <>
or as
is a hammer that can make things look as you want them to bypassing typechecks, it's a powerful tool that needs to be used with caution, in right hands it can do wonders, so can it shoot you in your foot
Exactly... well put!
Saleh Abdel Motaal
On Jun 23, 2017, at 7:05 PM, Aleksey Bykov notifications@github.com wrote:
type assertions via <> or as is a hammer that can make things look as you want them to bypassing typechecks, it's a powerful tool that needs to be used with caution, in right hands it can do wonders, so can it shoot you in your foot
— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.
@aleksey-bykov Have you managed to write a function that can pattern match on this As
type? Been having a lot of trouble trying to come up with a match
function in which inference works myself.
Here's my idea for nominal types:
Syntax:
new type Type<T> = Foo<T> | Bar<T>;
new interface Foo<T> extends Bar<T> {
// ...
}
new
before the type declarationnew class
would conflict, but it is unnecessary with the recommended new compiler optionSemantics:
new
interfaces cannot merge with any other interface, with two exceptions:
new
and non-new
interfaces may freely merge, resulting in a nominal interfacenew
interfaces may only be extended with non-new
interfacesPotental FAQ:
new
? I drew inspiration from Haskell's (and derivatives') newtype
declarations, but I didn't want to re-invent yet another mess.new
interface merging across files? It's to decouple interface extension from the type of interface it is. It also makes it easier to update the standard library and existing definition files to leverage this without fear of mass breakage.new
interface merging within a single file? It's to aid those who generate source code and/or definition files, so they don't have to do as much.Thought it's about time someone comes up with a more detailed proposal.
you should look at the function/method overloads, its the only way to "pattern match" types at compile time (which is where as
makes any sense before it gets erased)
getById(id: number & As<'one-entity'>): OneEntity;
getById(id: number & As<'another-entity'>): AnotherEntity;
getById(id: number): Entity {
// your code here
}
Just an example of false type assertion by TypeScript:
class Animal {
run() { }
}
// Create a nominally-typed animal
const a = new Animal()
// Create a structurally-typed animal
const b: Animal = {
run() { }
}
function makeAnimalRun(animal: Animal | string) {
if (animal instanceof Animal) {
// Here animal is inferred as an Animal, which is true
animal.run()
}
else {
// Here animal is inferred as a string, but still COULD BE of structural type Animal
console.log(`${animal.toUpperCase()}!!!!`)
}
}
makeAnimalRun(a)
makeAnimalRun(b)
Maybe until TypeScript supports Nominal Types, the behaviour of instanceof
in the type inference should redefined.
That's indeed a bug, the type of animal inside of the else should still be Animal | string
You should make a separate issue with just this bug.
Edit: be more exact in what values are eligible.
Here's a proposal for dealing with nominal typing on a smaller scale (what JS actually does, and similar to what Flow does):
Any interface that shares an identifier reference with a value that is an object/function with a Symbol.hasInstance
callable property should be considered a nominal interface. Additionally, that property should be specified as appropriate in the various built-ins, including for constructible types by default. Some of the effects:
Symbol.hasInstance
to set certain constraints to check.@isiahmeadows Can you elaborate? It sounds like a really bad idea with lots of edge cases, but I want to see some code samples.
I like the general idea of incorporating Symbol.hasInstance into the fold
@simonbuchan Here's an idea of how you could create a nominal subtype of number
:
// Positive.ts
export {Positive};
interface Positive extends number {}
const Positive = {
[Symbol.hasInstance]: x => typeof x === "number" && x > 0,
};
interface NotNegativeOrZero extends number {}
const NotNegativeOrZero = {
[Symbol.hasInstance]: x => typeof x === "number" && x > 0,
};
// main.ts
import {Positive, NotNegativeOrZero} from "./Positive";
const x = 0;
if (x instanceof Positive) {
let pos: Positive = x;
let num: number = x; // Error
let nnz: NotNegativeOrZero = x; // Error, even though technically equivalent.
}
In general, it shares mostly the same number of edge cases JS has - instanceof
requires the RHS to be an object (Function
or Object
) with a Symbol.hasInstance
, or failing that, a callable object (Function
). Technically, there are a few edge cases in the ES spec itself that engines do have to follow, like this one that's pure ES5:
// Create callable object with no `prototype` property
function F() {}
// Define the property without respect to inherited descriptors
Object.defineProperty(F, Symbol.hasInstance, {value: void 0});
// Set the prototype property to something that doesn't require
// referencing the constructor
F.prototype = Object.prototype;
// Do an `instanceof` check. This should evaluate to `true` even
// in ES6, since it uses the fallback ES5 behavior, which checks
// against `F.prototype === Object.prototype`
assert.strictEqual({} instanceof F, true);
There is one key edge case unique to my proposal: you could add a Symbol.hasInstance
to the corresponding value's type via interface merging, and then it could be structural in one place, nominal in another. In practice, there's a few ways to handle this effectively, each with pros and cons:
Symbol.hasInstance
extensions outside of the file the value is first defined in
Symbol.hasInstance
extensions non-locally to the modules that don't import it
One issue with defining it for native builtins Edit: constructors is that you'd have to make it a compiler flag for it, since it could break a lot of code.
@isiahmeadows the problem is that that's really unintuitive with the interface checking for a similarly named variable. Looking through my code, that would break some of the projects I'm currently working on too.
I like the general idea though, why not just add a keyword to the interface to reference which Symbol.hasInstance carrying variable it refers to?
@SimonMeskens
Looking through my code, that would break some of the projects I'm currently working on too.
If you're referring to existing usage of Symbol.hasInstance
, I did propose a flag at the end to enable the type addition and edited that part to say "constructors" (by default), which should hopefully avert the issue with existing code.
the problem is that that's really unintuitive with the interface checking for a similarly named variable.
I was explaining it in technical, fairly exacting terms, but short summary:
[Symbol.hasInstance]
method to the value.I like the general idea though, why not just add a keyword to the interface to reference which Symbol.hasInstance carrying variable it refers to?
Proposal: support non-structural typing (e.g. new user-defined base-types, or some form of basic nominal typing). This allows programmer to have more refined types supporting frequently used idioms such as:
1) Indexes that come from different tables. Because all indexes are strings (or numbers), it's easy to use the an index variable (intended for one table) with another index variable intended for a different table. Because indexes are the same type, no error is given. If we have abstract index classes this would be fixed.
2) Certain classes of functions (e.g. callbacks) can be important to be distinguished even though they have the same type. e.g. "() => void" often captures a side-effect producing function. Sometimes you want to control which ones are put into an event handler. Currently there's no way to type-check them.
3) Consider having 2 different interfaces that have different optional parameters but the same required one. In typescript you will not get a compiler error when you provide one but need the other. Sometimes this is ok, but very often this is very not ok and you would love to have a compiler error rather than be confused at run-time.
Proposal (with all type-Error-lines removed!):