microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.54k stars 12.43k forks source link

Support some non-structural (nominal) type matching #202

Open iislucas opened 10 years ago

iislucas commented 10 years ago

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!):

// Define FooTable and FooIndex
nominal FooIndex = string;  // Proposed new kind of nominal declaration.
interface FooTable {
  [i: FooIndex]: { foo: number };
}
let s1: FooIndex;
let t1: FooTable;

// Define BarTable and BarIndex
nominal BarIndex = string; // Proposed new kind of nominal declaration.
interface BarTable {
  [i: BarIndex]: { bar: string };
}
let s2: BarIndex;
let t2: BarTable;

// For assignment from base-types and basic structures: no type-overloading is needed.
s1 = 'foo1';
t1 = {};
t1[s1] = { foo: 1 };

s2 = 'bar1';
t2 = { 'bar1': { bar: 'barbar' }};

console.log(s2 = s1); // Proposed to be type error.
console.log(s2 == s1); // Proposed to be type error.
console.log(s2 === s1); // Proposed to be type error.

t1[s2].foo = 100; // Gives a runtime error. Proposed to be type error.
t1[s1].foo = 100;

function BadFooTest(t: FooTable) {
  if (s2 in t) {  // Proposed to be type error.
    console.log('cool');
    console.log(t[s2].foo); // Proposed to be type error.
  }
}

function GoodBarTest(t: BarTable) {
  if (s2 in t) {
    console.log('cool');
    console.log(t[s2].bar);
  }
}

BadFooTest(t1); // Gives runtime error;
BadFooTest(t2); // No runtime error, Proposed to be type error.
GoodBarTest(t1); // Gives runtime error; Proposed to be type error.
GoodBarTest(t2);
antanas-arvasevicius commented 8 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 ) we'll just add that object into WeakMap. And when doing pattern matching / instanceof we'll can check for type match inside WeakMap.

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?

zpdDG4gta8XKpMCd commented 8 years ago

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 .

shelby3 commented 8 years ago

@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).

yortus commented 8 years ago

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).

shelby3 commented 8 years ago

@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).

spion commented 8 years ago

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.

SimonMeskens commented 8 years ago

@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.

shelby3 commented 8 years ago

@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, Textand 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.

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).

shelby3 commented 8 years ago

@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.

nahuel commented 7 years ago

@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.
falsandtru commented 7 years ago

I want TypeScript to support phantom types when supporting nominal types.

SimonMeskens commented 7 years ago

@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.

MartinJohns commented 7 years ago

@SimonMeskens

Nominal types are supported already

Really? How can I define a nominal type based on string that I can use in an indexer?

falsandtru commented 7 years ago

no, still investigating: https://github.com/Microsoft/TypeScript/wiki/Roadmap

SimonMeskens commented 7 years ago

I'm not at a dev computer right now, but almost all use cases have some way to implement them, using tagged types.

MartinJohns commented 7 years ago

@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.

spion commented 7 years ago

@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?

dead-claudia commented 7 years ago

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}
KiaraGrouwstra commented 7 years ago

Use-case: implement functional programming constructs like the interfaces from fantasy-land; many of them cannot be implemented using structural typing, e.g. Chain.

dead-claudia commented 7 years ago

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 .

dead-claudia commented 7 years ago

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 .

KiaraGrouwstra commented 7 years ago

@isiahmeadows: thank you for correcting me; guess I'll be following #1213.

dead-claudia commented 7 years ago

Just as a reminder, WebGL typings need this. #5855

PanayotCankov commented 7 years ago

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!

jonaskello commented 7 years ago

I have found another use-case for nominal typing in #15408. The idea is to have abstract types like in Ocaml/reason or opaque types as they are called in Elm.

mrpmorris commented 7 years ago

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 } );

SimonMeskens commented 7 years ago

@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.

AJamesPhillips commented 7 years ago

@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.

SimonMeskens commented 7 years ago

@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).

dead-claudia commented 7 years ago

@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).

aluanhaddad commented 7 years ago

@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.

SimonMeskens commented 7 years ago

@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.

aluanhaddad commented 7 years ago

@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.

SimonMeskens commented 7 years ago

@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.

PAStheLoD commented 7 years ago

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.

zpdDG4gta8XKpMCd commented 7 years ago

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'>;
zpdDG4gta8XKpMCd commented 7 years ago

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 .

SMotaal commented 7 years ago

@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.

zpdDG4gta8XKpMCd commented 7 years ago

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

SMotaal commented 7 years ago

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.

masaeedu commented 7 years ago

@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.

dead-claudia commented 7 years ago

Here's my idea for nominal types:

Syntax:

new type Type<T> = Foo<T> | Bar<T>;
new interface Foo<T> extends Bar<T> {
    // ...
}
  • As simple as adding an extra new before the type declaration
  • No equivalent for classes/enums exist, but they can be implicitly elevated via a new compiler option
  • No conflict with existing syntax, single token lookahead required
    • new class would conflict, but it is unnecessary with the recommended new compiler option

Semantics:

  • It would elevate the type to be nominal rather than structural
  • Nominal types are considered subtypes of their matching structural type
  • new interfaces cannot merge with any other interface, with two exceptions:
    • Within the same file, new and non-new interfaces may freely merge, resulting in a nominal interface
    • Imported new interfaces may only be extended with non-new interfaces

Potental FAQ:

  • Why new? I drew inspiration from Haskell's (and derivatives') newtype declarations, but I didn't want to re-invent yet another mess.
  • Why is it kind of decorator-ish? I wanted to ensure it was easy and obvious to create and learn, but without making it the default.
  • How do I set a variable to a literal of a nominal type? How do I pass a literal in a matching nominal argument? How do I return a literal as a nominal type? Cast it. It's no different than any other form of downcasting.
  • How could you make existing classes nominal without setting the compiler option? Just create a matching nominal, zero-member interface.
  • Why not enable it by default for classes or enums? That could break a lot of existing code, especially for classes. Also, in particular, enums are currently equivalent to their unions, and breaking that could break some larger code bases.
  • Why ban 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.
  • Why allow such liberal 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.

zpdDG4gta8XKpMCd commented 7 years ago

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
}
kube commented 7 years ago

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.

SimonMeskens commented 7 years ago

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.

dead-claudia commented 7 years ago

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:

  • Most classes will be newly considered nominal.
  • Most built-ins will now be considered nominal types.
  • Nominal primitive "subtypes" can use a Symbol.hasInstance to set certain constraints to check.
  • Most everything else won't be affected.
SimonMeskens commented 7 years ago

@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

dead-claudia commented 7 years ago

@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:

  1. Ignore Symbol.hasInstance extensions outside of the file the value is first defined in
    • Avoids most of the confusion with an interface being both nominal and structural
    • Creates an odd edge case that those unfamiliar with the design rationale wouldn't get
  2. Propagate Symbol.hasInstance extensions non-locally to the modules that don't import it
  3. If multiple identifiers within a scope reference the same interface, with at least one seeing it as nominal (due to an extension in that file or an imported file), make them all nominal in that scope
    • Technically, nominal subtypes are always subtypes of their structural variants (like how enums are now), so the latter is observably sound
    • Users could choose to opt in to nominal types incrementally
    • With enough indirection when importing, you could import a type nominally when you thought it'd be structural.
  4. Don't
    • Makes resolution simpler and easier to explain in full
    • These kinds of issues are likely to be rare in practice, and when it does occur, it's already going to be a code smell
    • You could define two modules, one exposing the interface structurally, the other nominally

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.

SimonMeskens commented 7 years ago

@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?

dead-claudia commented 7 years ago

@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:

  • If a compiler flag is set, any interface that shares a name with a constructor is considered nominal.
    • This includes classes.
  • Nominal subtypes can be manually constructed by:
    1. Creating an interface and value with matching names.
    2. Add a [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?

  1. If necessary, you can define a value alias, and [it does still narrow correctly, even when checking it](http://www.typescriptlang.org/play/index.html#src=class%20Foo%20%7B%20%7D%0D%0Avar%20Bar%20%3D%20Foo%0D%0Atype%20Bar%20%3D%20Foo%3B%0D%0A%0D%0Avar%20foo%3A%20any%20%3D%20new%20Foo()%0D%0A%0D%0Aif%20(foo%20instanceof%20Bar)%20%7B%0D%0A%20%20%20%20type%20T%20%3D%20typeof%20foo%20%2F%2F%20Hover%20over%20the%20%60T%60%0D%0A%7D).
  2. It seems really odd to check one variable to test a differently-named type - I'd find it far less intuitive. Out of curiosity, what would be a use case for that, where defining a local alias wouldn't work?