We have dealt with function argument and return types, but there are a few more in-depth features we need to cover.
Callable types
Both type aliases and interfaces offer the capability to describe call signatures:
tsTry
interfaceTwoNumberCalculation {(x : number,y : number): number}typeTwoNumberCalc = (x : number,y : number) => numberconstadd :TwoNumberCalculation = (a ,b ) =>a +b constsubtract :TwoNumberCalc = (x ,y ) =>x -y
Let’s pause for a minute to note:
- The return type for an interface is
:number
, and for the type alias it’s=> number
- Because we provide types for the functions
add
andsubtract
, we don’t need to provide type annotations for each individual function’s argument list or return type
void
1
Sometimes functions don’t return anything, and we know from
experience with JavaScript, what actually happens in the situation below
is that x
will be undefined
:
tsTry
functionprintFormattedJSON (obj : string[]) {console .log (JSON .stringify (obj , null, " "))}constx =printFormattedJSON (["hello", "world"])
So why is it showing up as void
?
void
is a special type, that’s specifically used to describe
function return values. It has the following meaning:
The return value of a void function is intended to be ignored
We could type functions as returning undefined
, but there are some interesting
differences that highlight the reason for void
’s existence:
tsTry
functioninvokeInFourSeconds (callback : () => undefined) {setTimeout (callback , 4000)}functioninvokeInFiveSeconds (callback : () => void) {setTimeout (callback , 5000)}constvalues : number[] = []Type 'number' is not assignable to type 'undefined'.2322Type 'number' is not assignable to type 'undefined'.invokeInFourSeconds (() =>values .push (4))invokeInFiveSeconds (() =>values .push (4))
It happens that Array.prototype.push
returns a number,
and our invokeInFourSeconds
function above is unhappy about this being returned from the callback.
Construct signatures
Construct signatures are similar to call signatures, except they describe what should happen with the new
keyword.
tsTry
interfaceDateConstructor {new (value : number):Date }letMyDateConstructor :DateConstructor =Date constd = newMyDateConstructor ()
These are rare, but if you ever happen to come across them - you now know what they are.
Function overloads
Imagine the following situation:
html
<iframe src="https://example.com" /><!-- // --><form><input type="text" name="name" /><input type="text" name="email" /><input type="password" name="password" /><input type="submit" value="Login" /></form>
What if we had to create a function that allowed us to register a “main event listener”?
- If we are passed a
form
element, we should allow registration of a “submit callback” - If we are passed an
iframe
element, we should allow registration of a ”postMessage
callback”
Let’s give it a shot:
tsTry
typeFormSubmitHandler = (data :FormData ) => voidtypeMessageHandler = (evt :MessageEvent ) => voidfunctionhandleMainEvent (elem :HTMLFormElement |HTMLIFrameElement ,handler :FormSubmitHandler |MessageHandler ) {}constmyFrame =document .getElementsByTagName ("iframe")[0]handleMainEvent (myFrame , (val ) => {})
This is not good — we are allowing too many possibilities here, including things we don’t aim to support (e.g., using a HTMLIFrameElement
with FormSubmitHandler
, which doesn’t make much sense).
We can solve this using function overloads, where we define multiple function heads that serve as entry points to a single implementation:
tsTry
typeFormSubmitHandler = (data :FormData ) => voidtypeMessageHandler = (evt :MessageEvent ) => voidfunctionhandleMainEvent (elem :HTMLFormElement ,handler :FormSubmitHandler )functionhandleMainEvent (elem :HTMLIFrameElement ,handler :MessageHandler )functionhandleMainEvent (elem :HTMLFormElement |HTMLIFrameElement ,handler :FormSubmitHandler |MessageHandler ) {}constmyFrame =document .getElementsByTagName ("iframe")[0]constmyForm =document .getElementsByTagName ("form")[0]handleMainEvent (myFrame , (val ) => {})handleMainEvent (myForm , (val ) => {})
Look at that! We have effectively created a linkage between
the first and second arguments, which allows our callback’s
argument type to change, based on the type of handleMainEvent
’s first argument.
Let’s take a closer look at the function declaration:
tsTry
functionhandleMainEvent (elem :HTMLFormElement ,handler :FormSubmitHandler )functionhandleMainEvent (elem :HTMLIFrameElement ,handler :MessageHandler )functionhandleMainEvent (elem :HTMLFormElement |HTMLIFrameElement ,handler :FormSubmitHandler |MessageHandler ) {}handleMainEvent
This looks like three function declarations, but it’s really two “heads” that define an argument list and a return type, followed by our original implementation.
If you take a close look at tooltips and autocomplete feedback you get from the TypeScript language server, it’s clear that you are only able to call into the two “heads”, leaving the underlying “third head + implementation” inaccessible from the outside world.
One last thing that’s important to note: “implementation” function signature must be general enough to include everything that’s possible through the exposed first and second function heads. For example, this wouldn’t work
tsTry
functionhandleMainEvent (elem :HTMLFormElement ,handler :FormSubmitHandler )functionThis overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.( handleMainEvent elem :HTMLIFrameElement ,handler :MessageHandler )functionhandleMainEvent (elem :HTMLFormElement ) {}handleMainEvent
this
types
Sometimes we have a free-standing function that has a strong opinion around
what this
will end up being, at the time it is invoked.
For example, if we had a DOM event listener for a button:
html
<button onClick="myClickHandler">Click Me!</button>
We could define myClickHandler
as follows
tsTry
functionmyClickHandler (event :Event ) {'this' implicitly has type 'any' because it does not have a type annotation.2683'this' implicitly has type 'any' because it does not have a type annotation.this .disabled = true}myClickHandler (newEvent ("click")) // seems ok
Oops! TypeScript isn’t happy with us. Despite the fact that we know that this
will be element that fired the event, the compiler doesn’t seem to be happy with us using it in this way.
To address the problem, we need to give this function a this
type
tsTry
functionmyClickHandler (this :HTMLButtonElement ,event :Event ) {this.disabled = true}The 'this' context of type 'void' is not assignable to method's 'this' of type 'HTMLButtonElement'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'HTMLButtonElement'.myClickHandler (newEvent ("click")) // seems no longer ok
Now when we try to directly invoke myClickHandler
on the last line of the code snippet above
we get a new compiler error. Effectively, we have failed to provide the this
that this function
states it wants.
tsTry
functionmyClickHandler (this :HTMLButtonElement ,event :Event ) {this.disabled = true}myClickHandler constmyButton =document .getElementsByTagName ("button")[0]constboundHandler =myClickHandler .bind (myButton )boundHandler (newEvent ("click")) // bound version: okmyClickHandler .call (myButton , newEvent ("click")) // also ok
Note TypeScript understands that .bind
, .call
or .apply
will result in the proper this
being passed to the function as part of its invocation.
Function type best practices
Explicitly define return types
TypeScript is capable of inferring function return types quite effectively, but this accommodating behavior can lead to unintentional ripple effects where types change throughout your codebase
consider the following example
tsTry
export async functiongetData (url : string) {constresp = awaitfetch (url )constdata = (awaitresp .json ()) as {properties : string[]}returndata }functionloadData () {getData ("https://example.com").then ((result ) => {console .log (result .properties .join (", "))})}
and what if we made a seemingly innocent change
diff
export async function getData(url: string) {const resp = await fetch(url)+ if (resp.ok) {const data = await resp.json()return data+ }}
We’ll see some type-checking errors pop up, but at the invocation site, not the declaration site.
Imagine if we were passing this value through several other functions before reaching the point where type checking alerted us to a problem!
tsTry
async functiongetData (url : string) {constresp = awaitfetch (url )if (resp .ok ) {constdata = (awaitresp .json ()) as {properties : string[]}returndata }}functionloadData () {getData ("https://example.com").then ((result ) => {'result' is possibly 'undefined'.18048'result' is possibly 'undefined'.console .log (. result .properties join (", "))})}
If we use the same example, but define a return type explicitly, the error message is surfaced at the declaration site
tsTry
async functiongetData (url : string):Function lacks ending return statement and return type does not include 'undefined'.2366Function lacks ending return statement and return type does not include 'undefined'.Promise <{properties : string[] }> {constresp = awaitfetch (url )if (resp .ok ) {constdata = (awaitresp .json ()) as {properties : string[]}returndata }}functionloadData () {getData ("https://example.com").then ((result ) => {console .log (result .properties .join (", "))})}
-
There is a native Javascript concept of a native
↩void
keyword, but it’s not related to the TypeScript concept of the same name.