Closed niieani closed 6 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
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.
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?
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.
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
.
@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.
What I'm saying is, that if you are going to write unsafe code you can just use any
, no other features are required.
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.
const result: T = await fetch(
/api/${userId}/${type})
is obviously unsafe, because fetch returns Promise<any>
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
.
What I mean is that there is not difference between const x = get<Foo>()
and const x: Foo = get()
.
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>()
.
I see what you mean. Indeed, in this case this feature saves some typing, but that's about it.
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.
this has landed on Flow 72, we can close this
That's true! Would you like to make a quick PR with the update to readme @sibelius?
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.