You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When using a functional programming style it is common to have a module that contains a record type, constructor functions for that record type and other function that operate on that type. For example consider a simple module for a date record:
date.ts
exportinterfaceDate{readonlyday : number,readonlymonth : number,readonlyyear : number};exportfunctioncreateDate(day: number,month: number,year: number): Date{return{day, month, year};}exportfunctiondiffDays(date1: Date,date2: Date): number{// Imagine implementation that calculates // the difference in days between date1 and date2}// Extract the year from the dateexportfunctionyear(date: Date): number{returndate.year;}
This works well to start with. However we may at some point want to refactor the structure of the Date type. Let's say we decide it is better to have it store ticks since epoch:
exportinterfaceDate{readonlyticks : number,};
The problem is now that other modules may have directly read the Date.years property instead of going through our year() function. So other modules are now directly coupled to the type's structure, making our refactoring hard because we need to go through and change all calling modules instead of just changing our date module. (In real-world scenarios the record type is more complex and perhaps nested).
The Ocaml programming language has a nice solution for this called abstract types (see docs here, under the heading "Abstract Types".
The idea is that we export the fact that there is a Date type and you have to use that type to call our functions. However we do not export the structure of the Date type. The structure is only known within our date.ts module.
Yes that's a good point. class is certainly the right way to achieve encapsulation if you are doing OO style programming. However class is not a good fit for FP style. In FP your building blocks are immutable types (data records) and pure functions. As Javascript is multi-paradigm (OO and FP) I think it would make sense for typescript to support FP style encapsulation too.
Other than the fact that classis a bad fit for FP, there are practical reasons why you would prefer pure data objects over classes. One being that they are easy to serialize to JSON. Another that they are easy to deep compare. Also duck typing works for data objects but not classes. They are also easy to merge etc.
year is exposed because I need to access it within the module and typescript does not allow any method of hiding it outside the module while still keeping it a plain immutable data record. Hence the feature request :-). For the purpose of FP style, I want to work with only plain immutable data records (types with constructors). There is a good example of how abstract types are used in the Ocaml docs I linked above. They are also mentioned here in a talk about reason which is an Ocaml dialect by facebook with syntax more similar to typescript.
Could you elaborate on how I would use accessors with plain objects? How does the typing look for Date in your example above and what would be the implementation in the { ... } be? Would the accessors not be functions/methods? And would not the accessors be available to other modules, so everyone would know the shape of the Date type?
Jonas Kello (@jonaskello) that is correct, the accessors would be methods. Such get accessors imply nothing about the structure of the underlying value and can simply return values from the enclosing lexical scope (no need for this).
This does mean that other modules would know the shape of Date but I am not sure how this can be avoided in a structural type system. Well Ocaml is structurally typed...
I think you have a good point about not being able to hide the structure in a structural type system. What is needed for abstract types would probably be more akin to nominal typing. However I don't think a full nominal type system is needed just to get abstract types?
I made an example which tries to emulate abstract types. As you can see it breaks down because of structural typing.
EDIT: I managed to update the example so it to some degree emulates nominal typing by using a string literal type as a tag. This may actually be good enough but still it would be nice to have proper support for abstract types.
date.ts
exportinterfaceDate{readonlytype: "ReallySecretDate"};interfaceDateContent{readonlyday: number,readonlymonth: number,readonlyyear: number};typeInternalDate=Date&DateContent;exportfunctioncreateDate(day: number,month: number,year: number): Date{consttheDate: InternalDate={type: "ReallySecretDate", day, month, year };returntheDate;}exportfunctiondiffYears(date1: Date,date2: Date): number{constdate1_internal=date1asInternalDate;constdate2_internal=date2asInternalDate;returndate1_internal.year-date2_internal.year;}// Extract the year from the dateexportfunctionyear(date: Date): number{constdate_internal=dateasInternalDate;returndate_internal.year;}
consumer.ts
import{createDate,diffYears,year}from"./date";constdate1=createDate(2017,1,1);constdate2=createDate(2018,1,1);// const year1 = date1.year; // Error - goodconstyear2=year(date1);// OK - goodconstdiff=diffYears(date1,date2);// good// const year1 = year({}); // Error - good// const diff2 = diffYears({}, {}); // Error - goodconstyear1=year({type: "ReallySecretDate"});// No Error - not good but maybe good enoughconstdiff2=diffYears({type: "ReallySecretDate"},{type: "ReallySecretDate"});// No Error - not good but maybe good enough
So I think one conclusion is that two abstract types would need to be considered different even if their structure is the same. This would probably create a simple nominal typing system. So in that sense it may be related to #202.
I haven't used Elm myself but it seems to have an abstract type concept, only it is called opaque types. I also think Elm uses structural typing. I just mention it as an example of a language with both structural typing and abstract/opaque types.
Calling the types "opaque" may be better than "abstract" since that is an overloaded term already considering abstract classes. Also considering the distinct keyword from this proposal for nominal typing, I would revise my syntax proposal for abstract/opaque types to:
The following pattern is a good out of the box emulation.
Date.ts
classDate{publicconstructor(privatereadonlyday: number,privatereadonlymonth: number,privatereadonlyyear: number){}// You can make the constructor private and use a static creator// function if you prefer itstaticcreateDate(day: number,month: number,year: number){returnnewDate(day,month,year);}staticyear(date: Date){returndate.year;}staticdiffYears(date1: Date,date2: Date): number{returndate1.year-date2.year;}};export{Date};exportconstcreateDate=Date.createDate;exportconstyear=Date.year;exportconstdiffYears=Date.diffYears;
test.ts
import{Date,createDate,year,diffYears}from'./Date'consty2017=createDate(1,1,2017);// It's ok to use "pojo" Dates as we don't rely on any "methods"// or hidden class being presentconstpojoYear: Date=JSON.parse('{ "year": 2000, "month": 1, "day": 1 }');y2017.year// errordiffYears(y2017,pojoYear);// 17, as expecteddiffYears(y2017,{year: 2000,month: 1,day: 1});// error
Note that you don't use classes as OOP classes. You just use the visibility primitives they provide. Such a usage is more of a "namespace" or a module-like dictionary, thus I think it's perfectly inline with the functional style.
falsandtru (@falsandtru) That's an interesting approach :-). However now we have two levels of encapsulation, the module level and the class level. I think for FP style we want the module to be the only level and not use class at all (I actually ban class with linting rules). Also the module level encapsulation we cannot git rid of so if one has to go it would have to be the class level.
Don't get me wrong. I'm not an advocate of OOP, quite the opposite actually. I personally use classes only when they are the only option and I still think how I can avoid them. However the way currently JS works is not with hidden object state. TypeScript has made a deviation in classes and I would prefer that it doesn't do any more free-will choices (namespaces, enums, private, protected, three-slash references are already enough). On the other hand, mindfully using these features where appropriate is not a deadly sin.
I don't see what the differences between ADTs and interfaces have to do with the problem at hand. In traditional languages both are nominal and both precisely state what the programmer can access. ADTs have the benefit that they may be one of several options, while interfaces describe a single shape. In the language that I'm familiar with - Haskell, if you export a data type and it's value constructors, you can pattern match on them any way you want.
Your ask seems to preclude two things - nominal types and a mechanism to export only the data type without its constructors (if we follow ADT's analogy).
Now, lets get a step back again. We can already achieve this behaviour with an alternative mechanism. Why not use it? After all, languages are different, the idea is what matters :)
PS: The module itself is an implicit static class. If we were able to make our static class the export object itself than there would be no difference between the two concepts.
Ok, checked #13002, #13347 but still think readonly is really useful in practice, and those issues are more about splitting hairs :-). Imagine having to make all of your redux state objects as class instead of plain objects in order to make it immutable. That to me is syntactic noise.
Regarding hidden state, I think JS objects neither hides or exposes their state as you don't know the type of the object. So for me, an object in JS has "undetermined" state which may be specified by comments/annotations.
Edit: The module is actually an implicit instance of an Object and can be typed as such if you import it with import * as Date. I sometimes use this if I have several modules that are "plug-ins". I can import them with star syntax and get objects that adhere to the same interface. So there is already no difference between the Object and module concept. Only class is different.
I just wanted to point out that in typescript's ADT system, the types has no interface construct in the OOP sense even if typescripts calls its type declarations interface instead of type sometimes. They are just types. So in FP style, I think we should do what you describe. Export types and constructors at the module level. So the "interface" in the sense of exposing things is on the module (and this interface is unrelated to the type declarations done with interface keyword). I think this maps nicely to ES6 modules and exports. Wrapping things in another layer with a class just adds syntactic noise. ES6 requires you to have modules but you can skip the classes. We already have module and exports in ES6, why not use them for encapsulation?
Yes, you are absolutely correct, I want nominal typing in order to support exporting of opaque types.
Regarding why not use class for opaque types, I think I already addressed that, please see my list of reasons above.
I guess I could use the same type of arguments as Douglas Crockford in this video. Having two ways of doing the same thing is "clutter" in the language. We have two ways to encapsulate things, module and class. Considering FP style only we only need one way. We cannot get rid of module so class has to go. "Class does not spark joy" :-).
Mohamed Hegazy (@mhegazy) From the above discussion I think we can boil this proposal down to privacy within a module. #5228 seems to be about privacy within a package.
The core of this proposal is that you can declare a type within a module and specify that the properties of that type are only visible to code within that module. To clarify, when I mention "module" I'm talking about what is sometimes referred to as an "ES6 module".
I realised that nominal typing is not needed for opaque types and the notion of module level privacy probably fits better into what typescript already are doing.
So I made #15465 to better reflect the current state of this proposal.
The resulting JavaScript is completely valid and even indistinguishable when exported from a module.
I don't think this is true at all. Classes are very different from objects in JS, they are not indistinguishable to the consumer. For example:
To construct a class instance you use new. Objects are constructed via literals or are returned from constructor functions.
Classes does not live through a JSON.stringify(), JSON.parse() cycle but objects do. The consumer needs to apply special handling of classes for serialization.
EDIT: I ran through your class emulation example above with JSON.parse() and that actually works. So if you take special care you can get away with using class to emulate POJO. However that would require that we ban some parts of class syntax to not get into trouble. To me that is not very idiomatic JS, so I'm not convinced we should be doing that. The JS example I provided above is much more idiomatic IMO and also requires a lot less code than your example. Keep in mind that in JS object members are not public. In JS, what is public on an object is specified by docs, not types. This is why you are recommended to use the docs and not the code as reference when creating definition files for existing JS libs. So I still don't see why you are against having the type system enforce the docs of idiomatic JS. I would actually argue that having TS forcing you to write unidiomatic JS is worse than TS adding the "free-will" things you mention.
Keep in mind that in JS object members are not public. In JS, what is public on an object is specified by docs, not types.
Jonas Kello (@jonaskello) I actually disagree with that. It might not be true for every developer, but when I'm writing in JS I consider everything that I can access a part of the public interface, unless there is an obvious convention that it's not - e.g. underscores, reserved prefixed names, etc. In my experience that is the unwritten rule people use. That's why it's standard to rely on duck-typing, keys iteration, enumeration, augmentation and monkey patching.
I agree that writing type-safe code may alter the way you would have written it otherwise. But that's the name of the game. You reap benefits in return.
I could use Crockford's arguments as well. There is an already existing way to do what you ask for (albeit a workaround), so why add a new one? My stance is that with such "type level" hiding you are hurting JS users and creating discrepancy between JS and TS - not a good thing. If you care for compatibility either wait for an official native private or use conventions obvious for everyone (for which you wouldn't even need any changes to the type-system). TypeScript has already made deviations, lets keep them to a minimum.
Yes I definitely think we disagree on this. For me the big selling point of TS is that it is idiomatic JS with types to express your constraints. You seem to look at TS more like Dart, Elm or some other compile-to-JS language where you write things different than in JS and don't care about the emitted code. For me, being able to write idiomatic JS and having a tight relationship between what I write in TS and what is emitted in JS are the big selling points and the reason I use TS. That and the easy integration into existing JS libs. I'm not sure why you would choose to use TS over other compile-to-JS languages with the stance you are taking.
We also seem to disagree on what creates discrepancies between TS and JS. I think TS forcing you to write things different than in idiomatic JS causes discrepancy. You seem to think that having the TS type system express things that are not available as native constructs in JS, but instead are conventionally expressed in docs, are causing discrepancies.
Your use of Crockford's argument is certainly valid if we regard TS and JS as two disparate languages, but not so if we regard one as the superset of the other.
I guess discussing further with so different views on the basics above would probably not be constructive. Anyway, I enjoyed the discussion and your input is much appreciated. It helped shape my suggestion in #15465 and I will continue to think about your way of viewing this.
Activity
jingyu9575 commentedon Apr 27, 2017
date.year can be a function call, see Accessors.
jonaskello commentedon Apr 27, 2017
Yes that's a good point.
classis certainly the right way to achieve encapsulation if you are doing OO style programming. Howeverclassis not a good fit for FP style. In FP your building blocks are immutable types (data records) and pure functions. As Javascript is multi-paradigm (OO and FP) I think it would make sense for typescript to support FP style encapsulation too.Other than the fact that
classis a bad fit for FP, there are practical reasons why you would prefer pure data objects over classes. One being that they are easy to serialize to JSON. Another that they are easy to deep compare. Also duck typing works for data objects but not classes. They are also easy to merge etc.aluanhaddad commentedon Apr 27, 2017
Just FYI, accessors are also available for object literals
I guess my question is, if callers should not have been consuming
year, why was it exposed?jonaskello commentedon Apr 27, 2017
yearis exposed because I need to access it within the module and typescript does not allow any method of hiding it outside the module while still keeping it a plain immutable data record. Hence the feature request :-). For the purpose of FP style, I want to work with only plain immutable data records (types with constructors). There is a good example of how abstract types are used in the Ocaml docs I linked above. They are also mentioned here in a talk about reason which is an Ocaml dialect by facebook with syntax more similar to typescript.Could you elaborate on how I would use accessors with plain objects? How does the typing look for
Datein your example above and what would be the implementation in the{ ... }be? Would the accessors not be functions/methods? And would not the accessors be available to other modules, so everyone would know the shape of theDatetype?aluanhaddad commentedon Apr 27, 2017
Jonas Kello (@jonaskello) that is correct, the accessors would be methods. Such
getaccessors imply nothing about the structure of the underlying value and can simply return values from the enclosing lexical scope (no need forthis).This does mean that other modules would know the shape of
Datebut I am not sure how this can be avoided in a structural type system. Well Ocaml is structurally typed...jonaskello commentedon Apr 27, 2017
I think you have a good point about not being able to hide the structure in a structural type system. What is needed for abstract types would probably be more akin to nominal typing. However I don't think a full nominal type system is needed just to get abstract types?
I made an example which tries to emulate abstract types. As you can see it breaks down because of structural typing.
EDIT: I managed to update the example so it to some degree emulates nominal typing by using a string literal type as a tag. This may actually be good enough but still it would be nice to have proper support for abstract types.
date.ts
consumer.ts
jonaskello commentedon Apr 28, 2017
So I think one conclusion is that two abstract types would need to be considered different even if their structure is the same. This would probably create a simple nominal typing system. So in that sense it may be related to #202.
jonaskello commentedon Apr 28, 2017
I haven't used Elm myself but it seems to have an abstract type concept, only it is called opaque types. I also think Elm uses structural typing. I just mention it as an example of a language with both structural typing and abstract/opaque types.
jonaskello commentedon Apr 28, 2017
Calling the types "opaque" may be better than "abstract" since that is an overloaded term already considering abstract classes. Also considering the
distinctkeyword from this proposal for nominal typing, I would revise my syntax proposal for abstract/opaque types to:Or perhaps
distinctis already implied byopaqueand could be left out.falsandtru commentedon Apr 28, 2017
gcnew commentedon Apr 28, 2017
As falsandtru (@falsandtru) said, the trick is to use
private.The following pattern is a good out of the box emulation.
Date.ts
test.ts
Note that you don't use classes as OOP classes. You just use the visibility primitives they provide. Such a usage is more of a "namespace" or a module-like dictionary, thus I think it's perfectly inline with the functional style.
jonaskello commentedon Apr 28, 2017
falsandtru (@falsandtru) That's an interesting approach :-). However now we have two levels of encapsulation, the module level and the class level. I think for FP style we want the module to be the only level and not use
classat all (I actually banclasswith linting rules). Also the module level encapsulation we cannot git rid of so if one has to go it would have to be the class level.jonaskello commentedon Apr 28, 2017
Marin Marinov (@gcnew)
privatemight be a good idea, if it could be applied without usingclass, maybe something like this:gcnew commentedon Apr 28, 2017
Jonas Kello (@jonaskello) Why does
classbother you?falsandtru commentedon Apr 28, 2017
Private constructor and properties hide behaviors of classes.
11 remaining items
gcnew commentedon Apr 28, 2017
Don't get me wrong. I'm not an advocate of OOP, quite the opposite actually. I personally use classes only when they are the only option and I still think how I can avoid them. However the way currently JS works is not with hidden object state. TypeScript has made a deviation in classes and I would prefer that it doesn't do any more free-will choices (namespaces, enums, private, protected, three-slash references are already enough). On the other hand, mindfully using these features where appropriate is not a deadly sin.
On
readonly: #13002, #13347[-]Suggestion: Abstract types[/-][+]Suggestion: Abstract/Opaque types[/+]gcnew commentedon Apr 28, 2017
I don't see what the differences between ADTs and interfaces have to do with the problem at hand. In traditional languages both are nominal and both precisely state what the programmer can access. ADTs have the benefit that they may be one of several options, while interfaces describe a single shape. In the language that I'm familiar with - Haskell, if you export a data type and it's value constructors, you can pattern match on them any way you want.
Your ask seems to preclude two things - nominal types and a mechanism to export only the data type without its constructors (if we follow ADT's analogy).
Now, lets get a step back again. We can already achieve this behaviour with an alternative mechanism. Why not use it? After all, languages are different, the idea is what matters :)
PS: The module itself is an implicit static class. If we were able to make our static class the export object itself than there would be no difference between the two concepts.
jonaskello commentedon Apr 28, 2017
Ok, checked #13002, #13347 but still think
readonlyis really useful in practice, and those issues are more about splitting hairs :-). Imagine having to make all of your redux state objects asclassinstead of plain objects in order to make it immutable. That to me is syntactic noise.Regarding hidden state, I think JS objects neither hides or exposes their state as you don't know the type of the object. So for me, an object in JS has "undetermined" state which may be specified by comments/annotations.
Edit: The module is actually an implicit instance of an Object and can be typed as such if you import it with
import * as Date. I sometimes use this if I have several modules that are "plug-ins". I can import them with star syntax and get objects that adhere to the sameinterface. So there is already no difference between the Object and module concept. Onlyclassis different.jonaskello commentedon Apr 28, 2017
I just wanted to point out that in typescript's ADT system, the types has no interface construct in the OOP sense even if typescripts calls its type declarations
interfaceinstead oftypesometimes. They are just types. So in FP style, I think we should do what you describe. Export types and constructors at the module level. So the "interface" in the sense of exposing things is on the module (and this interface is unrelated to the type declarations done withinterfacekeyword). I think this maps nicely to ES6 modules and exports. Wrapping things in another layer with a class just adds syntactic noise. ES6 requires you to have modules but you can skip the classes. We already have module and exports in ES6, why not use them for encapsulation?Yes, you are absolutely correct, I want nominal typing in order to support exporting of opaque types.
Regarding why not use
classfor opaque types, I think I already addressed that, please see my list of reasons above.jonaskello commentedon Apr 28, 2017
I guess I could use the same type of arguments as Douglas Crockford in this video. Having two ways of doing the same thing is "clutter" in the language. We have two ways to encapsulate things, module and class. Considering FP style only we only need one way. We cannot get rid of module so class has to go. "Class does not spark joy" :-).
mhegazy commentedon Apr 28, 2017
So this proposal is basically #5228 for all types and not just classes?
jonaskello commentedon Apr 29, 2017
Mohamed Hegazy (@mhegazy) From the above discussion I think we can boil this proposal down to privacy within a module. #5228 seems to be about privacy within a package.
The core of this proposal is that you can declare a type within a module and specify that the properties of that type are only visible to code within that module. To clarify, when I mention "module" I'm talking about what is sometimes referred to as an "ES6 module".
jonaskello commentedon Apr 29, 2017
I realised that nominal typing is not needed for opaque types and the notion of module level privacy probably fits better into what typescript already are doing.
So I made #15465 to better reflect the current state of this proposal.
jonaskello commentedon Apr 29, 2017
Marin Marinov (@gcnew) from above regarding the class approach:
I don't think this is true at all. Classes are very different from objects in JS, they are not indistinguishable to the consumer. For example:
new. Objects are constructed via literals or are returned from constructor functions.JSON.stringify(),JSON.parse()cycle but objects do. The consumer needs to apply special handling of classes for serialization.EDIT: I ran through your
classemulation example above withJSON.parse()and that actually works. So if you take special care you can get away with usingclassto emulate POJO. However that would require that we ban some parts of class syntax to not get into trouble. To me that is not very idiomatic JS, so I'm not convinced we should be doing that. The JS example I provided above is much more idiomatic IMO and also requires a lot less code than your example. Keep in mind that in JS object members are not public. In JS, what is public on an object is specified by docs, not types. This is why you are recommended to use the docs and not the code as reference when creating definition files for existing JS libs. So I still don't see why you are against having the type system enforce the docs of idiomatic JS. I would actually argue that having TS forcing you to write unidiomatic JS is worse than TS adding the "free-will" things you mention.jonaskello commentedon Apr 29, 2017
Just to illustrate what I mean by unidiomatic JS, lets look at the
classworkaround example from above in plain JS:date.js
If we were to put that code into a JS codebase I think we would get more than one question about what we were thinking.
gcnew commentedon Apr 29, 2017
Jonas Kello (@jonaskello) I actually disagree with that. It might not be true for every developer, but when I'm writing in JS I consider everything that I can access a part of the public interface, unless there is an obvious convention that it's not - e.g. underscores, reserved prefixed names, etc. In my experience that is the unwritten rule people use. That's why it's standard to rely on duck-typing, keys iteration, enumeration, augmentation and monkey patching.
I agree that writing type-safe code may alter the way you would have written it otherwise. But that's the name of the game. You reap benefits in return.
I could use Crockford's arguments as well. There is an already existing way to do what you ask for (albeit a workaround), so why add a new one? My stance is that with such "type level" hiding you are hurting JS users and creating discrepancy between JS and TS - not a good thing. If you care for compatibility either wait for an official native private or use conventions obvious for everyone (for which you wouldn't even need any changes to the type-system). TypeScript has already made deviations, lets keep them to a minimum.
jonaskello commentedon Apr 29, 2017
Yes I definitely think we disagree on this. For me the big selling point of TS is that it is idiomatic JS with types to express your constraints. You seem to look at TS more like Dart, Elm or some other compile-to-JS language where you write things different than in JS and don't care about the emitted code. For me, being able to write idiomatic JS and having a tight relationship between what I write in TS and what is emitted in JS are the big selling points and the reason I use TS. That and the easy integration into existing JS libs. I'm not sure why you would choose to use TS over other compile-to-JS languages with the stance you are taking.
We also seem to disagree on what creates discrepancies between TS and JS. I think TS forcing you to write things different than in idiomatic JS causes discrepancy. You seem to think that having the TS type system express things that are not available as native constructs in JS, but instead are conventionally expressed in docs, are causing discrepancies.
Your use of Crockford's argument is certainly valid if we regard TS and JS as two disparate languages, but not so if we regard one as the superset of the other.
I guess discussing further with so different views on the basics above would probably not be constructive. Anyway, I enjoyed the discussion and your input is much appreciated. It helped shape my suggestion in #15465 and I will continue to think about your way of viewing this.