TypeScript provides two mechanisms for centrally defining types and giving them useful and meaningful names: interfaces and type aliases. We will study both concepts in depth, and explain when it makes sense to use each type.
Type aliases
Think back to the : {name: string, email: string}
syntax we’ve used up until this point for type annotations. This
syntax will get increasingly complicated as more properties are added to this type. Furthermore, if we pass
objects of this type around through various functions and variables, we will end up with a lot of types that need to be manually updated whenever we need to make any changes!
Type aliases help to address this, by allowing us to:
- define a more meaningful name for this type
- declare the particulars of the type in a single place
- import and export this type from modules, the same as if it were an exported value
tsTry
///////////////////////////////////////////////////////////// @filename: types.tsexport typeUserContactInfo = {name : string}///////////////////////////////////////////////////////////// @filename: utilities.tsimport {UserContactInfo } from "./types"functionprintContactInfo (info :UserContactInfo ) {console .log (info )console .log (info .}
We can see a couple of things here:
- the tooltip on
info
is now a lot cleaner and more semantic (meaningful, in connection with the concept behind it) - import/export of this
type
works just as it would for a function or a class in JavaScript
It’s important to realize that the name
UserContactInfo
is just for our convenience. This is still a structural type system
tsTry
///////////////////////////////////////////////////////////// @filename: utilities.tsimport {UserContactInfo } from "./types"functionprintContactInfo (info :UserContactInfo ) {console .log (info )console .log (info .}constpainter = {name : "Robert Ross",favoriteColor : "Titanium White",}printContactInfo (painter ) // totally fine
Let’s look at the declaration syntax for a moment:
tsTry
typeUserContactInfo = {name : string}
A few things to point out here:
- This is a rare occasion where we see type information on the right hand side of the assignment operator (
=
) - We’re using
TitleCase
to format the alias’ name. This is a common convention - As we can see below, we can only declare an alias of a given name once within a given scope. This is kind of like how a
let
orconst
variable declaration works
tsTry
typeDuplicate identifier 'UserContactInfo'.2300Duplicate identifier 'UserContactInfo'.= { UserContactInfo name : string}typeDuplicate identifier 'UserContactInfo'.2300Duplicate identifier 'UserContactInfo'.= { UserContactInfo fail : "this will not work"}
A type alias can hold any type, as it’s literally an alias (name) for a type of some sort.
Here’s an example of how we can “cleaned up” an the code from our Union and Intersection Types section (previous chapter) through the use of type aliases:
tsTry
///////////////////////////////////////////////////////////// @filename: original.ts/*** ORIGINAL version*/export functionmaybeGetUserInfo ():| ["error",Error ]| ["success", {name : string;// implementation is the same in both examplesif (Math .random () > 0.5) {return ["success",{name : "Mike North",]} else {return ["error",newError ("The coin landed on TAILS :("),]}}///////////////////////////////////////////////////////////// @filename: with-aliases.tstypeUserInfoOutcomeError = ["error",Error ]typeUserInfoOutcomeSuccess = ["success",{name : string;]typeUserInfoOutcome =|UserInfoOutcomeError |UserInfoOutcomeSuccess /*** CLEANED UP version*/export functionmaybeGetUserInfo ():UserInfoOutcome {// implementation is the same in both examplesif (Math .random () > 0.5) {return ["success",{name : "Mike North",]} else {return ["error",newError ("The coin landed on TAILS :("),]}}
Inheritance in type aliases
You can create type aliases that combine existing types with new behavior
by using Intersection (&
) types.
tsTry
typeSpecialDate =Date & {getReason (): string }constnewYearsEve :SpecialDate = {...newDate (),getReason : () => "Last day of the year",}newYearsEve .getReason
While there’s no true extends
keyword that can be used when defining type aliases, this pattern has a very similar effect
Interfaces
An interface is a way of defining an object type. An “object type” can be thought of as, “an instance of a class could conceivably look like this”.
For example, string | number
is not an object type, because it
makes use of the union type operator.
tsTry
interfaceUserInfo {name : string}functionprintUserInfo (info :UserInfo ) {info .name }
Like type aliases, interfaces can be imported/exported between modules just like values, and they serve to provide a “name” for a specific type.
Inheritance in interfaces
extends
If you’ve ever seen a JavaScript class that “inherits” behavior from a base class,
you’ve seen an example of what TypeScript calls a heritage clause: extends
jsTry
classAnimal {eat (food ) {consumeFood (food )}}classDog extendsAnimal {bark () {return "woof"}}constd = newDog ()d .eat d .bark
- Just as in in JavaScript, a subclass
extends
from a base class. - Additionally a “sub-interface”
extends
from a base interface, as shown in the example below
tsTry
interfaceAnimal {isAlive (): boolean}interfaceMammal extendsAnimal {getFurOrHairColor (): string}interfaceDog extendsMammal {getBreed (): string}functioncareForDog (dog :Dog ) {dog .getBreed }
implements
TypeScript adds a second heritage clause that can be used to
state that a given class should produce instances that confirm
to a given interface: implements
.
tsTry
interfaceAnimalLike {eat (food ): void}classClass 'Dog' incorrectly implements interface 'AnimalLike'. Property 'eat' is missing in type 'Dog' but required in type 'AnimalLike'.2420Class 'Dog' incorrectly implements interface 'AnimalLike'. Property 'eat' is missing in type 'Dog' but required in type 'AnimalLike'.implements Dog AnimalLike {bark () {return "woof"}}
In the example above, we can see that TypeScript is objecting
to us failing to add an eat()
method to our Dog
class.
Without this method, instances of Dog
do not conform to the
AnimalLike
interface. Let’s update our code:
tsTry
interfaceAnimalLike {eat (food ): void}classDog implementsAnimalLike {bark () {return "woof"}eat (food ) {consumeFood (food )}}
There, that’s better. While TypeScript (and JavaScript) does
not support true multiple inheritance (extending from more than one base class),
this implements
keyword gives us the ability to validate, at compile time, that instances of a class conform to one or more “contracts” (types). Note that both extends
and implements
can be used together:
tsTry
classLivingOrganism {isAlive () {return true}}interfaceAnimalLike {eat (food ): void}interfaceCanBark {bark (): string}classDog extendsLivingOrganism implementsAnimalLike ,CanBark {bark () {return "woof"}eat (food ) {consumeFood (food )}}
While it’s possible to use implements
with a type alias, if the type ever breaks the “object type” rules there’s some
potential for problems…
tsTry
typeCanBark =| number| {bark (): string}classA class can only implement an object type or intersection of object types with statically known members.2422A class can only implement an object type or intersection of object types with statically known members.Dog implements{ CanBark bark () {return "woof"}eat (food ) {consumeFood (food )}}
For this reason, it is best to use interfaces for types that
are used with the implements
heritage clause.
Open Interfaces
TypeScript interfaces are “open”, meaning that unlike in type aliases, you can have multiple declarations in the same scope:
tsTry
interfaceAnimalLike {isAlive (): boolean}functionfeed (animal :AnimalLike ) {animal .eat animal .isAlive }// SECOND DECLARATION OF THE SAME NAMEinterfaceAnimalLike {eat (food ): void}
These declarations are merged together to create a result
identical to what you would see if both the isAlive
and eat
methods were on a single interface declaration.
You may be asking yourself: where and how is this useful?
Imagine a situation where you want to add a global property
to the window
object
tsTry
window .document // an existing propertywindow .exampleProperty = 42// tells TS that `exampleProperty` existsinterfaceWindow {exampleProperty : number}
What we have done here is augment an existing Window
interface
that TypeScript has set up for us behind the scene.
Choosing which to use
In many situations, either a type
alias or an interface
would be
perfectly fine, however…
- If you need to define something other than an object type (e.g., use of the
|
union type operator), you must use a type alias - If you need to define a type to use with the
implements
heritage term, it’s best to use an interface - If you need to allow consumers of your types to augment them, you must use an interface.
Recursion
Recursive types, are self-referential, and are often used to describe infinitely nestable types. For example, consider infinitely nestable arrays of numbers
ts
;[3, 4, [5, 6, [7], 59], 221]
You may read or see things that indicate you must use a combination of interface
and type
for recursive types. As of TypeScript 3.7
this is now much easier, and works with either type aliases or interfaces.
tsTry
typeNestedNumbers = number |NestedNumbers []constval :NestedNumbers = [3, 4, [5, 6, [7], 59], 221]if (typeofval !== "number") {val .push (41)Argument of type 'string' is not assignable to parameter of type 'NestedNumbers'.2345Argument of type 'string' is not assignable to parameter of type 'NestedNumbers'.val .push ("this will not work" )}