The main difference is that we don't type the return value, because is inferred.
I'm fully aware that inferred function return types are considered idiomatic in TypeScript. In the end though I think that simply reflects an attitude where convenience wins over type safety (i.e. the popularity of TypeScript + VS Code is much less about type safety but much more about convenience).
Whether to include return type annotations for functions and methods is up to the code author. Reviewers may ask for annotations to clarify complex return types that are hard to understand. Projects may have a local policy to always require return types, but this is not a general TypeScript style requirement.
There are two benefits to explicitly typing out the implicit return values of functions and methods:
More precise documentation to benefit readers of the code.
Surface potential type errors faster in the future if there are code changes that change the return type of the function.
One minor point is the fact that functions have a type. One of the more infuriating things is that you cannot type a function declaration with a function type - one has to use arrow functions for that. So by not explicitly specifying the return type half of the function's type is left unspecified/implicit. Sure, an IDE will be able to show that type but that means that now even code reviews have to be conducted in the presence of "assistive technologies" (as the explicit code simply doesn't tell the whole story).
The major point however:
A Gentle Introduction to haskell Version 98 - 2.2.1 Recursive Types
"For example, suppose we wish to define a function fringe that returns a list of all the elements in the leaves of a tree from left to right. It's usually helpful to write down the type of new functions first"
The return type of a function is part of its design, its contract so it makes sense to explicitly determine and state it even before the body is implemented. So if one is already thinking in types and is perhaps even practicing type driven development then it makes sense to explicitly capture the types in play, perhaps even adding "more boilerplate" by adding type aliases that give the types more descriptive and meaningful names.
An implicit return type projects an attitude that the return type is merely an accidental byproduct of the function's implementation - while types are mandatory for the inputs (arguments);
// Inferred return type is ("100" | 1)// Is that the intent?// A union of a string literal and a numeric literal?//constfn=(arg:number)=>arg>50?'100':1;
It's one thing to rely on type inference within the confines of a function body - but crossing the function (or method) boundary is significant enough to warrant an explicit type check/lint (even for one liners) in order to catch problems as close to the source as possible.
Does that mean more typing on the keyboard? Sure; not enough though to affect productivity.
Interestingly Effective Typescript has "Item 19: Avoid Cluttering Your Code with Inferable Types" (p.81) which unsurprisingly states:
"The explicit type annotation is redundant. Writing it just adds noise. If you’re unsure of the type, you can check it in your editor."
Only to later clarify (p.85):
"Similar considerations apply to a function’s return type. You may still want to annotate this even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function."
The item is summarized (p.87) with:
Avoid writing type annotations when TypeScript can infer the same type.
Ideally your code has type annotations in function/method signatures but not on local variables in their bodies.
Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
TypeScript + VS Code is much less about type safety but much more about convenience
TypeScript is about convenience. If it gets in the way instead of helping, you might be using it wrong. I've been using TypeScript since it first became public (almost 10 years ago), and my advice when using TS is generally always the same: Just use it when it can't figure out types by itself, or when it really helps readability. Using types just for the sake of using them is not that useful, because the idea of TS is that is just "JavaScript with types on top". Like using JSDocs directly in the language.
An implicit return type projects an attitude that the return type is merely an accidental byproduct of the function's implementation - while types are mandatory for the inputs (arguments);
I beg to differ. Inferring the return type of a function is not an "accidental byproduct", but actually the result of your arguments. Depending on what you do with those arguments, the type of the return. You can be explicit about the type if it helps readability or if you want to make sure you get an error if the operation you do in that function doesn't return what you expect, but it isn't a requirement. You can also create a type for the entire function in a "haskell like" kind of a approach:
But the value I see in inferred return types is that you can change the implementation and get the correct "new type" without having to go and change the output type every time you do this. And you still get all the type safety of TS, so if you were returning a number and now you return a string, you'll get the expected errors in all the places were you are using the output of that function as a number.
Your example looks like this:
// Inferred return type is ("100" | 1)// Is that the intent?// A union of a string literal and a numeric literal?constfn=(arg:number)=>(arg>50?"100":1);
It looks like the intent of that code is to take a number argument and return "100" or 1, so yes, that the correct type for its output. This will work in places were we expect number | string, so no need to type the output like that.
Does that mean more typing on the keyboard? Sure; not enough though to affect productivity.
Trust me, my advice has nothing to do with "typing less". I cringe when I see that folks use generics like T, or identifiers like e, so no, is not to type less. As I pointed out before, my advices is to use TS when actually needed, when it can't figure out stuff by itself. This way you're effectively doing "JS with types" instead of writing a plethora of types just to write "more TS".
Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
We have other options for that as well, you can for example make use of as const, and typeof, ReturnType and many other utils to infer types from static values.
constexample1=()=>[1,2];// Return type is `number[]`constexample2=()=>[1,2]asconst;// Return type is `readonly [1, 2]`consttuple=example2();constexample3=(tupleArgument:typeoftuple)=>tuple;// tupleArgument type is `readonly [1, 2]`constexample4=(tupleArgument:ReturnType<typeofexample2>)=>tuple;// tupleArgument type is `readonly [1, 2]`
I agree with you that we need to type the return when it helps readability, but when it doesn't, using the inferred type is more than ok and will help your code stay close the the JS it outputs, while still being type safe.
Inferring the return type of a function is not an "accidental byproduct", but actually the result of your arguments. Depending on what you do with those arguments, the type of the return.
the value I see in inferred return types is that you can change the implementation and get the correct "new type" without having to go and change the output type every time you do this.
So again the return type is a consequence of the implementation.
When you "think in/develop with types" the implementation is a consequence of the desired type - i.e. the relationship it flipped.
No error that the result from fn is no longer sufficient. If we are lucky somewhere the result will be delivered to an explicitly typed binding, alerting us to the problem with the function (or any others that produce a similar result).
With an explicit return type compilation will identify all the functions that no longer produce the correct type.
typeMyType={count:number;average:number;total:number;};functionfn(values:number[]):MyType{constcount=values.length;constaverage=count>0?values.reduce((sum,value)=>sum+value,0)/count:NaN;// Property 'total' is missing in type '{ count: number; average: number; }' but required in type 'MyType'.return{count,average,};}
i.e. the type is changed before the implementation(s).
A capability in terms of value-orientation:
What is the shape of the data I need
What is that shape of the data I have
How do I transform what I have into what I need.
i.e. contracts (constraints) first, implementation last.
It looks like the intent of that code is to take a number argument and return "100" or 1, so yes, that the correct type for its output.
My intent was to simulate a defect (which perhaps would only be detected at an explicitly typed site of use), i.e. the intended return type should either have been string or number - not string | number.
This way you're effectively doing "JS with types" instead of writing a plethora of types just to write "more TS".
JS Doc TS with (hand written) declaration files is "Typed JS". There's a clean separation between "type space" (TypeScript types) and "value space" (the JS implementation).
The entire point of the index.d.ts and internal.d.ts files is to explicitly formulate a constrained set of types that are expected to be encountered (without having to formulate them "ad hoc").
The problem is that using JS Doc TS well (as far as I can tell) requires a higher level of TS competence (especially its "typing language") than writing TS destined for transpilation.
and many other utils to infer types from static values.
Those features can help to cut down on verbosity and emerge as TS tries to tease whatever type information it can out of the JS value space to make it available in type space. But in terms of design, types are typically formulated before the implementations and defined in type space (rather than extracted from value space).
Type space derived from value space is like drafting a blueprint after the house has already been built.
I think your approach is heavily inspired by how folks solved stuff with classes. What I mean by this is that you think that types are like the design/blueprint of how everything should look like, way before you even start working on the implementation. Meanwhile, I prefer to use types in TypeScript as a tool to know what the type of something will be while I work on it, not before.
Considering your scenario, you could get that MyType from the function if you want:
So if you change the implementation, the type adjusts to that. Basically, you make TS work for JS instead of the other way around. With this approach, let's say we have other two functions that take the output of fn as input:
With your approach, if let's say count changes and now is a string instead of a number, we need to first change the type in MyType, then change the implementation in fn, then resolve the issues in doubleCount. With my approach you only change the implementation, and then get errors were is relevant (like doubleCount). No need to update types.
My intent was to simulate a defect (which perhaps would only be detected at an explicitly typed site of use), i.e. the intended return type should either have been string or number - not string | number.
If you want your function to return one or the other, and you're returning both, you'll know it as soon as you try to use it in a place where you're expecting a single type and you get both:
constfn=(arg:number)=>(arg>50?"100":1);constdouble=(number:number)=>number*2;double(fn(10));// Error here: Argument of type 'string | number' is not assignable to parameter of type 'number'
The difference is that now I can figure out if I want fn to return a number or if I want to handle the case where fn returns a string:
But in terms of design, types are typically formulated before the implementations and defined in type space (rather than extracted from value space).
I agree to a certain degree, but as I stated before, the types you "design first" should be there just to help out with the JS part, not for the sake of types. Generally, you need to type arguments to let others and yourself in the future know what a function takes, but the output should be defined by the implementation. You don't lose any safety and you still gain lots of flexibility.
Type space derived from value space is like drafting a blueprint after the house has already been built.
With this I disagree again, I feel a closer analogy would be that types derived from implementation are like having a map of your code, that updates automatically as you change it. If a mountain comes out of nowhere, you'll know. From my point of view, the usefulness of TypeScript comes when you augment JavaScript with it, not when you have to code in JavaScript to accommodate TypeScript types.
I understand that for you is valuable to make a "design first, and implement after", but I prefer to use types to help me out while I implement. More often than not, when you start coding the implementation you'll figure out that you might need changes in the design. With my approach, I just make those changes and types let me know if that change had any negative effect, but with yours, you need to go back to the types first, resolve the change there, and just then work on the implementation.
Just to clarify, is not just a whim or anything like that. I originally started using TypeScript because it had classes when JS didn't, and I was able to write code in a similar manner to C++ but on the web, so I get where you're coming from. Nowadays, after years of working with this language, not only did I stop using classes altogether, but also after working in lots of projects and companies with lots of people I found that the best use for TS in WebDevs is the one I suggest:
Is easier to grasp for people not familiar with TS.
Is easier to maintain and update.
Is equally safe, if you avoid any, non-null assertion and all those "I'm smarter than the language" kinds of things.
Is faster because we focus on the implementation, and the types are derived from that.
I think your approach is heavily inspired by how folks solved stuff with classes.
Design by Contract is attributed to Bertrand Meyer in connection with the Eiffel OOPL (1986) but the "type first" notion appears elsewhere like in Clojure's "thinking in data, modeling in data, and working in data" which is most definitely not OO.
Explicit types serve as deliberate "check points" during type checking/linting.
Is faster because we focus on the implementation, and the types are derived from that.
Expedience is always fielded as the primary motivation for "implementation first, contract last" approaches. But more often than not, contract last approaches save you time in the short term but create problems (that could be avoided by actively managing your types) in the long term (TTIS - Time-To-Initial-Success).
To some degree it's like saying "I know that JSDoc annotations are helpful for maintenance but keeping them in sync with the code slows me down so it's not worth it". Types are part of the application's specification that are supposed make it easier to reason about the code, so it helps if they are not buried somewhere in the implementation.
I guess the lesson here is: just because TypeScript is being used doesn't actually reveal whether the potential of types is being leveraged to maximum effect (to the extend that is even possible in TypeScript).
Chance
@chancethedev
Possibly hot take: feels like a lot of JS devs just like TypeScript for the intellisense but treat proper type guards as a burden
I'm fully aware that inferred function return types are considered idiomatic in TypeScript. In the end though I think that simply reflects an attitude where convenience wins over type safety (i.e. the popularity of TypeScript + VS Code is much less about type safety but much more about convenience).
Google TypeScript Style Guide: Return types
Whether to include return type annotations for functions and methods is up to the code author. Reviewers may ask for annotations to clarify complex return types that are hard to understand. Projects may have a local policy to always require return types, but this is not a general TypeScript style requirement.
There are two benefits to explicitly typing out the implicit return values of functions and methods:
One minor point is the fact that functions have a type. One of the more infuriating things is that you cannot type a function declaration with a function type - one has to use arrow functions for that. So by not explicitly specifying the return type half of the function's type is left unspecified/implicit. Sure, an IDE will be able to show that type but that means that now even code reviews have to be conducted in the presence of "assistive technologies" (as the explicit code simply doesn't tell the whole story).
The major point however:
A Gentle Introduction to haskell Version 98 - 2.2.1 Recursive Types
"For example, suppose we wish to define a function
fringe
that returns a list of all the elements in the leaves of a tree from left to right. It's usually helpful to write down the type of new functions first"The return type of a function is part of its design, its contract so it makes sense to explicitly determine and state it even before the body is implemented. So if one is already thinking in types and is perhaps even practicing type driven development then it makes sense to explicitly capture the types in play, perhaps even adding "more boilerplate" by adding type aliases that give the types more descriptive and meaningful names.
An implicit return type projects an attitude that the return type is merely an accidental byproduct of the function's implementation - while types are mandatory for the inputs (arguments);
It's one thing to rely on type inference within the confines of a function body - but crossing the function (or method) boundary is significant enough to warrant an explicit type check/lint (even for one liners) in order to catch problems as close to the source as possible.
Does that mean more typing on the keyboard? Sure; not enough though to affect productivity.
Interestingly Effective Typescript has "Item 19: Avoid Cluttering Your Code with Inferable Types" (p.81) which unsurprisingly states:
"The explicit type annotation is redundant. Writing it just adds noise. If you’re unsure of the type, you can check it in your editor."
Only to later clarify (p.85):
"Similar considerations apply to a function’s return type. You may still want to annotate this even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function."
The item is summarized (p.87) with:
TypeScript is about convenience. If it gets in the way instead of helping, you might be using it wrong. I've been using TypeScript since it first became public (almost 10 years ago), and my advice when using TS is generally always the same: Just use it when it can't figure out types by itself, or when it really helps readability. Using types just for the sake of using them is not that useful, because the idea of TS is that is just "JavaScript with types on top". Like using JSDocs directly in the language.
I beg to differ. Inferring the return type of a function is not an "accidental byproduct", but actually the result of your arguments. Depending on what you do with those arguments, the type of the return. You can be explicit about the type if it helps readability or if you want to make sure you get an error if the operation you do in that function doesn't return what you expect, but it isn't a requirement. You can also create a type for the entire function in a "haskell like" kind of a approach:
But the value I see in inferred return types is that you can change the implementation and get the correct "new type" without having to go and change the output type every time you do this. And you still get all the type safety of TS, so if you were returning a
number
and now you return astring
, you'll get the expected errors in all the places were you are using the output of that function as anumber
.Your example looks like this:
It looks like the intent of that code is to take a
number
argument and return"100"
or1
, so yes, that the correct type for its output. This will work in places were we expectnumber | string
, so no need to type the output like that.Trust me, my advice has nothing to do with "typing less". I cringe when I see that folks use generics like
T
, or identifiers likee
, so no, is not to type less. As I pointed out before, my advices is to use TS when actually needed, when it can't figure out stuff by itself. This way you're effectively doing "JS with types" instead of writing a plethora of types just to write "more TS".We have other options for that as well, you can for example make use of
as const
, andtypeof
,ReturnType
and many other utils to infer types from static values.I agree with you that we need to type the return when it helps readability, but when it doesn't, using the inferred type is more than ok and will help your code stay close the the JS it outputs, while still being type safe.
So again the return type is a consequence of the implementation.
When you "think in/develop with types" the implementation is a consequence of the desired type - i.e. the relationship it flipped.
Consider this scenario
We find that
MyType
also needstotal
.No error that the result from
fn
is no longer sufficient. If we are lucky somewhere the result will be delivered to an explicitly typed binding, alerting us to the problem with the function (or any others that produce a similar result).With an explicit return type compilation will identify all the functions that no longer produce the correct type.
i.e. the type is changed before the implementation(s).
A capability in terms of value-orientation:
i.e. contracts (constraints) first, implementation last.
Aside Why type-first development matters
My intent was to simulate a defect (which perhaps would only be detected at an explicitly typed site of use), i.e. the intended return type should either have been
string
ornumber
- notstring | number
.JS Doc TS with (hand written) declaration files is "Typed JS". There's a clean separation between "type space" (TypeScript types) and "value space" (the JS implementation).
The entire point of the
index.d.ts
andinternal.d.ts
files is to explicitly formulate a constrained set of types that are expected to be encountered (without having to formulate them "ad hoc").The problem is that using JS Doc TS well (as far as I can tell) requires a higher level of TS competence (especially its "typing language") than writing TS destined for transpilation.
Those features can help to cut down on verbosity and emerge as TS tries to tease whatever type information it can out of the JS value space to make it available in type space. But in terms of design, types are typically formulated before the implementations and defined in type space (rather than extracted from value space).
Type space derived from value space is like drafting a blueprint after the house has already been built.
I think your approach is heavily inspired by how folks solved stuff with classes. What I mean by this is that you think that types are like the design/blueprint of how everything should look like, way before you even start working on the implementation. Meanwhile, I prefer to use types in TypeScript as a tool to know what the type of something will be while I work on it, not before.
Considering your scenario, you could get that
MyType
from the function if you want:So if you change the implementation, the type adjusts to that. Basically, you make TS work for JS instead of the other way around. With this approach, let's say we have other two functions that take the output of
fn
as input:With your approach, if let's say
count
changes and now is astring
instead of a number, we need to first change the type inMyType
, then change the implementation infn
, then resolve the issues indoubleCount
. With my approach you only change the implementation, and then get errors were is relevant (likedoubleCount
). No need to update types.If you want your function to return one or the other, and you're returning both, you'll know it as soon as you try to use it in a place where you're expecting a single type and you get both:
The difference is that now I can figure out if I want
fn
to return anumber
or if I want to handle the case wherefn
returns a string:I agree to a certain degree, but as I stated before, the types you "design first" should be there just to help out with the JS part, not for the sake of types. Generally, you need to type arguments to let others and yourself in the future know what a function takes, but the output should be defined by the implementation. You don't lose any safety and you still gain lots of flexibility.
With this I disagree again, I feel a closer analogy would be that types derived from implementation are like having a map of your code, that updates automatically as you change it. If a mountain comes out of nowhere, you'll know. From my point of view, the usefulness of TypeScript comes when you augment JavaScript with it, not when you have to code in JavaScript to accommodate TypeScript types.
I understand that for you is valuable to make a "design first, and implement after", but I prefer to use types to help me out while I implement. More often than not, when you start coding the implementation you'll figure out that you might need changes in the design. With my approach, I just make those changes and types let me know if that change had any negative effect, but with yours, you need to go back to the types first, resolve the change there, and just then work on the implementation.
Just to clarify, is not just a whim or anything like that. I originally started using TypeScript because it had classes when JS didn't, and I was able to write code in a similar manner to C++ but on the web, so I get where you're coming from. Nowadays, after years of working with this language, not only did I stop using classes altogether, but also after working in lots of projects and companies with lots of people I found that the best use for TS in WebDevs is the one I suggest:
any
, non-null assertion and all those "I'm smarter than the language" kinds of things.Design by Contract is attributed to Bertrand Meyer in connection with the Eiffel OOPL (1986) but the "type first" notion appears elsewhere like in Clojure's "thinking in data, modeling in data, and working in data" which is most definitely not OO.
And the idea of "function types as contracts" is up-to-date - 3.7.1 Types and Contracts.
Explicit types serve as deliberate "check points" during type checking/linting.
Expedience is always fielded as the primary motivation for "implementation first, contract last" approaches. But more often than not, contract last approaches save you time in the short term but create problems (that could be avoided by actively managing your types) in the long term (TTIS - Time-To-Initial-Success).
To some degree it's like saying "I know that JSDoc annotations are helpful for maintenance but keeping them in sync with the code slows me down so it's not worth it". Types are part of the application's specification that are supposed make it easier to reason about the code, so it helps if they are not buried somewhere in the implementation.
I guess the lesson here is: just because TypeScript is being used doesn't actually reveal whether the potential of types is being leveraged to maximum effect (to the extend that is even possible in TypeScript).
Aside:
declare
)