Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript interface support #72

Closed
fdecampredon opened this issue Nov 20, 2014 · 27 comments
Closed

TypeScript interface support #72

fdecampredon opened this issue Nov 20, 2014 · 27 comments

Comments

@fdecampredon
Copy link

Flow seems to have a limited interface support, no implements (and not supported in declaration files).

Would mimicking typescript interface support seems reasonable ?

@avikchaudhuri
Copy link
Contributor

We'll (have to) have at least equivalent functionality, since we're going to try converting .d.ts files to Flow declaration files. That said, TypeScript interface syntax is quite fat, trying to kill too many birds with one stone. Their functionality can be decomposed into (a) type aliases (b) declare classes, both of which we have, and will augment however is needed to model different kinds of declarations elegantly.

@Davidhanson90
Copy link

Can you elaborate on this

"That said, TypeScript interface syntax is quite fat"

@avikchaudhuri
Copy link
Contributor

I mean TypeScript interfaces model, at the same time, objects, function-like things, constructors, classes, array-like things, ...by "syntax is quite fat" I mean "trying to do too many things at once, thus too many productions from that non-terminal".

In our experience, these features can be nicely factored out. Functions should be functions, arrays should be arrays, objects should be objects (yes, everything is internally an object, but the type language doesn't need to model that). What remains are what are internally things that behave like classes (by being constructors, holder of instance properties, holder of static properties, holder of a prototype, holder of an indexable, and so on...). For that, we have declare class. So basically we're trying to limit declare class to class-like things, which can be sufficiently general, while not polluting the syntax of all object types with that generality.

@laser
Copy link

laser commented Mar 25, 2015

For those of you who ended up here while searching for "Facebook Flow interfaces," perhaps a simple example would be good:

class User {
  n: number;
  constructor(n) {
    this.n = n;
  }
}

class CustomError {
  message: string;
  constructor(m) {
    this.message = m;
  }
}

type NodeCallback<E, V> = (err: ?E, value: ?V) => void;

// Defining an interface
//
type UserDAOIFace = {
  getUserById: (x: number, callback: NodeCallback<CustomError, User>) => void;
}

// Implementing an interface
// 
var userDAOImpl: UserDAOIFace = {
  getUserById(id, callback) {
    if (id === 555) {
      callback(new CustomError("User not found!"), null);
    }
    else {
      callback(null, new User(id));
    }
  }
}

// A second implementation
// 
var fakeUserDAOImpl: UserDAOIFace = {
  getUserById(id, callback) {
    callback(null, new User(123));
  }
}

Hope that helps, and, I hope that my example isn't too stupid.

Erin

@0xR
Copy link

0xR commented Jun 14, 2015

+1 to this. It would really help to have type interfaces for existing libraries like lodash, underscorejs, angular, ember or whatever. Right now getting started with an existing codebase mostly requires writing long interface files. Because of that adopting flowtype might be more work than it avoids by finding bugs earlier.

Of course making proper flow interfaces from .d.ts files would require some manual work because typescript doesn't support non-nullable types. Therefore a conversion to a flow interface would allow it to be updated manually to properly fit FlowType.

@samwgoldman
Copy link
Member

Merging with #7

@karelbilek
Copy link
Contributor

I am sorry to write it here, but I have no idea where to write it elsewhere.

It seems to me flow now has an interface keyword and it does something. I am just not sure what and how.

Is it documented anywhere?

Google returns me either my own issue #544, or things about the interface files, but that's not what I want. I want to know what declare interface does. (as seen here, for example)

edit: some limited test seems to be here

https://github.com/facebook/flow/blob/master/tests/traits/test2.js

but, well, it's not that helpful :D also I have no idea what mixins really does (and again can't find it anywhere)

@GGAlanSmithee
Copy link

@Runn1ng I don't think there is much documentation of interface declarations. See #833 for some discussion on the subject.

@karelbilek
Copy link
Contributor

OK, I think I slightly understand what interface does. I am still not sure what mixins does and how it relates to interface.

@vaukalak
Copy link

@Runn1ng if you do, please explain what is advantage of interfaces compared to types: https://gist.github.com/vaukalak/64be636be0b09181eb31

@amakhrov
Copy link

amakhrov commented Feb 1, 2016

@vaukalak in general the main difference between interface and type in languages without multiple inheritance is that a class can be a subtype of only one type, but can implement multiple interfaces.
The advantage only appears if we're able to explicitly declare that a class implements some interface(s). As far as I can see, it's not supported by Flow yet, hence, there is no advantage in using interfaces so far.

@vaukalak
Copy link

vaukalak commented Feb 1, 2016

@amakhrov actually you can implicitly implement a type, and as I understand, there is no difference in how many types you do implement. the extension can be used in classes only.

@estaub
Copy link

estaub commented Feb 17, 2016

The absence (afaict) of available interface files for third-party projects makes flow a non-starter for me; I primarily need it for enforcing correctness in external interfaces. I suspect I'm far from alone. So if there's any way to make .d.ts work, maybe it would enable a lot more Flow adoption. (I'm not disagreeing with the technical issues.)

@liquidboy
Copy link

+1 to @estaub ... I began down the path of implementing FlowType on my work project BUT barrier after barrier and then this issue with interfaces and lack of support of d.ts was the last straw ..
I pulled it out and went back to implementing TS (as I have with my other work projects) ..

p.s. this idea that TS's implementation is "Fat" is moot in my opinion, given its all compile time anyway..

@neonsquare
Copy link

@liquidboy
It is "conceptually" fat. Flow already supports much of what interfaces provide by simply using a type alias on an object type. Remember that type aliasses where originally very crippled in TypeScript - so the same would not have been possible there - even with object type literals.

From a structural typing point there is not really much of a difference between an interface or an aliassed (named) object type. There are some obvious syntactic differences though: Defining methods in interfaces is arguable easier than function types in object types. It certainly allows for easier converting of TypeScript declarations. You can also "extend" interfaces:

interface A {
foo();
}

interface B extends A {
bar();
}

var b : B = { foo() {}, bar() {}}

To do the same with object types would need a union type.

@cie
Copy link

cie commented Jun 10, 2016

For those who came here by searching for "interfaces in flowtype", let me elaborate on laser's great comment above (thanks laser, you helped me out with it!).

If you want to have a class that (sort of) implements a (sort of) interface, you can do the following:

type UserDAOIFace = {
    ...
}

/*:: (x: UserDAOImpl) => (x: UserDAOIface) */
class UserDAOImpl = {
  ...
}

The comment in the middle is a hack that does the same as what you would expect class UserDAOImpl implements UserDAOIFace do, that is, check if this class is a subtype of the interface type (has the same methods with compatible signatures).

Technically, this is a Flow Comment (one that Flow parses but the compiler ignores; make sure you have the double colon at the front). In the flow comment there is an expression that evaluates to an arrow function, with an argument of type UserDAOImpl, and in the body a Flow typecheck checking that x is of type UserDAOIface.

Conceptually this means that all variables of type UserDAOImpl must be of type UserDAOIface. You can read the expression as "x is UserDAOImpl implies that x is UserDAOIface", abusing the fact that => means implication. (In fact, this technique has a real use in Agda:)

You can even use the same expression without putting it into a comment (however, I personally prefer it with the comment):

(x: UserDAOImpl) => (x: UserDAOIface)
class UserDAOImpl = {
  ...
}

Or you can equally put it at the end:

class UserDAOImpl = {
  ...
}
(x: UserDAOImpl) => (x: UserDAOIface)

@jednano
Copy link

jednano commented Jan 19, 2017

@neonsquare, unfortunately you can't union or intersect exact object types together. In TypeScript, all interfaces are exact by default. Flow is backwards if you ask me, because now I'm forced to put pipes in 99% of my types (i.e., {|...|}). Even @cpojer agrees in his tweet that exact object types "should be your new default." If only there were a .flowconfig option to make it so – there's not.

Let me give you a quick and dirty example of a simple Point type in Flow (an exact type):

type Point = {|
    x: number,
    y: number
|};

That works out pretty well, until you want to extend it in any way whatsoever (#2626).

Coming from TypeScript, I'm finding myself extremely limited by what Flow has to offer. Let's take the Point example above and try to extend it in TypeScript. I'm going to make a 3D Point and add a Named interface to show you how you can extend multiple interfaces with ease.

interface Point {
    x: number;
    y: number;
}

interface Point3D extends Point {
    z: number;
}

interface Named {
    name: string;
}

interface NamedPoint extends Named, Point { }
interface NamedPoint3D extends Named, Point3D { }

const point: NamedPoint3D = {
    name: 'target',
    x: 50,
    y: 100,
    z: -25
}; 

Try doing the above in Flow w/o pulling your hair out, because it's not even possible (#2626).

Even #1326 wouldn't make me happy, because I expect to see errors when you extend/intersect incompatible interfaces/types.

Fortunately, in TypeScript, you can mimic Flow's default behavior with Excess Property Checks. Here's the syntax:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;  // <-- exact type becomes flexible (the Flow default)
}

Unfortunately, in Flow, you can't mimic TypeScript behavior in reverse.

I should probably create a host of issues here on Flow, but from the existing issues I've read, I'm wondering if it's just not in the design goals of Flow to meet these requirements.

Edit: Just did 👇

@Intregrisist
Copy link

How do I upvote @jedmao's comment with the power of a million votes.

@vkurchatkin
Copy link
Contributor

In TypeScript, all interfaces are exact by default.

That is not true, interfaces has the same semantics in TypeScript.

@jednano
Copy link

jednano commented Jan 19, 2017

@vkurchatkin, my statement holds true. In TypeScript (not Flow), all interfaces are exact by default (i.e., they do not accept excess properties).

I realize Flow has interface declarations; though, the implementation is immature. To support that statement, the documentation contains only 46 words to describe what it can do, which leads me to believe it can only do that one thing. Anything else is undocumented. In contrast, TypeScript's interface documentation has 2825 words in it. That's 61 times the documentation that Flow has!

Unfortunately, Flow's interfaces also don't do enough to solve the problems I've stated above (I checked), so until the interface support is more mature, I can't really compare it to a TypeScript interface, so that's not what I'm talking about at all.

If I had to translate Flow's object type into TypeScript today, I would write an interface. There is no 1:1 relationship, but that's what I would write. In other words, TypeScript's counterpart to a Flow object type is an interface. This is based on functionality, not grammar. Flow's counterpart to a TypeScript interface, however, would be more accurately described as the "exact" object type, as it uses Excess Property Checks to prevent someone from attempting to supply an unsupported property, because "it's probably an error."

Clear as mud?

@vkurchatkin
Copy link
Contributor

No, interfaces in TypeScript ar not exact. "Exact" means that if value has type T, it is guaranteed to not have any properties than those of T. There is no way to achieve this in TypeScript.

What you are talking about is that in some contexts TypeScript shows linter-level errors that has nothing to do with type system. There are no linter-level errors in Flow yet.

@jednano
Copy link

jednano commented Jan 19, 2017

As a user, why do I care "how" a type or interface guarantees exactness?

And how would a type system guarantee anything without throwing errors?

Call it what you want. If I have a TypeScript interface, TypeScript guarantees exactness via linter-level errors, does it not?

What do you mean "in some contexts"? In my experience, every context of an interface behaves this way.

@vkurchatkin
Copy link
Contributor

Call it what you want. If I have a TypeScript interface, TypeScript guarantees exactness via linter-level errors, does it not?

No it doesn't.

interface Point {
    x: number;
    y: number;
};

const a: { x: number; y: number; z: number; } = { x: 5, y: 5, z: 5 };
const b: Point = a;

In this example b has type Point, but it has properties other than x and y.

This is an error in TS indeed:

interface Point {
    x: number;
    y: number;
};

const a: Point = { x: 5, y: 5, z: 5 };

TypeScript prints the following:

Type '{ x: number; y: number; z: number; }' is not assignable to type 'Point'.

That is obviously incorrect, as in the first example we did just that.

Actually, TypeScript is probably right that this is probably a mistake. The only reference to this object doesn't know about z so it will be never be accessed. This is on the same level as saying that x === x is probably a mistake or unused property is probably a mistake, etc. These are what linters and other static analysis tools are for. It has nothing to do with typechecking.

@jednano
Copy link

jednano commented Jan 19, 2017

@vkurchatkin thanks for the concrete example. You are absolutely correct about TypeScript "not" guaranteeing exactness in your example; however, it can be fixed like so:

const a = <Point>{ x: 5, y: 5, z: 5 };

It's a bit confusing that TS doesn't treat const b: Point = a; the same way. I'll give you that.

That said, can you explain to me how anything can be guaranteed in either type system? Isn't throwing errors the only way to guarantee anything? You keep pointing out that linter-level errors have nothing to do with a type system, but why should I care about the mechanism behind the error that guarantees exactness?

@vkurchatkin
Copy link
Contributor

What I'm saying is that you shouldn't call this "exactness". It's not the same thing. It's a valid feature request, but making objects exact by default is not what you want.

@jednano
Copy link

jednano commented Jan 19, 2017

I think in the context of Flow, exactness "is" what I want, because I want to intersect exact object types into a new exact object type, same as #2626.

In the context of TypeScript, I also prefer exactness, so that would be a separate feature request, but it's not currently how things work.

@cpojer
Copy link
Contributor

cpojer commented Jan 21, 2017

Hey, I just wanna add some perspective here. I'm not on the flow team nor am I a type system expert (very far from it). Please don't use what I say as an authority to apply pressure on the flow team. That wasn't my intention and I'm sure they have reasons for their design decisions :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests