niieani / typescript-vs-flowtype

Differences between Flowtype and TypeScript -- syntax and usability
MIT License
1.73k stars 78 forks source link

Write more about Object definition differences for the same code #8

Open niieani opened 7 years ago

niieani commented 7 years ago

[DRAFT]

In TS you don't get "loose" properties on your objects, since you can't assign more than your definition expects:

/* @flow */
type NonExact = {}
const a : NonExact = { extraProp: '123' } // error in TS, error in Flow if: type Exact = $Exact<{}>

TS's above object's-exact-by-default stance might get you to design better APIs, unless you want to go and slap $Exact everywhere in your Flow code. This doesn't mean Flow is less-type-safe, it's just that this default is less safe than in TS. There are other cases where the opposite is true (TS's defaults being weaker-typed) then Flow.

vkurchatkin commented 7 years ago

just that this default is less safe than in TS

It's not less safe, it provides exactly the same amount of safety. There is nothing unsafe in having extra properties

niieani commented 7 years ago

There is nothing unsafe in having extra properties.

I have to disagree here.

export type Car = {
  brand: string,
  color?: 'red' | 'blue',
}
const car : Car = { brand: 'Tesla', colour: 'red' }

In TypeScript you'd spot the typo immediately (color => colour), therefore I maintain that expecting types to be exact is a more type-safe behavior. You can do that in Flow (with {| ... |} syntax), but for this specific feature, Flow is less safe by default than TypeScript.

Another example where it is less safe:

function storeInDatabase(car: Car) {
  db.insert(car)
  // I can be sure that 'car'
  // has only the expected properties
  // nothing more, nothing less
  // since nothing can ever be added by mistake
}

storeInDatabase({ brand: 'Tesla', color: 'red', thing: 123 })
vkurchatkin commented 7 years ago

Right, this is a typo, but it's unsafe, it doesn't lead to runtime errors.

I maintain that expecting types to be exact is a more type-safe behavior.

Once again, objects are not exact in Typescript. Exact means that it is guaranteed that object doesn't have properties other than declared. This is a unique feature of Flow.

Typescript catches additional properties when "fresh" object is assigned to annotated variable or passed to a function directly. Other than that it allows additional properties and doesn't guarantee anything.

Using your example:

type Car = {
  brand: string,
  color: string,
}

function storeInDatabase(car: Car) {
  db.insert(car)
  // I can be sure that 'car'
  // has only the expected properties
  // or can I?
}

const car = { brand: 'Tesla', color: 'red', thing: 123 };
storeInDatabase(car);
niieani commented 7 years ago

@vkurchatkin ok, that clears it up for me. Many thanks.

I don't think 'no runtime errors' should be the end-goal though. I think type safety, like strictness of definitions (and not allowing additional properties) could save us headaches in many cases like those cited above. Given that, it's a shame Flow doesn't use the safer, strict / closed types by default, and only allows to have "open" types when explicitly defined.

TypeScript default is still a tiny bit better than the Flow one, so in order from best safety to worst:

  1. best: use Flow's exact object types: {| ... |}
  2. good: use TypeScript's default object types (catches a few less errors than Flow's exact, but a few more than Flow's default)
  3. ok: Flow's default object types
vkurchatkin commented 7 years ago

Preventing typos is non-goal for Flow, but it is for Typescript. "No runtime errors" is the definitions of safety. Exact types are useful only in a limited amount of cases, because they are not subtype-able. Saying that that exact objects are just better is the same as saying that not having subtyping is better.

niieani commented 7 years ago

@vkurchatkin I get that Flow's definition of safety is "no runtime errors", but I don't think that covers it. To me, typos are just as part of programming safety, as are extraneous properties - and if we're being specific, both of those can cause runtime errors.

E.g. to reference the previous case: a database driver throwing an error after being passed an object with an extraneous, unexpected field present. Definitely not an edge case, and it classifies as a runtime error, no? Unless we use the comfortable definition of "only built-in errors", which is, to be honest kinda cheating.

Couldn't exact types could work with subtyping, if one could use destructuring syntax to extend types, and/or if you could annotate a non-exact version of the object for explicit non-exact use?

vkurchatkin commented 7 years ago

You confuse safety with correctness.

both of those can cause runtime errors

No, they cannot. That's the point.

a database driver throwing an error after being passed an object with an extraneous, unexpected field present.

Well, first of all, this is actually covered but Flow's exact type. Secondly, there is no way to prevent a library from throwing an error if it wants to. You can't encode every possible runtime check as a type.

Couldn't exact types could work with subtyping, if one could use destructuring syntax to extend types, and/or if you could annotate a non-exact version of the object for explicit non-exact use?

Not sure what you mean

RyanCavanaugh commented 7 years ago

Perhaps there's a useful row in the initial table based on this discussion

TypeScript Flow
Design Goal Identify errors in programs Enforce type safety
niieani commented 7 years ago

@RyanCavanaugh I agree, I'll add this. Thanks.