niieani / typescript-vs-flowtype

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

Expand on the generic function usage possible in TypeScript, but not in Flow #10

Closed niieani closed 6 years ago

niieani commented 7 years ago

TypeScript allows for more powerful usage of generics by defining type instance during function invocation. To my knowledge, [this is not possible](http://www.typescriptlang.org/play/#src=function%20someFactory%3CT%3E()%20%7B%0D%0A%20%20return%20class%20%7B%0D%0A%20%20%20%20method(someParam%20%3A%20T)%20%7B%0D%0A%20%20%20%20%20%20%0D%0A%20%20%20%20%7D%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0A%2F%2F%20how%20to%20invoke%20this%20factory%20with%20a%20defined%20%3CT%3E%3F%0D%0A%0D%0Aconst%20SomeClass%20%3D%20someFactory%3C%7B%20whatever%3A%20string%20%7D%3E()%0D%0Aconst%20someInstance%20%3D%20new%20SomeClass()%0D%0AsomeInstance.method('will-error-here')%0D%0A) with Flow, as it always needs to infer the generic types from usage, although I'd love to be proven wrong here.

vkurchatkin commented 7 years ago

This is correct, but this feature is planned in Flow. It's important to mention that this feature doesn't affect safety, on the contrary it enables some unsafe patterns

niieani commented 7 years ago

Great to hear that it is planned. Do you have a link to a related issue in the flow repo?

I'd also love some clarification on why you think it enables unsafe patterns? Example(s) would be welcome.

In any case, in TS it does enable some patterns, especially related to working with closures, which are much more flexible than when using Flow.

vkurchatkin commented 7 years ago

This: https://github.com/facebook/flow/issues/2630

I'd also love some clarification on why you think it enables unsafe patterns? Example(s) would be welcome.

It allows you to write something like this:

const foo = get<Foo>();
foo.doSomething();

This obviously can never be safe because you can't return Foo at runtime if you don't get it as argument. On the other hand, if you provide Foo as argument, you don't have to specify it explicitly, inference takes care of it.

specially related to working with closures, which are much more flexible than when using Flow

Can you give an example?

niieani commented 7 years ago

I still don't get how it "allows you" to write something like:

const foo = get<Foo>();
foo.doSomething();

What's the definition of get? If it's:

function get<T>(): T {
  return // whatever
}

TypeScript won't allow it, until you're really returning T. And since there is no way for TS to know what T is (cannot be inferred), that function will simply always be an invalid function, and you'll always get the error:

Type 'whateverYouReturned' is not assignable to type 'T'.


if you provide Foo as argument, you don't have to specify it explicitly, inference takes care of it.

The same is true for TypeScript.

As for patterns that are enabled by this, take a look at this example:

interface ReportTotals {
  sum: number;
  difference: number;
}

interface ReportRow {
  value: number;
  date: Date;
}

function makeReportFetcher<T>(type: 'totals' | 'row') {
  return async function getReport(userId: number) {
    const result: T = await fetch(`/api/${userId}/${type}`)
    return result
  }
}

const getTotalsReport = makeReportFetcher<ReportTotals>('totals')

// we can now infer type of 'totalsResult'
// without having to always re-define it manually
const totalsResult = getTotalsReport(123)

While obviously this is a bit contrived, if you use this sort of pattern with more generics and more complex types, you'll get to the point where explicitly re-declaring the type during usage, while possible...:

const getTotalsReport: (userId: number) => Promise<ReportTotals> = makeReportFetcher('totals')

...will get very tedious with complex types.

vkurchatkin commented 7 years ago

The same is true for TypeScript.

Not really, sometimes inference in TS doesn't work which leads to unsafe behavior.

Your example is a pretty good demonstration of my point: it's unsafe. You might as well return any.

niieani commented 7 years ago

@vkurchatkin I'm not sure what you mean by your last remark. Generally, touching the external world is unsafe in any language. Parsing a JSON string into an Object is "unsafe", but has to happen at some point. This is a very similar scenario. We don't want to be safe within the closed world of our type-system, but safe within the world of our assumptions. My code is only going to be as good as my assumptions about the types returned at any given bridge between my program and the externals.

I have to assume the fetch() will return an object of type T, but I cannot be sure, thus neither Flow, nor TypeScript would help me there, unless I used some sort of runtime checking. Given that, I'd still like to know if the rest of the code is internally sound, so it makes total sense to keep type checking that, at least within the confined set of my assumptions, and ensure it is as sound as possible.

vkurchatkin commented 7 years ago

What I'm saying is, that if you are going to write unsafe code you can just use any, no other features are required.

niieani commented 7 years ago

But why would I use any anywhere here? Which is the unsafe part?

makeReportFetcher is unsafe in a vacuum, but can be safe if its return value is ever used. Imagine simply replacing the function's contents and <T> with an explicit definition for each case of T. They're both 100% safe. All that this factory does is create a D-R-Y shortcut for generating the two, type-safe methods.

vkurchatkin commented 7 years ago

const result: T = await fetch(/api/${userId}/${type}) is obviously unsafe, because fetch returns Promise<any>

niieani commented 7 years ago

My point is, we have to make assumptions about the external world at some point. fetch is unsafe only if we cannot predict its value. If we know what a certain URL returns, we can, with certain predictability, assume that await fetch(...) returns a given type T. Applications aren't developed in a vacuum, but with a set of assumptions, so we need to be able to extend the language (and the type system) with the assumptions about the external world.

In TypeScript we can do await fetch<{ hello: string }>('...'), and that's partially the case. The assumption is explicit, but possible.

But I'll try to think of a different example for the above argument, which doesn't involve any.

vkurchatkin commented 7 years ago

What I mean is that there is not difference between const x = get<Foo>() and const x: Foo = get().

niieani commented 7 years ago

The difference is that in the first case, the type passed in can be deeply transformed before being returned (the example you gave is contrived and you'd almost never really do that).

For example:

const x = get<Foo>()

and here the type of x could even be a whole module, with 10 functions, 3 classes and 5 variables, all of which operate on the Foo type in same way. In order to do it in Flow, you'd have to duplicate the types of all the 10 functions, 3 classes and 5 variables as explicit Types and then annotate the type of x:

const x: {
  someMethod: () => Array<T>,
  SomeClass : //...,
  someVar: Array<Map<T, $ObjMap<T, ()=>string>>>,
  anotherVar: Set<$Shape<T>>,
  // and so on, and so forth
} = get()

Even if you made a generic type which returns the type of x<T>, you'd still have to write it out manually, instead of have it inferred from the result of get<T>().

vkurchatkin commented 7 years ago

I see what you mean. Indeed, in this case this feature saves some typing, but that's about it.

RyanCavanaugh commented 7 years ago

I usually argue on the side of @vkurchatkin here and tried to get a lint rule to this effect added to DefinitelyTyped. People write

const x = fn<Foo>(bar);

when they should write this, which is the same number of characters (in TS syntax)

const x = <Foo>fn(bar);

But as soon as the T gets nested any deeper than the top-level, then you have less safety because you're potentially writing a large anonymous type, which is more error-prone. Anything that's making you re-write a type where you could have parameterized it at the function declaration site is going to be more dangerous.

Maybe I'll push in a rule to DefinitelyTyped that you can't make a fake generic type parameter T if it's the top-level return type.

TL;DR if you're going to force people to assert a type to "add in" external knowledge, allow them to write the smallest possible type that provides that knowledge.

sibelius commented 6 years ago

this has landed on Flow 72, we can close this

niieani commented 6 years ago

That's true! Would you like to make a quick PR with the update to readme @sibelius?