TypeScript: Modern JavaScript...
Transcript of TypeScript: Modern JavaScript...
TypeScript:ModernJavaScriptDevelopment
TableofContents
TypeScript:ModernJavaScriptDevelopmentTypeScript:ModernJavaScriptDevelopmentCreditsPreface
WhatthislearningpathcoversWhatyouneedforthislearningpathWhothislearningpathisforReaderfeedbackCustomersupportDownloadingtheexamplecodeErrataPiracyQuestions
1.Module11.IntroducingTypeScript
TheTypeScriptarchitectureDesigngoalsTypeScriptcomponents
TypeScriptlanguagefeaturesTypes
OptionalstatictypenotationVariables,basictypes,andoperators
Var,let,andconstUniontypesTypeguardsTypealiasesAmbientdeclarationsArithmeticoperatorsComparisonoperatorsLogicaloperatorsBitwiseoperatorsAssignmentoperators
FlowcontrolstatementsThesingle-selectionstructure(if)Thedouble-selectionstructure(if…else)Theinlineternaryoperator(?)Themultiple-selectionstructure(switch)Theexpressionistestedatthetopoftheloop(while)Theexpressionistestedatthebottomoftheloop(do…while)Iterateoneachobject'sproperties(for…in)Countercontrolledrepetition(for)
FunctionsClassesInterfacesNamespaces
PuttingeverythingtogetherSummary
2.AutomatingYourDevelopmentWorkflowAmoderndevelopmentworkflowPrerequisites
Node.jsAtomGitandGitHub
SourcecontroltoolsPackagemanagementtools
npmBowertsd
TaskrunnersCheckingthequalityoftheTypeScriptcodeCompilingtheTypeScriptcodeOptimizingaTypeScriptapplicationManagingtheGulptasks'executionorder
TestrunnersSynchronizedcross-devicetestingContinuousIntegrationtoolsScaffoldingtoolsSummary
3.WorkingwithFunctionsWorkingwithfunctionsinTypeScript
FunctiondeclarationsandfunctionexpressionsFunctiontypesFunctionswithoptionalparametersFunctionswithdefaultparametersFunctionswithrestparametersFunctionoverloadingSpecializedoverloadingsignaturesFunctionscopeImmediatelyinvokedfunctionsGenericsTagfunctionsandtaggedtemplates
AsynchronousprogramminginTypeScriptCallbacksandhigher-orderfunctionsArrowfunctionsCallbackhell
PromisesGeneratorsAsynchronousfunctions–asyncandawait
Summary4.Object-OrientedProgrammingwithTypeScript
SOLIDprinciplesClassesInterfacesAssociation,aggregation,andcomposition
AssociationAggregationComposition
InheritanceMixins
GenericclassesGenericconstraints
MultipletypesingenerictypeconstraintsThenewoperatoringenerictypes
ApplyingtheSOLIDprinciplesTheLiskovsubstitutionprincipleTheinterfacesegregationprincipleThedependencyinversionprinciple
NamespacesModules
ES6modules–runtimeanddesigntimeExternalmodules–designtimeonlyAMDmodules–runtimeonlyCommonJSmodules–runtimeonlyUMDmodules–runtimeonlySystemJSmodules–runtimeonly
CirculardependenciesSummary
5.RuntimeTheenvironmentTheruntime
FramesStackQueueHeapTheeventloop
ThethisoperatorThethisoperatorintheglobalcontextThethisoperatorinafunctioncontextThecall,apply,andbindmethods
PrototypesInstancepropertiesversusclasspropertiesPrototypalinheritanceTheprototypechainAccessingtheprototypeofanobjectThenewoperator
ClosuresStaticvariableswithclosuresPrivatememberswithclosures
Summary6.ApplicationPerformance
PrerequisitesPerformanceandresourcesPerformancemetrics
AvailabilityTheresponsetimeProcessingspeedLatencyBandwidthScalability
PerformanceanalysisNetworkperformanceanalysisNetworkperformanceanduserexperience
NetworkperformancebestpracticesandrulesGPUperformanceanalysis
Framespersecond(FPS)CPUperformanceanalysisMemoryperformanceanalysisThegarbagecollector
PerformanceautomationPerformanceoptimizationautomationPerformancemonitoringautomationPerformancetestingautomation
ExceptionhandlingTheErrorclassThetry…catchstatementsandthrowstatements
Summary7.ApplicationTesting
SoftwaretestingglossaryAssertionsSpecsTestcasesSuitesSpies
DummiesStubsMocksTestcoverage
PrerequisitesGulpKarmaIstanbulMochaChaiSinon.JSTypedefinitionsPhantomJSSeleniumandNightwatch.js
TestingplanningandmethodologiesTest-drivendevelopmentBehavior-drivendevelopment(BDD)Testsplansandtesttypes
SettingupatestinfrastructureBuildingtheapplicationwithGulpRunningtheunittestwithKarmaRunningE2EtestswithSeleniumandNightwatch.js
Creatingtestassertions,specs,andsuiteswithMochaandChaiTestingtheasynchronouscodeAssertingexceptionsTDDversusBDDwithMochaandChai
TestspiesandstubswithSinon.JSSpiesStubs
Creatingend-to-endtestswithNightwatch.jsGeneratingtestcoveragereportsSummary
8.DecoratorsPrerequisitesAnnotationsanddecorators
TheclassdecoratorsThemethoddecoratorsThepropertydecoratorsTheparameterdecoratorsThedecoratorfactoryDecoratorswithargumentsThereflectionmetadataAPI
Summary9.ApplicationArchitecture
Thesingle-pageapplicationarchitectureTheMV*architectureCommoncomponentsandfeaturesintheMV*frameworks
ModelsCollectionsItemviewsCollectionviewsControllersEventsRouterandhash(#)navigationMediatorDispatcherClient-siderenderingandVirtualDOMUserinterfacedatabinding
One-waydatabindingTwo-waydatabinding
DataflowWebcomponentsandshadowDOM
ChoosinganapplicationframeworkWritinganMVCframeworkfromscratch
PrerequisitesApplicationeventsMediatorApplicationRouteEventemitterRouterDispatcherControllerModelandmodelsettingsViewandviewsettingsFramework
Summary10.PuttingEverythingTogether
PrerequisitesTheapplication'srequirementsTheapplication'sdataTheapplication'sarchitectureTheapplication'sfilestructureConfiguringtheautomatedbuildTheapplication'slayoutImplementingtherootcomponentImplementingthemarketcontrollerImplementingtheNASDAQmodel
ImplementingtheNYSEmodelImplementingthemarketviewImplementingthemarkettemplateImplementingthesymbolcontroller
ImplementingthequotemodelImplementingthesymbolviewImplementingthechartmodelImplementingthechartviewTestingtheapplicationPreparingtheapplicationforaproductionreleaseSummary
2.Module21.ToolsandFrameworks
InstallingtheprerequisitesInstallingNode.jsInstallingTypeScriptcompiler
ChoosingahandyeditorVisualStudioCode
ConfiguringVisualStudioCodeOpeningafolderasaworkspaceConfiguringaminimumbuildtask
SublimeTextwithTypeScriptpluginInstallingPackageControlInstallingtheTypeScriptplugin
OthereditororIDEoptionsAtomwiththeTypeScriptpluginVisualStudioWebStorm
GettingyourhandsontheworkflowConfiguringaTypeScriptproject
Introductiontotsconfig.jsonCompileroptions
targetmoduledeclarationsourceMapjsxnoEmitOnErrornoEmitHelpersnoImplicitAnyexperimentalDecorators*emitDecoratorMetadata*outDiroutFile
rootDirpreserveConstEnumsstrictNullChecksstripInternal*isolatedModules
AddingsourcemapsupportDownloadingdeclarationsusingtypings
InstallingtypingsDownloadingdeclarationfilesOption"save"
TestingwithMochaandIstanbulMochaandChai
WritingtestsinJavaScriptWritingtestsinTypeScript
GettingcoverageinformationwithIstanbulTestinginrealbrowserswithKarma
CreatingabrowserprojectInstallingKarmaConfiguringandstartingKarma
IntegratingcommandswithnpmWhynototherfancybuildtools?
Summary2.TheChallengeofIncreasingComplexity
ImplementingthebasicsCreatingthecodebaseDefiningtheinitialstructureofthedatatobesynchronizedGettingdatabycomparingtimestampsTwo-waysynchronizingThingsthatwentwrongwhileimplementingthebasics
PassingadatastorefromtheservertotheclientdoesnotmakesenseMakingtherelationshipsclear
GrowingfeaturesSynchronizingmultipleitems
SimplyreplacingdatatypewithanarrayServer-centeredsynchronization
SynchronizingfromtheservertotheclientSynchronizingfromclienttoserver
SynchronizingmultipletypesofdataSupportingmultipleclientswithincrementaldata
UpdatingtheclientsideUpdatingserverside
SupportingmoreconflictmergingNewdatastructuresUpdatingclientside
UpdatingtheserversideThingsthatgowrongwhileimplementingeverything
PilingupsimilaryetparallelprocessesDatastoresthataretremendouslysimplified
GettingthingsrightFindingabstractionImplementingstrategiesWrappingstores
Summary3.CreationalDesignPatterns
FactorymethodParticipantsPatternscopeImplementationConsequences
AbstractFactoryParticipantsPatternscopeImplementationConsequences
BuilderParticipantsPatternscopeImplementationConsequences
PrototypeSingleton
BasicimplementationsConditionalsingletons
Summary4.StructuralDesignPatterns
CompositePatternParticipantsPatternscopeImplementationConsequences
DecoratorPatternParticipantsPatternscopeImplementation
ClassicaldecoratorsDecoratorswithES-nextsyntax
ConsequencesAdapterPattern
ParticipantsPatternscopeImplementationConsequences
BridgePatternParticipantsPatternscopeImplementationConsequences
FaçadePatternParticipantsPatternscopeImplementationConsequences
FlyweightPatternParticipantsPatternscopeImplementationConsequences
ProxyPatternParticipantsPatternscopeImplementationConsequences
Summary5.BehavioralDesignPatterns
ChainofResponsibilityPatternParticipantsPatternscopeImplementationConsequences
CommandPatternParticipantsPatternscopeImplementationConsequences
MementoPatternParticipantsPatternscopeImplementationConsequences
IteratorPatternParticipantsPatternscope
ImplementationSimplearrayiteratorES6iterator
ConsequencesMediatorPattern
ParticipantsPatternscopeImplementationConsequences
Summary6.BehavioralDesignPatterns:Continuous
StrategyPatternParticipantsPatternscopeImplementationConsequences
StatePatternParticipantsPatternscopeImplementationConsequences
TemplateMethodPatternParticipantsPatternscopeImplementationConsequences
ObserverPatternParticipantsPatternscopeImplementationConsequences
VisitorPatternParticipantsPatternscopeImplementationConsequences
Summary7.PatternsandArchitecturesinJavaScriptandTypeScript
Promise-basedwebarchitecturePromisifyingexistingmodulesorlibrariesViewsandcontrollersinExpressAbstractionofresponsesAbstractionofpermissionsExpectederrors
DefiningandthrowingexpectederrorsTransformingerrors
ModularizingprojectAsynchronouspatterns
WritingpredictablecodeAsynchronouscreationalpatternsAsynchronousmiddlewareandhooksEvent-basedstreamparser
Summary8.SOLIDPrinciples
SingleresponsibilityprincipleExampleChoosinganaxis
Open-closedprincipleExampleAbstractioninJavaScriptandTypeScriptRefactorearlier
LiskovsubstitutionprincipleExampleTheconstraintsofsubstitution
InterfacesegregationprincipleExamplePropergranularity
DependencyinversionprincipleExampleSeparatinglayers
Summary9.TheRoadtoEnterpriseApplication
CreatinganapplicationDecisionbetweenSPAand"normal"webapplicationsTakingteamcollaborationintoconsideration
BuildingandtestingprojectsStaticassetspackagingwithwebpack
IntroductiontowebpackBundlingJavaScriptLoadingTypeScriptSplittingcodeLoadingotherstaticassets
AddingTSLinttoprojectsIntegratingwebpackandtslintcommandwithnpmscripts
VersioncontrolGitflow
MainbranchesSupportingbranches
FeaturebranchesReleasebranchesHotfixbranches
SummaryofGitflowPullrequestbasedcodereview
ConfiguringbranchpermissionsCommentsandmodificationsbeforemerge
TestingbeforecommitsGithooksAddingpre-commithookautomatically
ContinuousintegrationConnectingGitHubrepositorywithTravis-CI
DeploymentautomationPassivedeploymentbasedonGitserversidehooksProactivedeploymentbasedontimersornotifications
Summary3.Module3
1.TypeScript2.0FundamentalsWhatisTypeScript?Quickexample
TranspilingTypechecking
LearningmodernJavaScriptletandconstClassesArrowfunctionsFunctionargumentsArrayspreadDestructuringTemplatestringsNewclasses
TypecheckingPrimitivetypesDefiningtypesUndefinedandnullTypeannotations
Summary2.AWeatherForecastWidgetwithAngular2
UsingmodulesSettinguptheproject
DirectorystructureConfiguringTypeScriptBuildingthesystemTheHTMLfile
CreatingthefirstcomponentThetemplateTestingInteractionsOne-wayvariablebindingEventlisteners
AddingconditionstothetemplateDirectivesThetemplatetagModifyingtheabouttemplate
UsingthecomponentinothercomponentsShowingaforecast
UsingtheAPITypingtheAPI
CreatingtheforecastcomponentTemplatesDownloadingtheforecastAdding@Output
ThemaincomponentUsingourothercomponentsTwo-waybindingsListeningtooureventGeolocationAPIComponentsources
Summary3.Note-TakingAppwithaServer
SettinguptheprojectstructureDirectoriesConfiguringthebuildtoolTypedefinitions
GettingstartedwithNodeJSAsynchronouscode
CallbackapproachforasynchronouscodeDisadvantagesofcallbacks
ThedatabaseWrappingfunctionsinpromisesConnectingtothedatabaseQueryingthedatabase
UnderstandingthestructuraltypesystemGenericsTypingtheAPI
AddingauthenticationImplementingusersinthedatabaseAddinguserstothedatabase
TestingtheAPIAddingCRUDoperations
ImplementingthehandlersRequesthandling
WritingtheclientsideCreatingtheloginformCreatingamenuThenoteeditorThemaincomponent
ErrorhandlerRunningtheapplicationSummary
4.Real-TimeChatSettinguptheproject
ConfiguringgulpGettingstartedwithReact
CreatingacomponentwithJSXAddingpropsandstatetoacomponentCreatingthemenuTestingtheapplication
WritingtheserverConnectionsTypingtheAPIAcceptingconnectionsStoringrecentmessagesHandlingasessionImplementingachatmessagesession
ConnectingtotheserverAutomaticreconnectingSendingamessagetotheserverWritingtheeventhandler
CreatingthechatroomTwo-waybindingsStatelessfunctionalcomponentsRunningtheapplication
ComparingReactandAngularTemplatesandJSXLibrariesorframeworks
Summary5.NativeQRScannerApp
GettingstartedwithNativeScriptCreatingtheprojectstructure
AddingTypeScriptCreatingaHelloWorldpage
CreatingthemainviewAddingadetailsviewScanningQRcodes
TypedefinitionsImplementationTestingonadevice
AddingpersistentstorageStylingtheappComparingNativeScripttoalternativesSummary
6.AdvancedProgramminginTypeScriptUsingtypeguards
NarrowingNarrowinganyCombiningtypeguards
MoreaccuratetypeguardsAssignments
CheckingnullandundefinedGuardagainstnullandundefinedThenevertype
CreatingtaggeduniontypesComparingperformanceofalgorithms
Big-OhnotationOptimizingalgorithmsBinarysearchBuilt-infunctions
Summary7.SpreadsheetApplicationswithFunctionalProgramming
SettinguptheprojectFunctionalprogramming
CalculatingafactorialUsingdatatypesforexpressions
CreatingdatatypesTraversingdatatypesValidatinganexpressionCalculatingexpressions
ParsinganexpressionCreatingcoreparsersRunningparsersinasequenceParsinganumberOrderofoperations
DefiningthesheetCalculatingallfields
UsingtheFluxarchitecture
DefiningthestateCreatingthestoreanddispatcher
CreatingactionsAddingacolumnorarowChangingthetitleShowingtheinputpopupTestingactions
WritingtheviewRenderingthegridRenderingafieldShowingthepopupAddingstylesGluingeverythingtogether
AdvantagesofFluxGoingcross-platform
Summary8.PacManinHTML5
SettinguptheprojectUsingtheHTML5canvas
SavingandrestoringthestateDesigningtheframework
CreatingpicturesWrappingotherpicturesCreatingeventsBindingeverythingtogether
DrawingonthecanvasAddingutilityfunctionsCreatingthemodels
UsingenumsStoringthelevelCreatingthedefaultlevelCreatingthestate
DrawingtheviewHandlingevents
WorkingwithkeycodesCreatingthetimehandlerRunningthegameAddingamenu
ChangingthemodelRenderingthemenuHandlingeventsModifyingthetimehandler
Summary9.PlayingTic-Tac-ToeagainstanAI
CreatingtheprojectstructureConfigureTypeScript
AddingutilityfunctionsCreatingthemodels
ShowingthegridCreatingoperationsonthegridCreatingthegridAddingtestsRandomtesting
ImplementingtheAIusingMinimaxImplementingMinimaxinTypeScriptOptimizingthealgorithm
CreatingtheinterfaceHandlinginteractionCreatingplayers
TestingtheAITestingwitharandomplayer
Summary10.MigrateJavaScripttoTypeScript
GraduallymigratingtoTypeScriptAddingTypeScript
ConfiguringTypeScriptConfiguringthebuildtoolAcquiringtypedefinitionsTestingtheproject
MigratingeachfileConvertingtoESmodulesCorrectingtypesAddingtypeguardsandcastsUsingmodernsyntaxAddingtypes
RefactoringtheprojectEnablestrictchecks
SummaryA.BibliographyIndex
TypeScript:ModernJavaScriptDevelopment
TypeScript:ModernJavaScriptDevelopmentLeveragethefeaturesofTypeScripttoboostyourdevelopmentskillsandcreatecaptivatingwebapplications
Acourseinthreemodules
BIRMINGHAM-MUMBAI
TypeScript:ModernJavaScriptDevelopmentCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthiscoursemaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthiscoursetoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthiscourseissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthors,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythiscourse.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthiscoursebytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Publishedon:December2016
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
BirminghamB32PB,UK.
ISBN978-1-78728-908-6
www.packtpub.com
CreditsAuthors
RemoH.Jansen
VilicVane
IvoGabedeWolff
Reviewers
LiviuIgnat
JakubJedryszek
AndrewLeithMacrae
BrandonMills
IvoGabedeWolff
WanderWang
MatthewHill
ContentDevelopmentEditor
RohitKumarSingh
Graphics
JasonMonteiro
ProductionCoordinator
ShraddhaFalebhai
PrefaceItwasn’talongtimeagothatmanyJavaScriptengineersor,mostofthetime,webfrontendengineers,werestillfocusingonsolvingdetailedtechnicalissues,suchashowtolayoutspecificcontentcross-browsersandhowtosendrequestscross-domains.
Atthattime,agoodwebfrontendengineerwasusuallyexpectedtohavenotableexperienceonhowdetailedfeaturescanbeimplementedwithexistingAPIs.Onlyafewpeoplecaredabouthowtowriteapplication-scaleJavaScriptbecausetheinteractiononawebpagewasreallysimpleandnoonewroteASPinJavaScript.
However,thesituationhaschangedtremendously.JavaScripthasbecometheonlylanguagethatrunseverywhere,cross-platformandcross-device.Inthemainbattlefield,interactionsontheWebbecomemoreandmorecomplex,andpeoplearemovingbusinesslogicfromthebackendtothefrontend.WiththegrowthoftheNode.jscommunity,JavaScriptisplayingamoreandmoreimportantrolesinourlife.
TypeScriptisindeedanawesometoolforJavaScript.Unfortunately,intelligenceisstillrequiredtowriteactuallyrobust,maintainable,andreusablecode.TypeScriptallowsdeveloperstowritereadableandmaintainablewebapplications.Editorscanprovideseveraltoolstothedeveloper,basedontypesandstaticanalysisofthecode.
WhatthislearningpathcoversModule1,LearningTypeScript,introducesmanyoftheTypeScriptfeaturesinasimpleandeasy-to-understandformat.Thisbookwillteachyoueverythingyouneedtoknowinordertoimplementlarge-scaleJavaScriptapplicationsusingTypeScript.NotonlydoesitteachTypeScript’scorefeatures,whichareessentialtoimplementawebapplication,butitalsoexploresthepowerofsometools,designprinciples,bestpractices,anditalsodemonstrateshowtoapplytheminareal-lifeapplication.
Module2,TypeScriptDesignPatterns,iscollectionofthemostimportantpatternsyouneedtoimproveyourapplications’performanceandyourproductivity.Eachpatternisaccompaniedwithrichexamplesthatdemonstratethepowerofpatternsforarangeoftasks,frombuildinganapplicationtocodetesting.
Module3,TypeScriptBlueprints,showsyouhowtouseTypeScripttobuildcleanwebapplications.YouwilllearnhowtouseAngular2andReact.YouwillalsolearnhowyoucanuseTypeScriptforservers,mobileapps,command-linetools,andgames.Youwillalsolearnfunctionalprogramming.Thisstyleofprogrammingwillimproveyourgeneralcodeskills.YouwillseehowthisstylecanbeusedinTypeScript.
WhatyouneedforthislearningpathYouwillneedtheTypeScriptcompilerandatexteditor.ThislearningpathexplainshowtouseAtom,butitisalsopossibletouseothereditors,suchasVisualStudio2015,VisualStudioCode,orSublimeText.
YoualsoneedanInternetconnectiontodownloadtherequiredreferencesandonlinepackagesandlibraries,suchasjQuery,Mocha,andGulp.Dependingonyouroperatingsystem,youwillneedauseraccountwithadministrativeprivilegesinordertoinstallsomeofthetoolsusedinthislearningpath.AlsotocompileTypeScript,youneedNodeJS.Youcanfinddetailsonhowyoucaninstallitinthefirstchapterofthethirdmodule.
WhothislearningpathisforThislearningpathisfortheintermediate-levelJavaScriptdevelopersaimingtolearnTypeScripttobuildbeautifulwebapplicationsandfunprojects.NopriorknowledgeofTypeScriptisrequiredbutabasicunderstandingofjQueryisexpected.ThislearningpathisalsoforexperiencedTypeScriptdeveloperwantingtotaketheirskillstothenextlevel,andalsoforwebdeveloperswhowishtomakethemostofTypeScript.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthiscourse—whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.
Tosendusgeneralfeedback,simplye-mail<[email protected]>,andmentionthecourse’stitleinthesubjectofyourmessage.
Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktcourse,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthiscoursefromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthiscourseelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORT t0thetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthecourseintheSearchbox.5. Selectthecourseforwhichyou’relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthiscoursefrom.7. ClickonCodeDownload.
YoucanalsodownloadthecodefilesbyclickingontheCodeFilesbuttononthecourse’swebpageatthePacktPublishingwebsite.Thispagecanbeaccessedbyenteringthecourse’snameintheSearchbox.PleasenotethatyouneedtobeloggedintoyourPacktaccount.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthecourseisalsohostedonGitHubathttps://github.com/PacktPublishing/TypeScript-Modern-JavaScript-Development.Wealsohaveothercodebundlesfromourrichcatalogofbooks,videos,andcoursesavailableathttps://github.com/PacktPublishing/.Checkthemout!
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourcourses—maybeamistakeinthetextorthecode—wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthiscourse.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourcourse,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthecourseinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusat<[email protected]>withalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthiscourse,youcancontactusat<[email protected]>,andwewilldoourbesttoaddresstheproblem.
Part1.Module1LearningTypeScript
ExploitthefeaturesofTypeScripttodevelopandmaintaincaptivatingwebapplicationswithease
Chapter1.IntroducingTypeScriptThisbookfocusesonTypeScript'sobject-orientednatureandhowitcanhelpyoutowritebettercode.Beforedivingintotheobject-orientedprogramingfeaturesofTypeScript,thischapterwillgiveyouanoverviewofthehistorybehindTypeScriptandintroduceyoutosomeofthebasics.
Inthischapter,youwilllearnaboutthefollowingconcepts:
TheTypeScriptarchitectureTypeannotationsVariablesandprimitivedatatypesOperatorsFlowcontrolstatementsFunctionsClassesInterfacesModules
TheTypeScriptarchitectureInthissection,wewillfocusontheTypeScript'sinternalarchitectureanditsoriginaldesigngoals.
DesigngoalsInthefollowingpoints,youwillfindthemaindesigngoalsandarchitecturaldecisionsthatshapedthewaytheTypeScriptprogramminglanguagelooksliketoday:
StaticallyidentifyJavaScriptconstructsthatarelikelytobeerrors.TheengineersatMicrosoftdecidedthatthebestwaytoidentifyandpreventpotentialruntimeissueswastocreateastronglytypedprogramminglanguageandperformstatictypecheckingatcompilationtime.Theengineersalsodesignedalanguageserviceslayertoprovidedeveloperswithbettertools.HighcompatibilitywiththeexistingJavaScriptcode.TypeScriptisasupersetofJavaScript;thismeansthatanyvalidJavaScriptprogramisalsoavalidTypeScriptprogram(withafewsmallexceptions).Provideastructuringmechanismforlargerpiecesofcode.TypeScriptaddsclass-basedobjectorientation,interfaces,andmodules.Thesefeatureswillhelpusstructureourcodeinamuchbetterway.Wewillalsoreducepotentialintegrationissueswithinourdevelopmentteamandourcodewillbecomemoremaintainableandscalablebyadheringtothebestobject-orientedprinciplesandpractices.Imposenoruntimeoverheadonemittedprograms.ItiscommontodifferentiatebetweendesigntimeandexecutiontimewhenworkingwithTypeScript.WeusethetermdesigntimecodetorefertotheTypeScriptcodethatwewritewhiledesigninganapplication;weusethetermsexecutiontimecodeorruntimecodetorefertotheJavaScriptcodethatisexecutedaftercompilingsomeTypeScriptcode.
TypeScriptaddsfeaturestoJavaScriptbutthosefeaturesareonlyavailableatdesigntime.Forexample,wecandeclareinterfacesinTypeScriptbutsinceJavaScriptdoesn'tsupportinterfaces,theTypeScriptcompilerwillnotdeclareortrytoemulatethisfeatureintheoutputJavaScriptcode.
TheMicrosoftengineersprovidedtheTypeScriptcompilerwithmechanismssuchascodetransformations(convertingTypeScriptfeaturesintoplainJavaScriptimplementations)andtypeerasure(removingstatictypenotation)togeneratereallycleanJavaScriptcode.TypeerasureremovesnotonlythetypeannotationsbutalsoalltheTypeScriptexclusivelanguagefeaturessuchasinterfaces.
Furthermore,thegeneratedcodeishighlycompatiblewithwebbrowsersasittargetstheECMAScript3specificationbydefaultbutitalsosupportsECMAScript5andECMAScript6.Ingeneral,wecanusetheTypeScriptfeatureswhencompilingtoanyoftheavailablecompilationtargets,buttherearesomefeaturesthatwillrequireECMAScript5orhigherasthecompilationtarget.AlignwiththecurrentandfutureECMAScriptproposals.TypeScriptisnotjustcompatiblewiththeexistingJavaScriptcode;itwillalsopotentiallybecompatiblewithfutureversionsofJavaScript.ThemajorityofTypescript'sadditionalfeaturesarebasedonthefutureECMAScriptproposals;thismeansmanyTypeScriptfileswilleventuallybecomevalidJavaScriptfiles.
Beacross-platformdevelopmenttool.MicrosoftreleasedTypeScriptundertheopensourceApachelicenseanditcanbeinstalledandexecutedinallmajoroperatingsystems.
TypeScriptcomponentsTheTypeScriptlanguageisinternallydividedintothreemainlayers.Eachoftheselayersis,inturn,dividedintosublayersorcomponents.Inthefollowingdiagram,wecanseethethreelayers(green,blue,andorange)andeachoftheirinternalcomponents(boxes):
Note
Intheprecedingdiagram,theacronymVSreferstoMicrosoft'sVisualStudio,whichistheofficialintegrateddevelopmentenvironmentforalltheMicrosoftproducts(includingTypeScript).WewilllearnmoreaboutthisandtheotherIDEsinthenextchapter.
Eachofthesemainlayershasadifferentpurpose:
Thelanguage:ItfeaturestheTypeScriptlanguageelements.Thecompiler:Itperformstheparsing,typechecking,andtransformationofyourTypeScriptcodetoJavaScriptcode.Thelanguageservices:ItgeneratesinformationthathelpseditorsandothertoolsprovidebetterassistancefeaturessuchasIntelliSenseorautomatedrefactoring.IDEintegration:InordertotakeadvantagesoftheTypeScriptfeatures,someintegrationworkisrequiredtobedonebythedevelopersoftheIDEs.TypeScriptwasdesignedtofacilitatethedevelopmentoftoolsthathelptoincreasetheproductivityofJavaScriptdevelopers.Asaresultoftheseefforts,integratingTypeScriptwithanIDEisnotacomplicatedtask.AproofofthisisthatthemostpopularIDEsthesedaysincludeagoodTypeScriptsupport.
Note
Inotherbooksandonlineresources,youmayfindreferencestothetermtranspilerinsteadofcompiler.Atranspilerisatypeofcompilerthattakesthesourcecodeofaprogramminglanguageasitsinputandoutputsthesourcecodeintoanotherprogramminglanguagewithmoreorlessthesamelevelofabstraction.
Wedon'tneedtogointoanymoredetailasunderstandinghowtheTypeScriptcompiler
worksisoutofthescopeofthisbook;however,ifyouwishtolearnmoreaboutthistopic,refertotheTypeScriptlanguagespecification,whichcanbefoundonlineathttp://www.typescriptlang.org/.
TypeScriptlanguagefeaturesNowthatyouhavelearnedaboutthepurposeofTypeScript,it'stimetogetourhandsdirtyandstartwritingsomecode.
BeforeyoucanstartlearninghowtousesomeofthebasicTypeScriptbuildingblocks,youwillneedtosetupyourdevelopmentenvironment.TheeasiestandfastestwaytostartwritingsomeTypeScriptcodeistousetheonlineeditoravailableontheofficialTypeScriptwebsiteathttp://www.typescriptlang.org/Playground,asyoucanseeinthefollowingscreenshot:
Intheprecedingscreenshot,youwillbeabletousethetexteditorontheleft-handsidetowriteyourTypeScriptcode.ThecodeisautomaticallycompiledtoJavaScriptandtheoutputcodewillbeinsertedinthetexteditorlocatedontheright-handsideofthescreen.IfyourTypeScriptcodeisinvalid,theJavaScriptcodeontheright-handsidewillnotberefreshed.
Alternatively,ifyouprefertobeabletoworkoffline,youcandownloadandinstalltheTypeScriptcompiler.IfyouworkwithVisualStudio,youcandownloadtheofficial
TypeScriptextension(version1.5beta)fromhttps://visualstudiogallery.msdn.microsoft.com/107f89a0-a542-4264-b0a9-eb91037cf7af.IfyouareworkingwithVisualStudio2015,youdon'tneedtoinstalltheextensionasVisualStudio2015includesTypeScriptsupportbydefault.
IfyouuseadifferentcodeeditororyouusetheOSXorLinuxoperatingsystems,youcandownloadannpmmoduleinstead.Don'tworryifyouarenotfamiliarwithnpm.Fornow,youjustneedtoknowthatitstandsforNodePackageManagerandisthedefaultNode.jspackagemanager.
Note
ThereareTypeScriptpluginsavailableformanypopulareditorssuchasSublimehttps://github.com/Microsoft/TypeScript-Sublime-PluginandAtomhttps://atom.io/packages/atom-typescript.
Inordertobeabletousenpm,youwillneedtofirstinstallNode.jsinyourdevelopmentenvironment.YouwillbeabletofindtheNode.jsinstallationfilesontheofficialwebsiteathttps://nodejs.org/.
OnceyouhaveinstalledNode.jsinyourdevelopmentenvironment,youwillbeabletorunthefollowingcommandinaconsoleorterminal:
npminstall-gtypescript
OSXusersneedtousethesudocommandwheninstallingglobal(-g)npmpackages.Thesudocommandwillpromptforusercredentialsandinstallthepackageusingadministrativeprivileges:
sudonpminstall-gtypescript
Createanewfilenamedtest.tsandaddthefollowingcodetoit:
vart:number=1;
Savethefileintoadirectoryofyourchoiceandonceyouhavesavedthefileopentheconsole,selectthedirectorywhereyousavedthefile,andexecutethefollowingcommand:
tsctest.ts
ThetsccommandisaconsoleinterfacefortheTypeScriptcompiler.ThiscommandallowsyoutocompileyourTypeScriptfilesintoJavaScriptfiles.Thecompilerfeaturesmanyoptionsthatwillbeexploredintheupcomingchaptersofthisbook.
Intheprecedingexample,weusedthetsccommandtotransformthetest.tsfileintoaJavaScriptfile.
Ifeverythinggoesright,youwillfindafilenamedtest.jsinthesamedirectoryinwhichthe
test.tsfilewaslocated.Now,youknowhowtocompileyourTypeScriptcodeintoJavaScriptandwecanstartlearningabouttheTypeScriptfeatures.
Note
YouwillbeabletolearnmoreabouteditorsandothertoolsinChapter2,AutomatingYourDevelopmentWorkflow.
TypesAswehavealreadylearned,TypeScriptisatypedsupersetofJavaScript.TypeScriptaddedoptionalstatictypeannotationstoJavaScriptinordertotransformitintoastronglytypedprogramminglanguage.Theoptionalstatictypeannotationsareusedasconstraintsonprogramentitiessuchasfunctions,variables,andpropertiessothatcompilersanddevelopmenttoolscanofferbetterverificationandassistance(suchasIntelliSense)duringsoftwaredevelopment.
Strongtypingallowstheprogrammertoexpresshisintentionsinhiscode,bothtohimselfandtoothersinthedevelopmentteam.
Typescript'stypeanalysisoccursentirelyatcompiletimeandaddsnoruntimeoverheadtoprogramexecution.
Optionalstatictypenotation
TheTypeScriptlanguageserviceisreallygoodatinferringtypes,buttherearecertaincaseswhereitisnotabletoautomaticallydetectthetypeofanobjectorvariable.Forthesecases,TypeScriptallowsustoexplicitlydeclarethetypeofavariable.Thelanguageelementthatallowsustodeclarethetypeofavariableisknownasoptionalstatictypenotation.Foravariable,thetypenotationcomesafterthevariablenameandisprecededbyacolon:
varcounter;//unknown(any)type
varcounter=0;//number(inferred)
varcounter:number;//number
varcounter:number=0;//number
Asyoucansee,thetypeofthevariableisdeclaredafterthename,thisstyleoftypenotationisbasedontypetheoryandhelpstoreinforcetheideaoftypesbeingoptional.Whennotypeannotationsareavailable,TypeScriptwilltrytoguessthetypeofthevariablebyexaminingtheassignedvalues.Forexample,inthesecondlineintheprecedingcodesnippet,wecanseethatthevariablecounterhasbeenidentifiedasanumericvariablebecausethenumericvalue0wasassignedasitsvalue.ThisprocessinwhichtypesareautomaticallydetectedisknownasTypeinference,whenatypecannotbeinferredtheespecialtypeanyisusedasthetypeofthevariable.
Variables,basictypes,andoperatorsThebasictypesaretheBoolean,number,string,array,voidtypes,andalluserdefinedEnumtypes.AlltypesinTypeScriptaresubtypesofasingletoptypecalledtheAnytype.Theanykeywordreferencesthistype.Let'stakealookateachoftheseprimitivetypes:
DataType Description
Boolean
Whereasthestringandnumberdatatypescanhaveavirtuallyunlimitednumberofdifferentvalues,theBooleandatatypecanonlyhavetwo.Theyaretheliteralstrueandfalse.ABooleanvalueisatruthvalue;itspecifieswhethertheconditionistrueornot.
varisDone:boolean=false;
Number
AsinJavaScript,allnumbersinTypeScriptarefloatingpointvalues.Thesefloating-pointnumbersgetthetypenumber.
varheight:number=6;
String
YouusethestringdatatypetorepresenttextinTypeScript.Youincludestringliteralsinyourscriptsbyenclosingtheminsingleordoublequotationmarks.Doublequotationmarkscanbecontainedinstringssurroundedbysinglequotationmarks,andsinglequotationmarkscanbecontainedinstringssurroundedbydoublequotationmarks.
varname:string="bob";
name='smith';
Array
TypeScript,likeJavaScript,allowsyoutoworkwitharraysofvalues.Arraytypescanbewritteninoneofthetwoways.Inthefirst,youusethetypeoftheelementsfollowedby[]todenoteanarrayofthatelementtype:
varlist:number[]=[1,2,3];
Thesecondwayusesagenericarraytype,Array:
varlist:Array<number>=[1,2,3];
Anenumisawayofgivingmorefriendlynamestosetsofnumericvalues.Bydefault,enumsbeginnumberingtheirmembersstartingat0,butyoucanchange
Enum thisbymanuallysettingthevalueofonetoitsmembers.
enumColor{Red,Green,Blue};
varc:Color=Color.Green;
Any
TheanytypeisusedtorepresentanyJavaScriptvalue.AvalueoftheanytypesupportsthesameoperationsasavalueinJavaScriptandminimalstatictypecheckingisperformedforoperationsonanyvalues.
varnotSure:any=4;
notSure="maybeastringinstead";
notSure=false;//okay,definitelyaboolean
TheanytypeisapowerfulwaytoworkwithexistingJavaScript,allowingyoutograduallyoptinandoptoutoftypecheckingduringcompilation.Theanytypeisalsohandyifyouknowsomepartofthetype,butperhapsnotallofit.Forexample,youmayhaveanarraybutthearrayhasamixofdifferenttypes:
varlist:any[]=[1,true,"free"];
list[1]=100;
Void
Theoppositeinsomewaystoanyisvoid,theabsenceofhavinganytypeatall.Youwillseethisasthereturntypeoffunctionsthatdonotreturnavalue.
functionwarnUser():void{
alert("Thisismywarningmessage");
}
JavaScript'sprimitivetypesalsoincludeundefinedandnull.InJavaScript,undefinedisapropertyintheglobalscopethatisassignedasavaluetovariablesthathavebeendeclaredbuthavenotyetbeeninitialized.Thevaluenullisaliteral(notapropertyoftheglobalobject).Itcanbeassignedtoavariableasarepresentationofnovalue.
varTestVar;//variableisdeclaredbutnotinitialized
alert(TestVar);//showsundefined
alert(typeofTestVar);//showsundefined
varTestVar=null;//variableisdeclaredandvaluenullisassignedas
value
alert(TestVar);//showsnull
alert(typeofTestVar);//showsobject
InTypeScript,wewillnotbeabletousenullorundefinedastypes:
varTestVar:null;//Error,Typeexpected
varTestVar:undefined;//Error,cannotfindnameundefined
Sincenullorundefinedcannotbeusedastypes,boththevariabledeclarationsintheprecedingcodesnippetareinvalid.
Var,let,andconst
WhenwedeclareavariableinTypeScript,wecanusethevar,let,orconstkeywords:
varmynum:number=1;
letisValid:boolean=true;
constapiKey:string="0E5CE8BD-6341-4CC2-904D-C4A94ACD276E";
Variablesdeclaredwithvararescopedtothenearestfunctionblock(orglobal,ifoutsideafunctionblock).
Variablesdeclaredwithletarescopedtothenearestenclosingblock(orglobalifoutsideanyblock),whichcanbesmallerthanafunctionblock.
Theconstkeywordcreatesaconstantthatcanbeglobalorlocaltotheblockinwhichitisdeclared.Thismeansthatconstantsareblockscoped.YouwilllearnmoreaboutscopesinChapter5,Runtime.
Note
TheletandconstkeywordshavebeenavailablesincethereleaseofTypeScript1.4butonlywhenthecompilationtargetisECMAScript6.However,theywillalsoworkwhentargetingECMAScript3andECMAScript5onceTypeScript1.5isreleased.
Uniontypes
TypeScriptallowsyoutodeclareuniontypes:
varpath:string[]|string;
path='/temp/log.xml';
path=['/temp/log.xml','/temp/errors.xml'];
path=1;//Error
Uniontypesareusedtodeclareavariablethatisabletostoreavalueoftwoormoretypes.Intheprecedingexample,wehavedeclaredavariablenamedpaththatcancontainasinglepath(string),oracollectionofpaths(arrayofstring).Intheexample,wehavealsosetthevalueofthevariable.Weassignedastringandanarrayofstringswithouterrors;however,whenweattemptedtoassignanumericvalue,wegotacompilationerrorbecausetheuniontypedidn'tdeclareanumberasoneofthevalidtypesofthevariable.
Typeguards
Wecanexaminethetypeofanexpressionatruntimebyusingthetypeoforinstanceofoperators.TheTypeScriptlanguageservicelooksfortheseoperatorsandwillchangetypeinferenceaccordinglywhenusedinanifblock:
varx:any={/*...*/};
if(typeofx==='string'){
console.log(x.splice(3,1));//Error,'splice'doesnotexiston'string'
}
//xisstillany
x.foo();//OK
Intheprecedingcodesnippet,wehavedeclaredanxvariableoftypeany.Later,wecheckthetypeofxatruntimebyusingthetypeofoperator.Ifthetypeofxresultstobestring,wewilltrytoinvokethemethodsplice,whichissupposedtoamemberofthexvariable.TheTypeScriptlanguageserviceisabletounderstandtheusageoftypeofinaconditionalstatement.TypeScriptwillautomaticallyassumethatxmustbeastringandletusknowthatthesplicemethoddoesnotexistonthetypestring.Thisfeatureisknownastypeguards.
Typealiases
TypeScriptallowsustodeclaretypealiasesbyusingthetypekeyword:
typePrimitiveArray=Array<string|number|boolean>;
typeMyNumber=number;
typeNgScope=ng.IScope;
typeCallback=()=>void;
Typealiasesareexactlythesameastheiroriginaltypes;theyaresimplyalternativenames.Typealiasescanhelpustomakeourcodemorereadablebutitcanalsoleadtosomeproblems.
Ifyouworkaspartofalargeteam,theindiscriminatecreationofaliasescanleadtomaintainabilityproblems.Inthebook,MaintainableJavaScript,NicholasC.Zakas,theauthorrecommendstoavoidmodifyingobjectsyoudon'town.Nicholaswastalkingaboutadding,removing,oroverridingmethodsinobjectsthathavenotbeendeclaredbyyou(DOMobjects,BOMobjects,primitivetypes,andthird-partylibraries)butwecanapplythisruletotheusageofaliasesaswell.
Ambientdeclarations
AmbientdeclarationallowsyoutocreateavariableinyourTypeScriptcodethatwillnotbetranslatedintoJavaScriptatcompilationtime.ThisfeaturewasdesignedtofacilitateintegrationwiththeexistingJavaScriptcode,theDOM(DocumentObjectModel),andBOM(BrowserObjectModel).Let'stakealookatanexample:
customConsole.log("Alogentry!");//error
IfyoutrytocallthememberlogofanobjectnamedcustomConsole,TypeScriptwillletusknowthatthecustomConsoleobjecthasnotbeendeclared:
//Cannotfindname'customConsole'
Thisisnotasurprise.However,sometimeswewanttoinvokeanobjectthathasnotbeen
defined,forexample,theconsoleorwindowobjects.
console.log("LogEntry!");
varhost=window.location.hostname;
WhenweaccessDOMorBOMobjects,wedon'tgetanerrorbecausetheseobjectshavealreadybeendeclaredinaspecialTypeScriptfileknownasdeclarationfiles.Youcanusethedeclareoperatortocreateanambientdeclaration.
Inthefollowingcodesnippet,wewilldeclareaninterfacethatisimplementedbythecustomConsoleobject.WethenusethedeclareoperatortoaddthecustomConsoleobjecttothescope:
interfaceICustomConsole{
log(arg:string):void;
}
declarevarcustomConsole:ICustomConsole;
Note
Interfacesareexplainedingreaterdetaillaterinthechapter.
WecanthenusethecustomConsoleobjectwithoutcompilationerrors:
customConsole.log("Alogentry!");//ok
TypeScriptincludes,bydefault,afilenamedlib.d.tsthatprovidesinterfacedeclarationsforthebuilt-inJavaScriptlibraryaswellastheDOM.
Declarationfilesusethefileextension.d.tsandareusedtoincreasetheTypeScriptcompatibilitywiththird-partylibrariesandrun-timeenvironmentssuchasNode.jsorawebbrowser.
Note
WewilllearnhowtoworkwithdeclarationfilesinChapter2,AutomatingYourDevelopmentWorkflow.
Arithmeticoperators
TherefollowingarithmeticoperatorsaresupportedbytheTypeScriptprogramminglanguage.Inordertounderstandtheexamples,youmustassumethatvariableAholds10andvariableBholds20.
Operator Description Example
+ Thisaddstwooperands A+Bwillgive30
- Thissubtractsthesecondoperandfromthefirst A-Bwillgive-10
* Thismultipliesboththeoperands A*Bwillgive200
/ Thisdividesthenumeratorbythedenominator B/Awillgive2
% Thisisthemodulusoperatorandremainderafteranintegerdivision B%Awillgive0
++ Thisistheincrementoperatorthatincreasestheintegervalueby1 A++willgive11
-- Thisisthedecrementoperatorthatdecreasestheintegervalueby1 A--willgive9
Comparisonoperators
ThefollowingcomparisonoperatorsaresupportedbytheTypeScriptlanguage.Inordertounderstandtheexamples,youmustassumethatvariableAholds10andvariableBholds20.
Operator Description Example
== Thischeckswhetherthevaluesoftwooperandsareequalornot.Ifyes,thentheconditionbecomestrue.
(A==B)isfalse.A=="10"istrue.
=== Thischeckswhetherthevalueandtypeoftwooperandsareequalornot.Ifyes,thentheconditionbecomestrue.
A===Bisfalse.A==="10"isfalse.
!= Thischeckswhetherthevaluesoftwooperandsareequalornot.Ifthevaluesarenotequal,thentheconditionbecomestrue. (A!=B)istrue.
>Thischeckswhetherthevalueoftheleftoperandisgreaterthanthevalueoftherightoperand;ifyes,thentheconditionbecomes (A>B)isfalse.
true.
<Thischeckswhetherthevalueoftheleftoperandislessthanthevalueoftherightoperand;ifyes,thentheconditionbecomestrue.
(A<B)istrue.
>=Thischeckswhetherthevalueoftheleftoperandisgreaterthanorequaltothevalueoftherightoperand;ifyes,thentheconditionbecomestrue.
(A>=B)isfalse.
<=Thischeckswhetherthevalueoftheleftoperandislessthanorequaltothevalueoftherightoperand;ifyes,thentheconditionbecomestrue.
(A<=B)istrue.
Logicaloperators
ThefollowinglogicaloperatorsaresupportedbytheTypeScriptlanguage.Inordertounderstandtheexamples,youmustassumethatvariableAholds10andvariableBholds20.
Operator Description Example
&& ThisiscalledthelogicalANDoperator.Ifboththeoperandsarenonzero,thentheconditionbecomestrue.
(A&&B)istrue.
|| ThisiscalledlogicalORoperator.Ifanyofthetwooperandsarenonzero,thentheconditionbecomestrue.
(A||B)istrue.
!ThisiscalledthelogicalNOToperator.Itisusedtoreversethelogicalstateofitsoperand.Ifaconditionistrue,thenthelogicalNOToperatorwillmakeitfalse.
!(A&&B)isfalse.
Bitwiseoperators
ThefollowingbitwiseoperatorsaresupportedbytheTypeScriptlanguage.Inordertounderstandtheexamples,youmustassumethatvariableAholds2andvariableBholds3.
Operator Description Example
& ThisiscalledtheBitwiseANDoperator.ItperformsaBooleanANDoperationoneachbitofitsintegerarguments.
(A&B)is2
| ThisiscalledtheBitwiseORoperator.ItperformsaBooleanORoperationoneachbitofitsintegerarguments.
(A|B)is3.
^
ThisiscalledtheBitwiseXORoperator.ItperformsaBooleanexclusiveORoperationoneachbitofitsintegerarguments.ExclusiveORmeansthateitheroperandoneistrueoroperandtwoistrue,butnotboth.
(A^B)is1.
~ ThisiscalledtheBitwiseNOToperator.Itisaunaryoperatorandoperatesbyreversingallbitsintheoperand.
(~B)is-4
<<
ThisiscalledtheBitwiseShiftLeftoperator.Itmovesallbitsinitsfirstoperandtotheleftbythenumberofplacesspecifiedinthesecondoperand.Newbitsarefilledwithzeros.Shiftingavalueleftbyonepositionisequivalenttomultiplyingby2,shiftingtwopositionsisequivalenttomultiplyingby4,andsoon.
(A<<1)is4
>>ThisiscalledtheBitwiseShiftRightwithsignoperator.Itmovesallbitsinitsfirstoperandtotherightbythenumberofplacesspecifiedinthesecondoperand.
(A>>1)is1
>>>ThisiscalledtheBitwiseShiftRightwithzerooperators.Thisoperatorisjustlikethe>>operator,exceptthatthebitsshiftedinontheleftarealwayszero,
(A>>>1)is1
Note
OneofthemainreasonstousebitwiseoperatorsinlanguagessuchasC++,Java,orC#isthatthey'reextremelyfast.However,bitwiseoperatorsareoftenconsiderednotthatefficientinTypeScriptandJavaScript.BitwiseoperatorsarelessefficientinJavaScriptbecauseitisnecessarytocastfromfloatingpointrepresentation(howJavaScriptstoresallofitsnumbers)toa32-bitintegertoperformthebitmanipulationandback.
Assignmentoperators
ThefollowingassignmentoperatorsaresupportedbytheTypeScriptlanguage.
Operator Description Example
= Thisisasimpleassignmentoperatorthatassignsvaluesfromtheright-sideoperandstotheleft-sideoperand.
C=A+BwillassignthevalueofA+BintoC
+=ThisaddstheANDassignmentoperator.Itaddstherightoperandtotheleftoperandandassignstheresulttotheleftoperand.
C+=AisequivalenttoC=C+A
-=ThissubtractstheANDassignmentoperator.Itsubtractstherightoperandfromtheleftoperandandassignstheresulttotheleftoperand.
C-=AisequivalenttoC=C-A
*=ThismultipliestheANDassignmentoperator.Itmultipliestherightoperandwiththeleftoperandandassignstheresulttotheleftoperand.
C*=AisequivalenttoC=C*A
/=ThisdividestheANDassignmentoperator.Itdividestheleftoperandwiththerightoperandandassignstheresulttotheleftoperand.
C/=AisequivalenttoC=C/A
%=ThisisthemodulusANDassignmentoperator.Ittakesthemodulususingtwooperandsandassignstheresulttotheleftoperand.
C%=AisequivalenttoC=C%A
FlowcontrolstatementsThissectiondescribesthedecision-makingstatements,theloopingstatements,andthebranchingstatementssupportedbytheTypeScriptprogramminglanguage.
Thesingle-selectionstructure(if)
ThefollowingcodesnippetdeclaresavariableoftypeBooleanandnameisValid.Then,anifstatementwillcheckwhetherthevalueofisValidisequaltotrue.Ifthestatementturnsouttobetrue,theIsvalid!messagewillbedisplayedonthescreen.
varisValid:boolean=true;
if(isValid){
alert("isvalid!");
}
Thedouble-selectionstructure(if…else)
ThefollowingcodesnippetdeclaresavariableoftypeBooleanandnameisValid.Then,anifstatementwillcheckwhetherthevalueofisValidisequaltotrue.Ifthestatementturnsouttobetrue,themessageIsvalid!willbedisplayedonthescreen.Ontheotherside,ifthestatementturnsouttobefalse,themessageIsNOTvalid!willbedisplayedonthescreen.
varisValid:boolean=true;
if(isValid){
alert("Isvalid!");
}
else{
alert("IsNOTvalid!");
}
Theinlineternaryoperator(?)
Theinlineternaryoperatorisjustanalternativewayofdeclaringadouble-selectionstructure.
varisValid:boolean=true;
varmessage=isValid?"Isvalid!":"IsNOTvalid!";
alert(message);
TheprecedingcodesnippetdeclaresavariableoftypeBooleanandnameisValid.Thenitcheckswhetherthevariableorexpressionontheleft-handsideoftheoperator?isequaltotrue.
Ifthestatementturnsouttobetrue,theexpressionontheleft-handsideofthecharacterwillbeexecutedandthemessageIsvalid!willbeassignedtothemessagevariable.
Ontheotherhand,ifthestatementturnsouttobefalse,theexpressionontheright-handsideoftheoperatorwillbeexecutedandthemessage,IsNOTvalid!willbeassignedtothe
messagevariable.
Finally,thevalueofthemessagevariableisdisplayedonthescreen.
Themultiple-selectionstructure(switch)
Theswitchstatementevaluatesanexpression,matchestheexpression'svaluetoacaseclause,andexecutesstatementsassociatedwiththatcase.Aswitchstatementandenumerationsareoftenusedtogethertoimprovethereadabilityofthecode.
Inthefollowingexample,wewilldeclareafunctionthattakesanenumerationAlertLevel.Insidethefunction,wewillgenerateanarrayofstringstostoree-mailaddressesandexecuteaswitchstructure.Eachoftheoptionsoftheenumerationisacaseintheswitchstructure:
enumAlertLevel{
info,
warning,
error
}
functiongetAlertSubscribers(level:AlertLevel){
varemails=newArray<string>();
switch(level){
caseAlertLevel.info:
emails.push("[email protected]");
break;
caseAlertLevel.warning:
emails.push("[email protected]");
emails.push("[email protected]");
break;
caseAlertLevel.error:
emails.push("[email protected]");
emails.push("[email protected]");
emails.push("[email protected]");
break;
default:
thrownewError("Invalidargument!");
}
returnemails;
}
getAlertSubscribers(AlertLevel.info);//["[email protected]"]
getAlertSubscribers(AlertLevel.warning);//["[email protected]",
Thevalueofthelevelvariableistestedagainstallthecasesintheswitch.Ifthevariablematchesoneofthecases,thestatementassociatedwiththatcaseisexecuted.Oncethecasestatementhasbeenexecuted,thevariableistestedagainstthenextcase.
Oncetheexecutionofthestatementassociatedtoamatchingcaseisfinalized,thenextcasewillbeevaluated.Ifthebreakkeywordispresent,theprogramwillnotcontinuetheexecutionofthefollowingcasestatement.
Ifnomatchingcaseclauseisfound,theprogramlooksfortheoptionaldefaultclause,andiffound,ittransferscontroltothatclauseandexecutestheassociatedstatements.
Ifnodefaultclauseisfound,theprogramcontinuesexecutionatthestatementfollowingtheendofswitch.Byconvention,thedefaultclauseisthelastclause,butitdoesnothavetobeso.
Theexpressionistestedatthetopoftheloop(while)
Thewhileexpressionisusedtorepeatanoperationwhileacertainrequirementissatisfied.Forexample,thefollowingcodesnippet,declaresanumericvariablei.Iftherequirement(thevalueofiislessthan5)issatisfied,anoperationtakesplace(increasethevalueofiby1anddisplayitsvalueinthebrowserconsole).Oncetheoperationhascompleted,theaccomplishmentoftherequirementwillbecheckedagain.
vari:number=0;
while(i<5){
i+=1;
console.log(i);
}
Inawhileexpression,theoperationwilltakeplaceonlyiftherequirementissatisfied.
Theexpressionistestedatthebottomoftheloop(do…while)
Thedo-whileexpressionisusedtorepeatanoperationuntilacertainrequirementisnotsatisfied.Forexample,thefollowingcodesnippetdeclaresanumericvariableiandrepeatsanoperation(increasethevalueofiby1anddisplayitsvalueinthebrowserconsole)foraslongastherequirement(thevalueofiislessthan5)issatisfied.
vari:number=0;
do{
i+=1;
console.log(i);
}while(i<5);
Unlikethewhileloop,thedo-whileexpressionwillexecuteatleastonceregardlessoftherequirementvalueastheoperationwilltakeplacebeforecheckingifacertainrequirementissatisfiedornot.
Iterateoneachobject'sproperties(for…in)
Thefor-instatementbyitselfisnotabadpractice;however,itcanbemisused,forexample,toiterateoverarraysorarray-likeobjects.Thepurposeofthefor-instatementistoenumerateoverobjectproperties.
varobj:any={a:1,b:2,c:3};
for(varkeyinobj){
console.log(key+"="+obj[key]);
}
//Output:
//"a=1"
//"b=2"
//"c=3"
Thefollowingcodesnippetwillgoupintheprototypechain,alsoenumeratingtheinheritedproperties.Thefor-instatementiteratestheentireprototypechain,alsoenumeratingtheinheritedproperties.Whenyouwanttoenumerateonlytheobject'sownproperties(theonesthataren'tinherited),youcanusethehasOwnPropertymethod:
for(varkeyinobj){
if(obj.hasOwnProperty(prop)){
//propisnotinherited
}
}
Countercontrolledrepetition(for)
Theforstatementcreatesaloopthatconsistsofthreeoptionalexpressions,enclosedinparenthesesandseparatedbysemicolons,followedbyastatementorasetofstatementsexecutedintheloop.
for(vari:number=0;i<9;i++){
console.log(i);
}
Theprecedingcodesnippetcontainsaforstatement,itstartsbydeclaringthevariableiandinitializingitto0.Itcheckswhetheriislessthan9,performsthetwosucceedingstatements,andincrementsiby1aftereachpassthroughtheloop.
FunctionsJustasinJavaScript,TypeScriptfunctionscanbecreatedeitherasanamedfunctionorasananonymousfunction.Thisallowsustochoosethemostappropriateapproachforanapplication,whetherwearebuildingalistoffunctionsinanAPIoraone-offfunctiontohandovertoanotherfunction.
//namedfunction
functiongreet(name?:string):string{
if(name){
return"Hi!"+name;
}
else
{
return"Hi!";
}
}
//anonymousfunction
vargreet=function(name?:string):string{
if(name){
return"Hi!"+name;
}
else
{
return"Hi!";
}
}
Aswecanseeintheprecedingcodesnippet,inTypeScriptwecanaddtypestoeachoftheparametersandthentothefunctionitselftoaddareturntype.TypeScriptcaninferthereturntypebylookingatthereturnstatements,sowecanalsooptionallyleavethisoffinmanycases.
Thereisanalternativefunctionsyntax,whichusesthearrow(=>)operatorafterthefunction’sreturntypeandskipstheusageofthefunctionkeyword.
vargreet=(name:string):string=>{
if(name){
return"Hi!"+name;
}
else
{
return"Hi!mynameis"+this.fullname;
}
};
Thefunctionsdeclaredusingthissyntaxarecommonlyknownasarrowfunctions.Let'sreturntothepreviousexampleinwhichwewereassigningananonymousfunctiontothegreetvariable.Wecannowaddthetypeannotationstothegreetvariabletomatchtheanonymousfunctionsignature.
vargreet:(name:string)=>string=function(name:string):string{
if(name){
return"Hi!"+name;
}
else
{
return"Hi!";
}
};
Note
Keepinmindthatthearrowfunction(=>)syntaxchangesthewaythethisoperatorworkswhenworkingwithclasses.Wewilllearnmoreaboutthisintheupcomingchapters.
Nowyouknowhowtoaddtypeannotationstoforceavariabletobeafunctionwithaspecificsignature.Theusageofthiskindofannotationsisreallycommonwhenweuseacallback(functionsusedasanargumentofanotherfunction).
functionsume(a:number,b:number,callback:(result:number)=>void){
callback(a+b);
}
Intheprecedingexample,wearedeclaringafunctionnamedsumethattakestwonumbersandacallbackasafunction.Thetypeannotationswillforcethecallbacktoreturnvoidandtakeanumberasitsonlyargument.
Note
WewillfocusonfunctionsinChapter3,WorkingwithFunctions.
ClassesECMAScript6,thenextversionofJavaScript,addsclass-basedobjectorientationtoJavaScriptand,sinceTypeScriptisbasedonES6,developersareallowedtouseclass-basedobjectorientationtoday,andcompilethemdowntoJavaScriptthatworksacrossallmajorbrowsersandplatforms,withouthavingtowaitforthenextversionofJavaScript.
Let'stakealookatasimpleTypeScriptclassdefinitionexample:
classCharacter{
fullname:string;
constructor(firstname:string,lastname:string){
this.fullname=firstname+""+lastname;
}
greet(name?:string){
if(name)
{
return"Hi!"+name+"!mynameis"+this.fullname;
}
else
{
return"Hi!mynameis"+this.fullname;
}
}
}
varspark=newCharacter("Jacob","Keyes");
varmsg=spark.greet();
alert(msg);//"Hi!mynameisJacobKeyes"
varmsg1=spark.greet("Dr.Halsey");
alert(msg1);//"Hi!Dr.Halsey!mynameisJacobKeyes"
Intheprecedingexample,wehavedeclaredanewclassCharacter.Thisclasshasthreemembers:apropertycalledfullname,aconstructor,andamethodgreet.WhenwedeclareaclassinTypeScript,allthemethodsandpropertiesarepublicbydefault.
You'llnoticethatwhenwerefertooneofthemembersoftheclass(fromwithinitself)weprependthethisoperator.Thethisoperatordenotesthatit'samemberaccess.Inthelastlines,weconstructaninstanceoftheCharacterclassusinganewoperator.Thiscallsintotheconstructorwedefinedearlier,creatinganewobjectwiththeCharactershape,andrunningtheconstructortoinitializeit.
TypeScriptclassesarecompiledintoJavaScriptfunctionsinordertoachievecompatibilitywithECMAScript3andECMAScript5.
Note
Wewilllearnmoreaboutclassesandotherobject-orientedprogrammingconceptsinChapter4,Object-OrientedProgrammingwithTypeScript.
InterfacesInTypeScript,wecanuseinterfacestoenforcethataclassfollowthespecificationinaparticularcontract.
interfaceLoggerInterface{
log(arg:any):void;
}
classLoggerimplementsLoggerInterface{
log(arg){
if(typeofconsole.log==="function"){
console.log(arg);
}
else
{
alert(arg);
}
}
}
Intheprecedingexample,wehavedefinedaninterfaceloggerInterfaceandaclassLogger,whichimplementsit.TypeScriptwillalsoallowyoutouseinterfacestodeclarethetypeofanobject.Thiscanhelpustopreventmanypotentialissues,especiallywhenworkingwithobjectliterals:
interfaceUserInterface{
name:string;
password:string;
}
varuser:UserInterface={
name:"",
password:""//errorpropertypasswordismissing
};
Note
Wewilllearnmoreaboutinterfacesandotherobject-orientedprogrammingconceptsinChapter4,Object-OrientedProgrammingwithTypeScript.
NamespacesNamespaces,alsoknownasinternalmodules,areusedtoencapsulatefeaturesandobjectsthatshareacertainrelationship.Namespaceswillhelpyoutoorganizeyourcodeinamuchclearerway.TodeclareanamespaceinTypeScript,youwillusethenamespaceandexportkeywords.
namespaceGeometry{
interfaceVectorInterface{
/*...*/
}
exportinterfaceVector2dInterface{
/*...*/
}
exportinterfaceVector3dInterface{
/*...*/
}
exportclassVector2dimplementsVectorInterface,Vector2dInterface{
/*...*/
}
exportclassVector3dimplementsVectorInterface,Vector3dInterface{
/*...*/
}
}
varvector2dInstance:Geometry.Vector2dInterface=newGeometry.Vector2d();
varvector3dInstance:Geometry.Vector3dInterface=newGeometry.Vector3d();
Intheprecedingcodesnippet,wehavedeclaredanamespacethatcontainstheclassesvector2dandvector3dandtheinterfacesVectorInterface,Vector2dInterface,andVector3dInterface.Notethatthefirstinterfaceismissingthekeywordexport.Asaresult,theinterfaceVectorInterfacewillnotbeaccessiblefromoutsidethenamespace'sscope.
Note
InChapter4,Object-OrientedProgrammingwithTypeScript,we'llbecoveringnamespaces(internalmodules)andexternalmodulesandwe'lldiscusswheneachisappropriateandhowtousethem.
PuttingeverythingtogetherNowthatwehavelearnedhowtousethebasicTypeScriptbuildingblocksindividually,let'stakealookatafinalexampleinwhichwewillusemodules,classes,functions,andtypeannotationsforeachoftheseelements:
moduleGeometry{
exportinterfaceVector2dInterface{
toArray(callback:(x:number[])=>void):void;
length():number;
normalize();
}
exportclassVector2dimplementsVector2dInterface{
private_x:number;
private_y:number;
constructor(x:number,y:number){
this._x=x;
this._y=y;
}
toArray(callback:(x:number[])=>void):void{
callback([this._x,this._y]);
}
length():number{
returnMath.sqrt(this._x*this._x+this._y*this._y);
}
normalize(){
varlen=1/this.length();
this._x*=len;
this._y*=len;
}
}
}
Theprecedingexampleisjustasmallportionofabasic3DenginewritteninJavaScript.In3Dengines,therearealotofmathematicalcalculationsinvolvingmatricesandvectors.Asyoucansee,wehavedefinedamoduleGeometrythatwillcontainsomeentities;tokeeptheexamplesimple,wehaveonlyaddedtheclassVector2d.Thisclassstorestwocoordinates(xandy)in2dspaceandperformssomeoperationsonthecoordinates.Oneofthemostusedoperationsonvectorsisnormalization,whichisoneofthemethodsinourVector2dclass.
3Denginesarecomplexsoftwaresolutions,andasadeveloper,youaremuchmorelikelytouseathird-party3Denginethancreateyourown.Forthisreason,itisimportanttounderstandthatTypeScriptwillnotonlyhelpyoutodeveloplarge-scaleapplications,butalsotoworkwithlarge-scaleapplications.Inthefollowingcodesnippet,wewillusethemoduledeclaredearliertocreateaVector2dinstance:
varvector:Geometry.Vector2dInterface=newGeometry.Vector2d(2,3);
vector.normalize();
vector.toArray(function(vectorAsArray:number[]){
alert('x:'+vectorAsArray[0]+'y:'+vectorAsArray[1]);
});
ThetypecheckingandIntelliSensefeatureswillhelpuscreateaVector2dinstance,normalizeitsvalue,andconvertitintoanarraytofinallyshowitsvalueonscreenwithease.
SummaryInthischapter,youhavelearnedaboutthepurposesofTypeScript.YouhavealsolearnedaboutsomeofthedesigndecisionsmadebytheTypeScriptengineersatMicrosoft.
Towardstheendofthischapter,youlearnedalotaboutthebasicbuildingblocksofaTypeScriptapplication.YoustartedtowritesomeTypeScriptcodeforthefirsttimeandyoucannowworkwithtypeannotations,variablesandprimitivedatatypes,operators,flowcontrolstatements,functions,andclasses.
Inthenextchapter,youwilllearnhowtoautomateyourdevelopmentworkflow.
Chapter2.AutomatingYourDevelopmentWorkflowAftertakingafirstlookatthemainTypeScriptlanguagefeatures,wewillnowlearnhowtousesometoolstoautomateourdevelopmentworkflow.Thesetoolswillhelpustoreducetheamountoftimethatweusuallyspendonsimpleandrepetitivetasks.
Inthischapter,wewilllearnaboutthefollowingtopics:
AnoverviewofthedevelopmentworkflowSourcecontroltoolsPackagemanagementtoolsTaskrunnersTestrunnersIntegrationtoolsScaffoldingtools
AmoderndevelopmentworkflowDevelopingawebapplicationwithhighqualitystandardshasbecomeatime-consumingactivity.Ifwewanttoachieveagreatuserexperience,wewillneedtoensurethatourapplicationscanrunassmoothlyaspossibleonmanydifferentwebbrowsers,devices,Internetconnectionspeeds,andscreenresolutions.Furthermore,wewillneedtospendalotofourtimeworkingonqualityassuranceandperformanceoptimizationtasks.
Asdevelopers,weshouldtrytominimizethetimespentonsimpleandrepetitivetasks.Thismightsoundfamiliaraswehavebeendoingthisforyears.Westartedbywritingbuildscripts(suchasmakefiles)orautomatedtestsandtoday,inamodernwebdevelopmentworkflow,weusemanytoolstotrytoautomateasmanytasksaswecan.Thesetoolscanbecategorizedintothefollowinggroups:
SourcecontroltoolsPackagemanagementtoolsTaskrunnersTestrunnersContinuousintegrationtoolsScaffoldingtools
PrerequisitesYouareabouttolearnhowtowriteascript,whichwillautomatemanytasksinyourdevelopmentworkflow;however,beforethat,weneedtoinstallafewtoolsinourdevelopmentenvironment.
Node.jsNode.jsisaplatformbuiltonV8(Google'sopensourceJavaScriptengine).Node.jsallowsustorunJavaScriptoutsideawebbrowser.WecanwritebackendanddesktopapplicationsusingJavaScriptwithNode.js.
Wearenotgoingtowriteserver-sideJavaScriptapplicationsbutwearegoingtoneedNode.jsbecausemanyofthetoolsusedinthischapterareNode.jsapplications.
Ifyoudidn'tinstallNode.jsinthepreviouschapter,youcanvisithttps://nodejs.orgtodownloadtheinstallerforyouroperatingsystem.
AtomAtomisanopensourceeditordevelopedbytheGitHubteam.Theopensourcecommunityaroundthiseditorisreallyactiveandhasdevelopedmanypluginsandthemes.YoucandownloadAtomfromhttps://atom.io/.
Onceyouhavecompletedtheinstallation,opentheeditorandgotothepreferenceswindow.Youshouldbeabletofindasectionwithinthepreferenceswindowtomanagepackagesandanothertomanagethemesjustliketheonesthatwecanseeinthefollowingscreenshot:
Note
TheAtomuserinterfaceisslightlydifferentfromtheotheroperatingsystems.RefertotheAtomdocumentationathttps://atom.io/docsifyouneedadditionalhelptomanagepackagesandthemes.
Weneedtosearchfortheatom-typescriptpackageinthepackagemanagementsectionandinstallit.Wecanadditionallyvisitthethemessectionandinstallathemethatmakesusfeelmorecomfortablewiththeeditor.
Note
WewilluseAtominsteadofVisualStudiobecauseAtomisavailableforLinux,OSX,andWindows,soitwillsuitmostreaders.
Unfortunately,wewillnotcoverVisualStudioCodebecauseitwasannouncedwhenthisbookwasabouttobepublished.VisualStudioCodeisalightweightIDEdevelopedbyMicrosoftandavailableforfreeforWindows,OSX,andLinux.Youcanvisithttps://code.visualstudio.com/ifyouwishtolearnmoreaboutit.
IfyouwanttoworkwithVisualStudio,youwillbeabletofindtheextensiontoenable
TypescriptsupportinVisualStudioathttps://visualstudiogallery.msdn.microsoft.com/2d42d8dc-e085-45eb-a30b-3f7d50d55304.
Oneofthehighestratedthemesiscalledseti-uiandisparticularlyusefulbecauseitusesareallygoodsetoficonstohelpustoidentifyeachfileinourapplication.Forexample,thegulpfile.jsorbower.jsonfiles(wewilllearnaboutthesefileslater)arejustJavaScriptandJSONfilesbuttheseti-uithemeisabletoidentifythattheyaretheGulpandBowerconfigurationfilesrespectivelyandwilldisplaytheiriconsaccordingly.
Wecaninstallthisthemebyopeningtheconsoleofouroperatingsystemandrunningthefollowingcommands:
cd~/.atom/packages
gitclonehttps://github.com/jesseweed/seti-ui--depth=1
Note
YouneedtoinstallGittobeabletoruntheprecedingcommand.YouwillfindsomeinformationabouttheGitinstallationlateroninthischapter.
OncewehaveinstalledthethemeandTypeScriptplugin,wewillneedtoclosetheAtomeditorandopenitagaintomakethechangeseffective.Ifeverythinggoeswell,wewillgeta
confirmationmessageinthetop-rightcorneroftheeditorwindow.
GitandGitHubTowardstheendofthischapter,wewilllearnhowtoconfigureacontinuousintegrationbuildserver.Thebuildserverwillobservechangesinourapplication'scodeandensurethatthechangesdon'tbreaktheapplication.
Inordertobeabletoobservethechangesinthecode,wewillneedtouseasourcecontrolsystem.Thereareafewsourcecontrolsystemsavailable.SomeofthemostwidelyusedonesareSubversion,MercurialandGit.
Sourcecontrolsystemshavemanybenefits.First,theyenablemultipledeveloperstoworkonasourcefilewithoutanyworkbeingoverridden.
Second,sourcecontrolsystemsarealsoagoodwayofkeepingpreviouscopiesofafileorauditingitschanges.Thesefeaturescanbereallyuseful,forexample,whentryingtofindoutwhenanewbugwasintroducedforthefirsttime.
Whileworkingthroughtheexamples,wewillperformsomechangestothesourcecode.WewilluseGitandGitHubtomanagethesechanges.ToinstallGit,gotohttp://git-scm.com/downloadsanddownloadtheexecutableforyouroperatingsystem.Then,gotohttps://github.com/tocreateaGitHubaccount.WhilecreatingtheGitHubaccount,youwillbeofferedafewdifferentsubscriptionplans,thefreeplanofferseverythingweneedtofollowtheexamplesinthischapter.
SourcecontroltoolsNowthatwehaveinstalledGitandcreatedaGitHubaccount,wewilluseGitHubtocreateanewcoderepository.Arepositoryisacentralfilestoragelocation.Itisusedbythesourcecontrolsystemstostoremultipleversionsoffiles.Whilearepositorycanbeconfiguredonalocalmachineforasingleuser,itisoftenstoredonaserver,whichcanbeaccessedbymultipleusers.
Note
GitHuboffersfreesourcecontrolrepositoriesforopensourceprojects.GitHubisreallypopularwithintheopensourcecommunityandmanypopularprojectsarehostedonGitHub(includingTypeScript).However,GitHubisnottheonlyoptionavailableandyoucanusealocalGitrepositoryoranothersourcecontrolserviceprovidersuchasBitbucket.Ifyouwishtolearnmoreaboutthesealternatives,refertotheofficialGitdocumentationathttps://git-scm.com/docortheBitBucketwebsiteathttps://bitbucket.org/.
TocreateanewrepositoryonGitHub,logintoyourGitHubaccountandclickonthelinktocreateanewrepository,whichwecanfindinthetop-rightcornerofthescreen.
Awebformsimilartotheoneinthefollowingscreenshotwillthenappear.Thisformcontainssomefields,whichallowustosettherepository'sname,description,andprivacysettings.
WecanalsoaddaREADME.mdfile,whichusesmarkdownsyntaxandisusedtoaddwhatevertextwewanttotherepository'shomepageonGitHub.Furthermore,wecanaddadefault.gitignorefile,whichisusedtospecifyfilesthatwewouldliketobeignoredbyGitandthereforenotsavedintotherepository.
Lastbutnotleast,wecanalsoselectasoftwarelicensetocoveroursourcecode.Oncewehavecreatedtherepository,wewillnavigatetoourprofilepageonGitHub,findtherepositorythatwehavejustcreated,andgototherepository'spage.Ontherepository'spage,wewillbeabletofindthecloneURLatthebottom-rightcornerofthepage.
Weneedtocopytherepository'scloneURL,openaconsole,andusetheURLasanargumentofthegitclonecommand:
gitclonehttps://github.com/user-name/repository-name.git
Note
SometimestheWindowscommand-lineinterfaceisnotabletofindtheGitandNode.jscommands.
TheeasiestwaytogetaroundthisissueistousetheGitconsole(installedwithGit)ratherthanusingtheWindowscommandline.
IfyouwanttousetheWindowsconsole,youwillneedtomanuallyaddtheGitandNodeinstallationpathstotheWindowsPATHenvironmentvariable.
Also,notethatwewillusetheUNIXpathsyntaxinalltheexamples.
IfyouareworkingwithOSXorLinux,thedefaultcommand-lineinterfaceshouldworkfine.
Thecommandoutputshouldlooksimilartothis:
Cloninginto'repository-name'...
remote:Countingobjects:3,done.
remote:Compressingobjects:100%(3/3),done.
remote:Total3(delta2),reused0(delta0),pack-reused0
Unpackingobjects:100%(3/3),done.
Checkingconnectivity...done.
Wecanthenmoveinsidetherepositorybyusingthechangedirectorycommand(cd)andusethegitstatuscommandtocheckthelocalrepository'sstatus:
cdrepository-name
gitstatus
Onbranchmaster
Yourbranchisup-to-datewith'origin/master'.
nothingtocommit,workingdirectoryclean
Note
WewilluseGitHubthroughoutthisbook.However,ifyouwanttousealocalrepository,youcanusetheGitinitcommandtocreateanemptyrepository.
RefertotheGitdocumentationathttp://git-scm.com/docs/git-inittolearnmoreaboutthegitinitcommandandworkingwithalocalrepository.
Thegitstatuscommandistellingusthattherearenochangesinourworkingdirectory.Let'sopentherepositoryfolderinAtomandcreateanewfilecalledgulpfile.js.Now,runthegitstatuscommandagain,andwewillseethattherearesomenewuntrackedfiles:
Onbranchmaster
Yourbranchisup-to-datewith'origin/master'.
Untrackedfiles:
(use"gitadd<file>..."toincludeinwhatwillbecommitted)
gulpfile.js
nothingaddedtocommitbutuntrackedfilespresent(use"gitadd"totrack)
Note
ThefilesintheAtomprojectexploreraredisplayedusingacolorcode,whichwillhelpustoidentifywhetherafileisnew,orhaschangedsinceweclonedtherepository.
Whenwemakesomechanges,suchasaddinganewfileorchanginganexistingfile,weneedtoexecutethegitaddcommandtoindicatethatwewanttoaddthatchangetoasnapshot:
gitaddgulpfile.js
gitstatus
Onbranchmaster
Yourbranchisup-to-datewith'origin/master'.
Changestobecommitted:
(use"gitresetHEAD<file>..."tounstage)
newfile:gulpfile.js
Nowthatwehavestagedthecontentwewanttosnapshot,wehavetorunthegitcommitcommandtoactuallyrecordthesnapshot.Recordingasnapshotrequiresacommentaryfield,whichcanbeprovidedusingthegitcommitcommandtogetherwithits-margument:
gitcommit-m"addedthenewgulpfile.js"
Ifeverythinghasgonewell,thecommandoutputshouldbesimilartothefollowing:
[master2a62321]addedthenewfilegulpfile.js
1filechanged,1insertions(+)
createmode100644gulpfile.js
Tosharethecommitwithotherdevelopers,weneedtopushourchangestotheremoterepository.Wecandothisbyexecutingthegitpushcommand:
gitpush
ThegitpushcommandwillaskforourGitHubusernameandpasswordandthensendthechangestotheremoterepository.Ifwevisittherepository'spageonGitHub,wewillbeabletofindtherecentlycreatedfile.WewillreturntoGitHublaterinthischaptertoconfigureourcontinuousintegrationserver.
Note
Ifyouareworkinginalargeteam,youmightencountersomefileconflictswhenattemptingtopushsomechangestotheremoterepository.Resolvingafileconflictisoutofthescopeofthisbook;however,ifyouneedfurtherinformationaboutGit,youwillfindanextensiveusermanualathttps://www.kernel.org/pub/software/scm/git/docs/user-manual.html.
PackagemanagementtoolsPackagemanagementtoolsareusedfordependencymanagement,sothatwenolongerhavetomanuallydownloadandmanageourapplication'sdependencies.Wewilllearnhowtoworkwiththreedifferentpackagemanagementtools:Bower,npm,andtsd.
npmThenpmpackagemanagerwasoriginallydevelopedasthedefaultNode.jspackagemanagementtool,buttodayitisusedbymanytools.Npmusesaconfigurationfile,calledpackage.json,tostorereferencestoallthedependenciesinourapplication.Itisimportanttorememberthatwewillnormallyusenpmtoinstalldependenciesthatwewilluseontheserverside,inadesktopapplication,orwithdevelopmenttools.
Beforeweinstallanypackages,weshouldaddapackage.jsonfiletoourproject.Wecandoitbyexecutingthefollowingcommand:
npminit
Thenpminitcommandwillaskforsomebasicinformationaboutourproject,includingitsname,version,description,entrypoint,testcommand,Gitrepository,keywords,authorandlicense.
Note
Refertotheofficialnpmdocumentationathttps://docs.npmjs.com/files/package.jsonifyouareunsureaboutthepurposesofsomeofthepackage.jsonfieldsmentionedearlier.
Thenpmcommandwillthenshowusapreviewofthepackage.jsonfilethatisabouttobegeneratedandaskforourfinalconfirmation.
Note
RememberthatyouneedtohaveNode.jsinstalledtobeabletousethenpmcommandtool.
Aftercreatingtheproject'spackage.jsonfile,runthenpminstallcommandtoinstallourfirstdependency.Thenpminstallcommandtakesthenameofoneormultipledependenciesseparatedbyasinglespaceasanargumentandasecondargumenttoindicatethescopeoftheinstallation.
Thescopecanbe:
Adependencyatdevelopmenttime(testingframeworks,compilers,andsoon)Adependencyatruntime(awebframework,databaseORMs,andsoon)
Wewillusethegulp-typescriptnpmpackagetocompileourTypeScriptcode;so,let'sinstallitasadevelopmentdependency(usingthe--save-devargument):
npminstallgulp-typescript--save-dev
Toinstallaglobaldependency,wewillusethe-gargument:
npminstalltypescript-g
Note
Wemightneedadministrativeprivilegestoinstallpackageswithglobalscopeinourdevelopmentenvironment,aswealreadylearnedinthepreviouschapter.
Also,notethatnpmwillnotaddanyentriestoourpackage.jsonfilewheninstallingpackageswithglobalscopebutitisimportantthatwemanuallyaddtherightdependenciestothedevDependenciesandpeerDependenciessectionsinthepackage.jsonfiletoguaranteethatthecontinuousintegrationbuildserverwillresolveallourproject'sdependenciescorrectly.Wewilllearnaboutthecontinuousintegrationbuildserverindetaillaterinthischapter.
Toinstallaruntimedependency,usethe--saveargument:
npminstalljquery--save
Note
JQueryisprobablythemostpopularJavaScriptframeworkorlibraryevercreated.ItisusedtofacilitatetheusageofsomebrowserAPIswithouthavingtoworryaboutsomevendor-specificdifferencesintheAPIs.JQueryalsoprovidesuswithmanyhelpersthatwillhelpusreducetheamountofcodenecessarytoperformtaskssuchasselectinganHTMLnodewithinthetreeofnodesinanHTMLdocument.
ItisassumedthatthereadersofthisbookhaveagoodunderstandingofJQuery.IfyouneedtolearnmoreaboutJQuery,refertotheofficialdocumentationathttps://api.jquery.com/.
Oncewehaveinstalledsomedependenciesinthepackage.jsonfile,thecontentsshouldlooksimilartothis:
{
"name":"repository-name",
"version":"1.0.0",
"description":"example",
"main":"index.html",
"scripts":{
"test":"test"
},
"repository":{
"type":"git",
"url":"https://github.com/username/repository-name.git"
},
"keywords":[
"typescript",
"demo",
"example"
],
"author":"NameSurname",
"contributors":[],
"license":"MIT",
"bugs":{
"url":"https://github.com/username/repository-name/issues"
},
"homepage":"https://github.com/username/repository-name",
"engines":{},
"dependencies":{
"jquery":"^2.1.4"
},
"devDependencies":{
"gulp-typescript":"^2.8.0"
}
}
Note
Somefieldsinthepackage.jsonfilemustbeconfiguredmanually.Tolearnmoreabouttheavailablepackage.jsonconfigurationfields,visithttps://docs.npmjs.com/files/package.json.
Theversionsofthenpmpackagesusedthroughoutthisbookmayhavebeenupdatedsincethepublicationofthisbook.Refertothepackagesdocumentationathttps://npmjs.comtofindoutpotentialincompatibilitiesandlearnaboutnewfeatures.
Allthenpmpackageswillbesavedunderthenode_modulesdirectory.Weshouldaddthenode_modulesdirectorytoour.gitignorefileasitisrecommendedtoavoidsavingtheapplication'sdependenciesintosourcecontrol.Wecandothisbyopeningthe.gitignorefileandaddinganewlinethatcontainsthenameofthefolder(node_modules).
Thenexttimewecloneourrepository,wewillneedtodownloadallourdependenciesagain,buttodoso,wewillonlyneedtoexecutethenpminstallcommandwithoutanyadditionalparameters:
npminstall
Thepackagemanagerwillthensearchforthepackage.jsonfileandinstallallthedeclareddependencies.
Note
If,inthefuture,weneedtofindannpmpackagename,wewillbeabletousethenpmsearchenginesathttps://www.npmjs.cominordertofindit.
BowerBowerisanotherpackagemanagementtool.Itisreallysimilartonpmbutitwasdesignedspecificallytomanagefrontenddependencies.Asaresult,manyofthepackagesareoptimizedforitsusageinawebbrowser.
WecaninstallBowerbyusingnpm:
npminstall-gbower
Insteadofthepackage.jsonfile,Bowerusesaconfigurationfilenamedbower.json.WecanusethemajorityofthenpmcommandsandargumentsinBower.Forexample,wecanusethebowerinitcommandtocreatetheinitialbowerconfigurationfile:
bowerinit
Note
Theinitialconfigurationfileisquitesimilartothepackage.jsonfile.Refertotheofficialdocumentationathttp://bower.io/docs/config/ifyouwanttolearnmoreaboutthebower.jsonconfigurationfields.
Wecanalsousethebowerinstallcommandtoinstallapackage:
bowerinstalljquery
Furthermore,wecanalsousetheinstallscopearguments:
bowerinstalljquery--save
bowerinstalljasmine--save-dev
AlltheBowerpackageswillbesavedunderthebower_componentsdirectory.Asyouhavealreadylearned,itisrecommendedtoavoidsavingyourapplication'sdependenciesinyourremoterepository,soyoushouldalsoaddthebower_componentsdirectorytoyour.gitignorefile.
tsdInthepreviouschapter,welearnedthatTypeScriptbydefaultincludesafilelib.d.tsthatprovidesinterfacedeclarationsforthebuilt-inJavaScriptobjectsaswellastheDocumentObjectModel(DOM)andBrowserObjectModel(BOM)APIs.TheTypeScriptfileswiththeextension.d.tsareaspecialkindofTypeScriptfileknownastypedefinitionfilesordeclarationfiles.
Thetypedefinitionfilesusuallycontainthetypedeclarationsofthird-partylibraries.ThesefilesfacilitatetheintegrationbetweentheexistingJavaScriptlibrariesandTypeScript.If,forexample,wetrytoinvoketheJQueryinaTypeScriptfile,wewillgetanerror:
$.ajax({/**/});//cannotfindname'$'
Toresolvethisissue,weneedtoaddareferencetotheJQuerytypedefinitionfileinourTypeScriptcode,asshowninthefollowinglineofcode:
///<referencepath="jquery.d.ts">
Fortunately,wedon'tneedtocreatethetypedefinitionfilesbecausethereisanopensourceprojectknownasDefinitelyTypedthatalreadycontainstypedefinitionfilesformanyJavaScriptlibraries.IntheearlydaysofTypeScriptdevelopment,developershadtomanuallydownloadandinstallthetypedefinitionfilesfromtheDefinitelyTypedprojectwebsite,butthosedaysaregone,andtodaywecanuseamuchbettersolutionknownastsd.
ThetsdacronymstandsforTypeScriptDefinitionsanditisapackagemanagerthatwillhelpustomanagethetypedefinitionfilesrequiredbyourTypeScriptapplication.Justlikenpmandbower,tsdusesaconfigurationfilenamedtsd.jsonandstoresallthedownloadedpackagesunderadirectorynamedtypings.
Runthefollowingcommandtoinstalltsd:
npminstalltsd-g
Wecanusethetsdinitcommandtogeneratetheinitialtsd.jsonfileandthetsdinstallcommandtodownloadandinstalldependencies:
tsdinit//generatetsd.json
tsdinstalljquery--save//installjquerytypedefinitions
YoucanvisittheDefinitelyTypedprojectwebsiteathttps://github.com/borisyankov/DefinitelyTypedtosearchfortsdpackages.
TaskrunnersAtaskrunnerisatoolusedtoautomatetasksinthedevelopmentprocess.ThetaskcanbeusedtoperformawidevarietyofoperationssuchasthecompilationofTypeScriptfilesorthecompressionofJavaScriptfiles.ThetwomostpopularJavaScripttaskrunnersthesedaysareGruntandGulp.
Gruntstartedtobecomepopularinearly2012andsincethentheopensourcecommunityhasdevelopedalargenumberofGrunt-compatibleplugins.
Ontheotherhand,Gulpstartedtobecomepopularinlate2013;therefore,therearelesspluginsavailableforGulp,butitisquicklycatchingupwithGrunt.
Besidesthenumberofpluginsavailable,themaindifferencebetweenGulpandGruntisthatwhileinGruntwewillworkusingfilesastheinputandoutputofourtasks,inGulpwewillworkwithstreamsandpipesinstead.Gruntisconfiguredusingsomeconfigurationfieldsandvalues.However,Gulppreferscodeoverconfiguration.ThisapproachmakestheGulpconfigurationsomehowmoreminimalisticandeasiertoread.
Note
Inthisbook,wewillworkwithGulp;however,ifyouwanttolearnmoreaboutGrunt,youcandosoathttp://gruntjs.com/.
InordertogainagoodunderstandingofGulp,wecanusetheprojectthatwehavealreadycreatedandaddsomeextrafoldersandfilestoit.Alternatively,wecanstartanewprojectfromscratch.Wewillconfiguresometasks,whichwillreferencepaths,folders,andfilesnumeroustimes,sothefollowingdirectorytreestructureshouldhelpusunderstandeachofthesetasks:
├──LICENSE
├──README.md
├──index.html
├──gulpfile.js
├──karma.conf.js
├──tsd.json
├──package.json
├──bower.json
├──source
│└──ts
│└──*.ts
├──test
│└──main.test.ts
├──data
│└──*.json
├──node_modules
│└──...
├──bower_components
│└──...
└──typings
└──...
Note
Acopyofafinishedexampleprojectisprovidedinthecompanionsourcecode.Thecodeisprovidedtohelpyoufollowthecontent.Youcanusethefinishedprojecttohelpimprovetheunderstandingoftheconceptsdiscussedintherestofthischapter.
Let'sstartbyinstallinggulpgloballywithnpm:
npminstall-ggulp
Theninstallgulpinourpackage.jsondevDependencies:
npminstall--save-devgulp
CreateaJavaScriptfilenamedgulpfile.jsinsidetherootfolderofourproject,whichshouldcontainthefollowingpieceofcode:
vargulp=require('gulp');
gulp.task('default',function(){
console.log('HelloGulp!');
});
And,finally,rungulp(wemustexecutethiscommandfromwherethegulpfile.jsfileislocated):
gulp
WehavecreatedourfirstGulptask,whichisnameddefault.Whenwerunthegulpcommand,itwillautomaticallytrytosearchforthegulpfile.jsfileinthecurrentdirectory,andoncefound,itwilltrytofindthedefaulttask.
CheckingthequalityoftheTypeScriptcodeThedefaulttaskisnotperforminganyoperationsintheprecedingexample,butwewillnormallyuseaGulppluginineachtask.Wewillnowaddasecondtask,whichwillusethegulp-tslintplugintocheckwhetherourTypeScriptcodefollowsaseriesofrecommendedpractices.
Weneedtoinstallthepluginwithnpm:
npminstallgulp-tslint--save-dev
Wecanthenloadthepluginintoourgulpfile.jsfileandaddanewtask:
vartslint=require('gulp-tslint');
gulp.task('lint',function(){
returngulp.src([
'./source/ts/**/**.ts','./test/**/**.test.ts'
]).pipe(tslint())
.pipe(tslint.report('verbose'));
});
Wehavenamedthenewtasklint.Let'stakealookattheoperationsperformedbythelinttask,stepbystep:
1. Thegulpsrcfunctionwillfetchthefilesinthedirectorylocatedat./source/tsanditssubdirectorieswiththefileextension.ts.Wewillalsofetchallthefilesinthedirectorylocatedat./testanditssubdirectorieswiththefileextension.test.ts.
2. Theoutputstreamofthesrcfunctionwillbethenredirectedusingthepipefunctiontobeusedasthetslintfunctioninput.
3. Finally,wewillusetheoutputofthetslintfunctionastheinputofthetslint.reportfunction.
Nowthatwehaveaddedthelinttask,wewillmodifythegulpfile.jsfiletoindicatethatwewanttorunlintasasubtaskofthedefaulttask:
gulp.task('default',['lint']);
Note
Manypluginsallowustoindicatethatsomefilesshouldbeignoredbyaddingtheexclamationsymbol(!)beforeapath.Forexample,thepath!path/*.d.tswillignoreallfileswiththeextension.d.ts;thisisusefulwhenthedeclarationfilesandsourcecodefilesarelocatedinthesamedirectory.
CompilingtheTypeScriptcodeWewillnowaddtwonewtaskstocompileourTypeScriptcode(onefortheapplication'slogicandonefortheapplication'sunittests).
Wewillusethegulp-typescriptplugin,soremembertoinstallitasadevelopmentdependencyusingthenpmpackagemanager,justaswedidpreviouslyinthischapter:
npminstall-ggulp-typescript
Wecanthencreateanewgulp-typescriptprojectobject:
varts=require('gulp-typescript');
vartsProject=ts.createProject({
removeComments:true,
noImplicitAny:true,
target:'ES3',
module:'commonjs',
declarationFiles:false
});
Note
Ithasbeenannouncedthatthegulp-typescriptpluginwillsoonsupporttheusageofaspecialJSONfilenamedtsconfig.json.ThisfileisusedtostoretheTypeScriptcompilerconfiguration.Whenthefileisavailable,itisusedbythecompilerduringthecompilationprocess.
Thetsconfig.jsonfileisusefulbecauseitpreventsusfromhavingtowriteallthedesiredcompilerparameterswhenusingitsconsoleinterface.Refertothegulp-typescriptdocumentation,whichcanbefoundathttps://www.npmjs.com/package/gulp-typescript,tolearnmoreaboutthisfeature.
Intheprecedingcodesnippet,wehaveloadedtheTypeScriptcompilerasadependencyandthencreatedanobjectnamedtsProject,whichcontainsthesettingstobeusedbytheTypeScriptcompilerduringthecompilationofourcode.Wearenowreadytocompileourapplication'ssourcecode:
gulp.task('tsc',function(){
returngulp.src('./source/ts/**/**.ts')
.pipe(ts(tsProject))
.js.pipe(gulp.dest('./temp/source/js'));
});
Thetsctaskwillfetchallthe.tsfilesinthedirectorylocatedat./source/tsanditssubdirectoriesandpassthemasastreamtotheTypeScriptcompiler.ThecompilerwillusethecompilationsettingspassedasthetsProjectargumentandthensavetheoutputJavaScriptfilesintothepath./temp/sources/js.
WealsoneedtocompilesomeunittestswritteninTypeScript.ThetestsarelocatedinthetestfolderandwewanttheoutputJavaScriptfilestobestoredundertemp/test.Usingthesameprojectconfigurationobjectinadifferenttaskandwithdifferentinputfilescanresultinbadperformanceandunexpectedbehavior;soweneedtoinitializeanothergulp-typescriptprojectobject.ThistimewewillnametheobjecttsTestProject:
vartsTestProject=ts.createProject({
removeComments:true,
noImplicitAny:true,
target:'ES3',
module:'commonjs',
declarationFiles:false
});
Thetsc-testtaskisalmostidenticaltothetsctask,butinsteadofcompilingtheapplication'scode,itwillcompiletheapplication'stests.Sincethesourceandtestarelocatedindifferentdirectories,wehaveuseddifferentpathsinthistask:
gulp.task('tsc-tests',function(){
returngulp.src('./test/**/**.test.ts')
.pipe(ts(tsTestProject))
.js.pipe(gulp.dest('./temp/test/'));
});
Wewillupdatethedefaulttaskoncemoreinordertoperformthenewtasks:
gulp.task('default',['lint','tsc','tsc-tests']);
OptimizingaTypeScriptapplicationWhenwecompileourTypescriptcode,thecompilerwillgenerateaJavaScriptfileforeachcompiledTypeScriptfile.Ifweruntheapplicationinawebbrowser,thesefileswon'treallybeusefulontheirownbecausetheonlywaytousethemwouldbetocreateanindividualHTMLscripttagforeachoneofthem.
Alternatively,wecouldfollowtwodifferentapproaches:
Wecoulduseatool,suchastheRequireJSlibrary,toloadeachofthosefilesondemandusingAJAX.Thisapproachisknownasasynchronousmoduleloading.Tofollowthisapproach,wewillneedtochangetheconfigurationoftheTypeScriptcompilertousetheasynchronousmoduledefinition(AMD)notation.WecouldconfiguretheTypeScriptcompilertousetheCommonJSmodulenotationanduseatool,suchasBrowserify,totracetheapplication'smodulesanddependenciesandgenerateahighlyoptimizedsinglefile,whichwillcontainalltheapplication'smodules.
Inthisbook,wewillusetheCommonJSmethodbecauseitishighlyintegratedwithBrowserifyandGulp.
Note
IfyouhaveneverworkedwithAMDorCommonJSmodulesbefore,don'tworrytoomuchaboutitfornow.WewillfocusonmodulesinChapter4,Object-OrientedProgrammingwithTypeScript.
Wecanfindtheapplication'srootmodule(namedmain.tsinourexample)inthecompanioncode.Thisfilecontainsthefollowingcode:
///<referencepath="./references.d.ts"/>
import{headerView}from'./header_view';
import{footerView}from'./footer_view';
import{loadingView}from'./loading_view';
headerView.render();
footerView.render();
loadingView.render();
Tip
Theprecedingimportstatementsareusedtoaccessthecontentsofsomeexternalmodules.WewilllearnmoreaboutexternalmodulesinChapter4,Object-OrientedProgrammingwithTypeScript.
Whencompiled(usingtheCommonJSmodulenotation),theoutputJavaScriptcodewilllooklikethis:
varheaderView=require('./header_view');
varfooterView=require('./footer_view');
varloadingView=require('./loading_view');
headerView.render();
footerView.render();
loadingView.render();
Aswecanseeinthefirstthreelines,themain.jsfiledependsontheotherthreeJavaScriptfiles:header_view.js,footer_view.js,andloading_view.js.Ifwecheckthecompanioncode,wewillseethatthesefilesalsohavesomedependencies.
Wewillnormallyrefertothesedependenciesasmodules.Importingamoduleallowsustousethepublicparts(alsoknownastheexportedparts)ofamodulefromanothermodule.
Browserifyisabletotracethefulltreeofdependenciesandgenerateahighlyoptimizedsinglefile,whichwillcontainalltheapplication'smodulesanddependencies.
Wewillnowaddtwonewtaskstoourautomatedbuild(gulpfile.js).Inthefirstone,wewillconfigureBrowserifytotracethedependenciesofourapplication'smodules.Inthesecondone,wewillconfigureBrowserifytotracethedependenciesofourapplication'sunittests.
Weneedtoinstallsomepackagesbeforeimplementingthenewtask:
npminstallbrowserifyvinyl-transformgulp-uglifygulp-sourcemaps
Wecanthenimportthemodulesandwritesomeinitializationcode:
Varbrowserify=require('browserify'),
transform=require('vinyl-transform'),
uglify=require('gulp-uglify'),
sourcemaps=require('gulp-sourcemaps');
varbrowserified=transform(function(filename){
varb=browserify({entries:filename,debug:true});
returnb.bundle();
});
Intheprecedingcodesnippet,wehaveloadedtherequiredpluginsanddeclaredafunctionnamedbrowserified,whichisrequiredforcompatibilityreasons.ThebrowserifiedfunctionwilltransformaregularNode.jsstreamintoaGulp(bufferedvinyl)stream.
Let'sproceedtoimplementtheactualtask:
gulp.task('bundle-js',function(){
returngulp.src('./temp/source/js/main.js')
.pipe(browserified)
.pipe(sourcemaps.init({loadMaps:true}))
.pipe(uglify())
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest('./dist/source/js/'));
});
Thetaskwejustdefinedwilltakethefilemain.jsastheentrypointofourapplicationandtracealltheapplication'smodulesanddependenciesfromthispoint.ItwillthengenerateonesinglestreamcontainingahighlyoptimizedJavaScript.
Wewillthenusetheuglifyplugintominimizetheoutputsize.Thereducedfilesizewillreducetheapplication'sloadingtime,butwillmakeithardertodebug.Wewillalsogenerateasourcemapfiletofacilitatethedebuggingprocess.
Note
Uglifyremovesalllinebreaksandwhitespacesandreducesthelengthofsomevariablenames.Thesourcemapfilesallowustomapthereducedfiletoitsoriginalcodewhiledebugging.
Asourcemapprovidesawayofmappingcodewithinacompressedfilebacktoitsoriginalpositioninasourcefile.Thismeanswecaneasilydebuganapplicationevenafteritsassetshavebeenoptimized.TheChromeandFirefoxdevelopertoolsbothshipwithbuilt-insupportforsourcemaps.
Thebundle-testtaskisreallysimilartotheprevioustask.Thistime,wewillavoidusinguglifyandsourcemapsbecauseusuallywewon'tneedtooptimizethedownloadtimesofourunittests.Asyoucansee,wedon'thaveasingleentrypointbecausewewillallowtheexistenceofmultipleentrypoints(eachentrypointwillbelikedtoacollectionofautomatedtestsknownastestsuite.Don'tworryifyouarenotfamiliarwiththisterm,aswewilllearnmoreaboutitinChapter7,ApplicationTesting):
gulp.task('bundle-test',function(){
returngulp.src('./temp/test/**/**.test.js')
.pipe(browserified)
.pipe(gulp.dest('./dist/test/'));
});
Finally,wehavetoupdatethedefaulttasktoalsoperformthenewtasks:
gulp.task('default',['lint','tsc','tsc-tests','bundle-js','bundle-test']);
Note
WehavecreatedatasktocompiletheTypeScriptfilesintoJavaScriptfiles.TheJavaScriptfilesarestoredinatemporaryfolderandasecondtaskbundlesalltheJavaScriptfilesintoasinglefile.Inarealcorporateenvironment,itisnotrecommendedtostorefilestemporarilywhenworkingwithGulp.Wecanperformalltheseoperationswithonesingletaskbypassingtheoutputstreamofanoperationastheinputofthefollowingoperation.However,inthisbook,wewilltrytosplitthetaskstofacilitatetheunderstandingofeachtask.
Ifwetrytoexecutethedefaulttaskafteraddingthesechanges,wewillprobablyexperiencesomeissuesbecausethetasksareexecutedinparallelbydefault.Wewillnowlearnhowtocontrolthetask'sexecutionordertoavoidthiskindofissue.
ManagingtheGulptasks'executionorderSometimeswewillneedtorunourtasksinacertainorder(forexample,weneedtocompileourTypeScriptintoJavaScriptbeforewecanexecuteourunittests).Controllingthetasks'executionordercanbechallengingsinceinGulpallthetasksareasynchronousbydefault.
Therearethreewaystomakeatasksynchronous:
PassinginacallbackReturningastreamReturningapromise
Tip
RefertoChapter3,WorkingwithFunctionstolearnmoreabouttheusagecallbacksandpromises.
Let'stakealookatthefirsttwoways(wewillnotcovertheusageofpromisesinthischapter):
//Passingacallback(cb)
gulp.task('sync',function(cb){//notethecbargument
//setTimeoutcouldbeanyasynctask
setTimeout(function(){
cb();//notethecbusagehere
},1000);
});
//Returningastream
gulp.task('sync',function(){
returngulp.src('js/*.js')//notethereturnkeywordhere
.pipe(concat('script.min.js')
.pipe(uglify())
.pipe(gulp.dest('../dist/js');
});
Nowthatwehaveasynchronoustask,wecancombineitwiththetaskdependencynotationtomanagetheexecutionorder:
gulp.task('secondTask',['sync'],function(){
//thistaskwillnotstartuntil
//thesynctaskisalldone!
});
Intheprecedingcodesnippet,thesecondTasktaskwillnotstartuntilthesynctaskisdone.Now,let'simaginethatthereisathirdtasknamedthirdTask.WewillwritethefollowingcodesnippethopingthatitwillexecutethesynctaskbeforethethirdTasktaskandfinallythedefaulttask,butitwillinfactrunthesynctaskandthirdTasktaskinparallel:
gulp.task('default',['sync','thirdTask'],function(){
//dostuff
});
Fortunately,wecaninstalltherun-sequenceGulppluginvianpm,whichwillallowustohavebettercontroloverthetaskexecutionorder:
varrunSequence=require('run-sequence');
gulp.task('default',function(cb){
runSequence(
'lint',//lint
['tsc','tsc-tests'],//compile
['bundle-js','bundle-test'],//optimize
'karma'//test
'browser-sync',//serve
cb//callback
);
});
Theprecedingcodesnippetwillruninthefollowingorder:
1. lint.2. tscandtsc-testsinparallel.3. bundle-jsandbundle-testinparallel.4. karma.5. browser-sync.
Note
TheGulpdevelopmentteamannouncedplanstoimprovethemanagementofthetaskexecutionorderwithouttheneedforexternalpluginswhenthisbookwasabouttobepublished.RefertotheGulpdocumentationandreleasenotesonfuturereleasestolearnmoreaboutit.Thedocumentationcanbefoundathttps://github.com/gulpjs/gulp/blob/master/docs/README.md.
TestrunnersAtestrunnerisatoolthatallowsustoautomatetheexecutionofourapplication'sunittests.
Note
Unittestingreferstothepracticeoftestingcertainfunctionsandareas(units)ofourcode.Thisgivesustheabilitytoverifythatourfunctionsworkasexpected.Itisassumedthatthereaderhassomeunderstandingoftheunittestprocess,butthetopicsexploredherewillbecoveredinamuchhigherlevelofdetailinChapter7,ApplicationTesting.
Wecanuseatestrunnertoautomaticallyexecuteourapplication'stestsuitesinmultiplebrowsersinsteadofhavingtomanuallyopeneachwebbrowserinordertoexecutethetests.
WewilluseatestrunnerknownasKarma.Karmaiscompatiblewithmultipleunittestingframeworks,butwewillusetheMochatestingframeworktogetherwithtwolibraries:Chai(anassertionlibrary)andSinon(amockingframework).
Note
Youdon'tneedtoworrytoomuchabouttheselibrariesrightnowbecausewewillfocusontheirusageinChapter7,ApplicationTesting.
Let'sstartbyusingnpmtoinstallthetestingframeworkthatwearegoingtouse:
npminstallmochachaisinon--save-dev
Wewillcontinuebyinstallingthekarmatestrunnerandsomedependencies:
npminstallkarmakarma-mochakarma-chaikarma-sinonkarma-coveragekarma-
phantomjs-launchergulp-karma--save-dev
Afterinstallingallthenecessarypackages,wehavetoaddanewGulptasktothegulpfile.jsfile.Thenewtaskwillruntheapplication'sunittestsusingKarma:
Varkarma=require("gulp-karma");
gulp.task('karma',function(cb){
gulp.src('./dist/test/**/**.test.js')
.pipe(karma({
configFile:'karma.conf.js',
action:'run'
}))
.on('end',cb)
.on('error',function(err){
//Makesurefailedtestscausegulptoexitnon-zero
throwerr;
});
});
Intheprecedingcodesnippet,wearefetchingallthefileswiththeextension.test.jsunderthedirectorylocatedat./dist/test/andallitssubdirectories.WewillthenpassthefilestotheKarmaplugintogetherwiththelocationofthekarma.conf.jsfile,whichcontainstheKarmaconfiguration.WewillcreateanewJavaScriptfilenamedkarma.conf.jsintheproject'srootdirectoryandcopythefollowingcodeintoit:
module.exports=function(config){
'usestrict';
config.set({
basePath:'',
frameworks:['mocha','chai','sinon'],
browsers:['PhantomJS'],
reporters:['progress','coverage'],
plugins:[
'karma-coverage',
'karma-mocha',
'karma-chai',
'karma-sinon',
'karma-phantomjs-launcher'
],
preprocessors:{
'./dist/test/*.test.js':['coverage']
},
port:9876,
colors:true,
autoWatch:false,
singleRun:false,
logLevel:config.LOG_INFO
});
};
TheconfigurationfiletellsKarmaabouttheapplication'sbasepath,frameworks(Mocha,Chai,andSinon.JS),browsers(PhantomJS),plugins,andreportersthatwewanttouseduringthetests'execution.PhantomJSisaheadlesswebbrowser,itisusefulbecauseitcanexecutetheunittestwithoutactuallyhavingtoopenawebbrowser.
Note
WeshouldrunthetestsinrealwebbrowsersalongwithPhantomJSbeforedoingaproductiondeployment.ThereareKarmaplugins,suchaskarma-firefox-launcherandkarma-chrome-launcher,whichwillallowustoruntheunittestsinthebrowsersofourchoice.
Karmausestheprogressreporterbydefaulttoletusknowthestatusofthetestexecutionprocess.Weaddedthecoveragereporteraswellbecausewewanttohaveanideaofwhatpercentageofourapplication'scodehasbeentestedwithunittests.Afteraddingthecoveragereporterandrunningourunittestswewillbeabletofindthecoveragereportunderafoldernamedcoverage,whichshouldbelocatedinthesamedirectorywherethekarma.conf.jsfilewaslocated.
IfwelookattheKarmaconfigurationdocumentationathttp://karma-runner.github.io/0.8/config/configuration-file.html,wewillnoticethatwearemissingthefilesfieldinourkarma.conf.jsfile.Wedidn'tindicatethelocationofourunittestsbecausetheGulptaskwillpassthestream,whichcontainstheunittests',filestoKarma,andthentheKarmataskisexecuted.
Synchronizedcross-devicetestingWewilladdonelasttasktothegulpfile.jsfile,whichwillallowustorunourapplicationinawebbrowser.Weneedtoinstallthebrowser-syncpackagebyusingnpm:
npminstall-gbrowser-sync
Wewillthencreatetwonewtasks.Thesetasksarejustusedtogroupafewtasksintoonemaintask.WearedoingthisbecausesometimeswewanttorefreshawebpagetoseetheeffectofchangingsomeTypeScriptcodeandweneedtorunanumberoftasks(compilation,bundling,andsoon)beforewecanactuallyseethechangesinawebbrowser.Bygroupingallthesetasksintohigher-leveltasks,wecansavesometimeandmakeourconfigurationfilesmorereadable:
gulp.task('bundle',function(cb){
runSequence('build',[
'bundle-js','bundle-test'
],cb);
});
gulp.task('test',function(cb){
runSequence('bundle',['karma'],cb);
});
Theprecedingtwotasksareusedtogroupallthebuild-relatedtasksintoahigher-leveltask(namedbundle)andtogroupallthetest-relatedtasksintoahigher-leveltask(namedtest).
Afterinstallingthepackageandimplementingtheprecedingtwotasks,wecanaddanewGulptasktothegulpfile.jsfile:
varbrowserSync=require('browser-sync');
gulp.task('browser-sync',['test'],function(){
browserSync({
server:{
baseDir:"./dist"
}
});
returngulp.watch([
"./dist/source/js/**/*.js",
"./dist/source/css/**.css",
"./dist/test/**/**.test.js",
"./dist/data/**/**",
"./index.html"
],[browserSync.reload]);
});
Inthistask,weareconfiguringBrowserSynctohostinthelocalwebserverallthestaticfilesunderthedistdirectory.Wethenusethegulpwatchfunctiontoindicatethat,ifthecontentofanyofthefilesunderthedistdirectorychanges,BrowserSyncshouldautomaticallyrefresh
ourwebbrowser.
Whensomechangesaredetected,thetesttaskisinvoked.Becausethetesttaskinvokesthebundletasks,anychangeswilltriggertheentireprocess(buildandtest)beforerefreshingthewebpageanddisplayingthenewfilesinawebbrowser.
BrowserSyncisareallypowerfultool,itallowsustotestinonedeviceandautomaticallyrepeatouractions(clicks,scrolls,andsoon)onasmanydevicesaswewant.Itwillalsoallowustodebugourapplicationsremotely,whichcanbereallyusefulwhenwearetestinganapplicationonmobiledevices.
Synchronizingdevicesisreallysimple.Ifwerunthebrowser-synctask,theapplicationwillbelaunchedinthedefaultwebbrowser.Ifwelookattheconsoleoutput,wewillseethattheapplicationisrunninginoneURL(http://localhost:3000)andtheBrowserSynctoolsareavailableinasecondURL(http://localhost:3001):
[BS]AccessURLs:
---------------------------------------
Local:http://localhost:3000
External:http://192.168.241.17:3000
---------------------------------------
UI:http://localhost:3001
UIExternal:http://192.168.241.17:3001
---------------------------------------
[BS]Servingfilesfrom:./dist
IfweopenanothertabinourbrowserpointingtotheBrowserSynctoolsURL(http://localhost:3001,intheexample),wewillaccesstheBrowserSynctoolsuserinterface:
WecanusetheBrowserSynctoolsuserinterfacetoaccesstheremotedebuggingoptionsanddevicesynchronizationoptions.Tosynchronizeanewdevice,wejustneedtouseaphoneortabletconnectedtothesamelocalareanetworkandopentheindicatedexternalURLinthedevice'swebbrowser.
IfyouwishtolearnmoreaboutBrowserSync,visittheofficialprojectdocumentationathttp://www.browsersync.io/docs/.
ContinuousIntegrationtoolsContinuousIntegration(CI)isadevelopmentpracticethathelpstopreventpotentialintegrationissues.Softwareintegrationissuesreferstothedifficultiesthatmayariseduringthepracticeofcombiningindividuallytestedsoftwarecomponentsintoanintegratedwhole.Softwareisintegratedwhencomponentsarecombinedintosubsystemsorwhensubsystemsarecombinedintoproducts.
Componentsmaybeintegratedafterallofthemareimplementedandtested,asinawaterfallmodelorabigbangapproach.Ontheotherhand,CIrequiresdeveloperstocommittheircodedailyintoaremotecoderepository.Eachcommitisthenverifiedbyanautomatedbuild,allowingteamstodetectintegrationissuesearlier.
Inthischapter,wehavecreatedaremotecoderepositoryandanautomatedbuild,butwehaven'tconfiguredatooltoobserveourcommitsandruntheautomatebuildaccordingly.WeneedaCIserver.TherearemanyoptionswhenitcomestochoosingaCIserver,butexploringtheseoptionsisoutofthescopeofthisbook.WewillworkwithTravisCIbecauseitishighlyintegratedwithGitHubandisfreeforopensourceprojectsandlearningpurposes.
ToconfigureTravisCI,weneedtovisitthewebsitehttps://travis-ci.organdloginusingourGitHubcredentials.Oncewehaveloggedin,wewillbeabletoseealistofourpublicGitHubrepositoriesandwillalsobeabletoenabletheCI.
Tofinishtheconfiguration,weneedtoaddafilenamedtravis.ymltoourapplication'srootdirectory,whichcontainstheTravisCIconfiguration:
language:node_js
node_js:
-"0.10""
Note
TherearemanyotheravailableTravisCIconfigurationoptions.Refertohttp://docs.travis-ci.com/tolearnmoreabouttheavailableoptions.
Aftercompletingthesetwosmallconfigurationsteps,TravisCIwillbereadytoobservethecommitstoourremotecoderepository.
Note
Ifthebuildworksinthelocaldevelopmentenvironment,butfailsintheCIserver,wewill
havetocheckthebuilderrorlogandtrytofigureoutwhatwentwrong.ChancesarethatthesoftwareversionsinourenvironmentwillbeaheadoftheonesintheCIserverandwewillneedtoindicatetoTravisCIthatadependencyneedstobeinstalledorupdated.WecanfindtheTravisCIdocumentationathttp://docs.travis-ci.com/user/build-configuration/tolearnhowtoresolvethiskindofissue.
ScaffoldingtoolsAscaffoldingtoolisusedtoautogeneratetheprojectstructure,buildscripts,andmuchmore.ThemostpopularscaffoldingtoolthesedaysisYeoman.Yeomanusesaninternalcommandknownasyo,apackagemanager,andataskrunnerofourchoicetogenerateprojectsbasedontemplates.
Theprojecttemplatesareknownasgeneratorsandtheopensourcecommunityhasalreadypublishedmanyofthem,soweshouldbeabletofindonethatmoreorlesssuitsourneeds.Alternatively,wecanwriteandpublishourownYeomangenerator.
WewillnowcreateanewprojecttoshowcasehowYeomancanhelpustosavesometime.Yeomanwillgeneratethepackage.jsonandbower.jsonfilesandautomaticallyinstallsomedependenciesforus.
Theyocommandcanbeinstalledusingnpm:
npminstall-gyo
Afterinstallingtheyocommand,wewillneedtoinstallatleastonegenerator.Weneedtofindageneratorforthekindofprojectthatwewishtocreate.
WearegoingcreateanewprojectusingGulpasthetaskrunnerandTypeScripttoshowcasetheusageofYeoman.Wecanuseageneratorcalledgenerator-typescript.Thelistofavailablegeneratorscanbefoundonlineathttp://yeoman.io/generators/.
Wecaninstallageneratorbyusingnpm:
npminstall-ggenerator-typescript
Afterinstallingthegenerator,wecanuseitwiththehelpoftheyocommand:
yotypescript
If,forexample,wealsowantedtouseSass,wecouldusethegenerator-gulp-sass-typescriptgeneratorinstead:
npminstall-ggenerator-gulp-sass-typescript
Someofthegeneratorsareinteractiveandwillallowustoselectwhetherwewanttoaddsomeoptionalthird-partylibrariestotheprojectornot.Let'srunthegeneratortoseewhatitlookslike:
yogenerator-gulp-sass-typescript
Thescreenthatisdisplayedcontainsaseriesofstepstoguideusthroughtheprocessofcreatinganewproject,whichincludesGulpasthetaskrunner,SassastheCSSpreprocessor,
andTypeScriptastheprogramminglanguage:
Afterexecutingthegenerator,theprojecttemplatewillgenerateadirectorytreesimilartothefollowingone:
├──app
│├──index.html
│├──sass
││└──styles.scss
│├──scripts
││└──main.js
│├──styles
││└──styles.css
│└──ts
│└──main.ts
├──bower.json
├──bower_components
│└──...
├──gulpfile.js
├──node_modules
│└──...
└──package.json
Thebower.json,package.json,andgulpfile.jsfiles(theGulptaskrunnerconfiguration)areautogeneratedandwillsaveusaconsiderableamountoftime.
Note
Itisneveragoodideatoletatoolgeneratesomecodeforusifwedon'treallyunderstandwhatthatcodedoes.WhileinthefutureyoushoulddefinitelyconsiderusingYeomantogenerateanewproject,itisrecommendedtogainagoodunderstandingoftaskandtestrunnersbeforeusingascaffoldingtool.
SummaryInthischapter,youlearnedhowtoworkwithasourcecontrolrepositoryandhowtouseGulptomanagethetasksinanautomatedbuild.TheautomatedbuildhelpsustovalidatethequalityoftheTypeScriptcode,compileit,testit,andoptimizeit.Youalsolearnedhowtoinstallthird-partypackagesandTypeScripttypedefinitionsforthosethird-partycomponents.
Towardstheendofthechapter,youlearnedhowtousetheautomatedbuildandacontinuousintegrationservertoreducetheimpactofpotentialintegrationissues.
Inthenextchapter,youwilllearnaboutfunctions.
Chapter3.WorkingwithFunctionsInChapter1,IntroducingTypeScript,wetookafirstlookattheusageoffunctions.FunctionsarethefundamentalbuildingblockofanyapplicationinTypeScript,andtheyarepowerfulenoughtodeservethededicationofanentirechaptertoexploretheirpotential.
Inthischapter,wewilllearntoworkwithfunctionsindepth.Thechapterisdividedintotwomainsections.Inthefirstsection,wewillstartwithaquickrecapofsomebasicconceptsandthenmoveontosomelesscommonlyknownfunctionfeaturesandusecases.Thefirstsectionincludesthefollowingconcepts:
FunctiondeclarationandfunctionexpressionsFunctiontypesFunctionswithoptionalparametersFunctionswithdefaultparametersFunctionswithrestparametersFunctionoverloadingSpecializedoverloadingsignatureFunctionscopeImmediatelyinvokedfunctionsGenericsTagfunctionsandtaggedtemplates
ThesecondsectionfocusesonTypeScriptasynchronousprogrammingcapabilitiesandincludesthefollowingconcepts:
CallbacksandhigherorderfunctionsArrowfunctionsCallbackhellPromisesGeneratorsAsynchronousfunctions(asyncandawait)
WorkingwithfunctionsinTypeScriptInthissection,wewillfocusonthedeclarationandusageoffunctions,parameters,andarguments.WewillalsointroduceoneofthemostpowerfulfeaturesofTypeScript:Generics.
FunctiondeclarationsandfunctionexpressionsInthefirstchapter,weintroducedthepossibilityofdeclaringfunctionswith(namedfunction)orwithout(unnamedoranonymousfunction)explicitlyindicatingitsname,butwedidn'tmentionthatwewerealsousingtwodifferenttypesoffunction.
Inthefollowingexample,thenamedfunctiongreetNamedisafunctiondeclarationwhilegreetUnnamedisafunctionexpression.Ignorethefirsttwolines,whichcontaintwoconsolelogstatements,fornow:
console.log(greetNamed("John"));
console.log(greetUnnamed("John"));
functiongreetNamed(name:string):string{
if(name){
return"Hi!"+name;
}
}
vargreetUnnamed=function(name:string):string{
if(name){
return"Hi!"+name;
}
}
Wemightthinkthattheseprecedingfunctionsarereallysimilar,buttheywillbehavedifferently.Theinterpretercanevaluateafunctiondeclarationasitisbeingparsed.Ontheotherhand,thefunctionexpressionispartofanassignmentandwillnotbeevaluateduntiltheassignmenthasbeencompleted.
Note
Themaincauseofthedifferentbehaviorofthesefunctionsisaprocessknownasvariablehoisting.Wewilllearnmoreaboutthevariablehoistingprocesslaterinthischapter.
IfwecompiletheprecedingTypeScriptcodesnippetintoJavaScriptandtrytoexecuteitinawebbrowser,wewillobservethatthefirstalertstatementwillworkbecauseJavaScriptknowsaboutthedeclarationfunctionandcanparseitbeforetheprogramisexecuted.
However,thesecondalertstatementwillthrowanexception,whichindicatesthatgreetUnnamedisnotafunction.TheexceptionisthrownbecausethegreetUnnamedassignmentmustbecompletedbeforethefunctioncanbeevaluated.
FunctiontypesWealreadyknowthatitispossibletoexplicitlydeclarethetypeofanelementinourapplicationbyusingtheoptionaltypedeclarationannotation:
functiongreetNamed(name:string):string{
if(name){
return"Hi!"+name;
}
}
Intheprecedingfunction,wehavespecifiedthetypeoftheparametername(string)anditsreturntype(string).Sometimes,wewillneedtonotjustspecifythetypesofthefunctionelements,butalsothefunctionitself.Let'stakealookatanexample:
vargreetUnnamed:(name:string)=>string;
greetUnnamed=function(name:string):string{
if(name){
return"Hi!"+name;
}
}
Intheprecedingexample,wehavedeclaredthevariablegreetUnnamedanditstype.ThetypeofgreetUnnamedisafunctiontypethattakesastringvariablecallednameasitsonlyparameterandreturnsastringafterbeinginvoked.Afterdeclaringthevariable,afunction,whosetypemustbeequaltothevariabletype,isassignedtoit.
WecanalsodeclarethegreetUnnamedtypeandassignafunctiontoitinthesamelineratherthandeclaringitintwoseparatelineslikewedidinthepreviousexample:
vargreetUnnamed:(name:string)=>string=function(name:string):string
{
if(name){
return"Hi!"+name;
}
}
Justlikeinthepreviousexample,theprecedingcodesnippetalsodeclaresavariablegreetUnnamedanditstype.Wewillassignafunctiontothisvariableinthesamelineinwhichitisdeclared.Theassignedfunctionmustbeequaltothevariabletype.
Note
Intheprecedingexample,wehavedeclaredthetypeofthegreetUnnamedvariableandthenassignedafunctionasitsvalue.Thetypeofthefunctioncanbeinferredfromtheassignedfunction,andforthisreason,itisunnecessarytoaddaredundanttypeannotation.Wehavedonethistofacilitatetheunderstandingofthissection,butitisimportanttomentionthataddingredundanttypeannotationscanmakeourcodehardertoread,anditisconsideredbadpractice.
FunctionswithoptionalparametersUnlikeJavaScript,theTypeScriptcompilerwillthrowanerrorifweattempttoinvokeafunctionwithoutprovidingtheexactnumberandtypeofparametersthatitssignaturedeclares.Let'stakealookatacodesampletodemonstrateit:
functionadd(foo:number,bar:number,foobar:number):number{
returnfoo+bar+foobar;
}
Theprecedingfunctioniscalledaddandwilltakethreenumbersasparameters:namedfoo,bar,andfoobar.Ifweattempttoinvokethisfunctionwithoutprovidingexactlythreenumbers,wewillgetacompilationerrorindicatingthatthesuppliedparametersdonotmatchthefunction'ssignature:
add();//Suppliedparametersdonotmatchanysignature
add(2,2);//Suppliedparametersdonotmatchanysignature
add(2,2,2);//returns6
Therearescenariosinwhichwemightwanttobeabletocallthefunctionwithoutprovidingallitsarguments.TypeScriptfeaturesoptionalparametersinfunctionstohelpustoincreasetheflexibilityofourfunctions.WecanindicatetoTypeScriptthatwewantafunction'sparametertobeoptionalbyappendingthecharacter?toitsname.Let'supdatethepreviousfunctiontotransformtherequiredparameterfoobarintoanoptionalparameter:
functionadd(foo:number,bar:number,foobar?:number):number{
varresult=foo+bar;
if(foobar!==undefined){
result+=foobar;
}
returnresult;
}
Notehowwehavechangedthefoobarparameternameintofoobar?,andhowwearecheckingthetypeoffoobarinsidethefunctiontoidentifyiftheparameterwassuppliedasanargumenttothefunctionornot.Afterdoingthesechanges,theTypeScriptcompilerwillallowustoinvokethefunctionwithouterrorswhenwesupplytwoorthreeargumentstoit:
add();//Suppliedparametersdonotmatchanysignature
add(2,2);//returns4
add(2,2,2);//returns6
Itisimportanttonotethattheoptionalparametersmustalwaysbelocatedaftertherequiredparametersinthefunction'sparameterslist.
FunctionswithdefaultparametersWhenafunctionhassomeoptionalparameters,wemustcheckifanargumenthasbeenpassedtothefunction(justlikewedidinthepreviousexample).
Therearesomescenariosinwhichitwouldbemoreusefultoprovideadefaultvalueforaparameterwhenitisnotsuppliedthantomakeitanoptionalparameter.Let'srewritetheaddfunction(fromtheprevioussection)usingtheinlineifstructure:
functionadd(foo:number,bar:number,foobar?:number):number{
returnfoo+bar+(foobar!==undefined?foobar:0);
}
Thereisnothingwrongwiththeprecedingfunction,butwecanimproveitsreadabilitybyprovidingadefaultvalueforthefoobarparameterinsteadofflaggingitasanoptionalparameter:
functionadd(foo:number,bar:number,foobar:number=0):number{
returnfoo+bar+foobar;
}
Toindicatethatafunctionparameterisoptional,wejustneedtoprovideadefaultvalueusingthe=operatorwhendeclaringthefunction'ssignature.TheTypeScriptcompilerwillgenerateanifstructureintheJavaScriptoutputtosetadefaultvalueforthefoobarparameterifitisnotpassedasanargumenttothefunction:
functionadd(foo,bar,foobar){
if(foobar===void0){foobar=0;}
returnfoo+bar+foobar;
}
Void0isusedbytheTypeScriptcompilertocheckifavariableisequaltoundefined.Whilemostdevelopersusetheundefinedvariable,mostcompilersusevoid0.
Justlikeoptionalparameters,defaultparametersmustbealwayslocatedafteranyrequiredparametersinthefunction'sparameterlist.
FunctionswithrestparametersWehaveseenhowtouseoptionalanddefaultparameterstoincreasethenumberofwaysthatwecaninvokeafunction.Let'sreturnonemoretimetothepreviousexample:
functionadd(foo:number,bar:number,foobar:number=0):number{
returnfoo+bar+foobar;
}
Wehaveseenhowtomakepossibletheusageoftheaddfunctionwithtwoorthreeparameters,butwhatifwewantedtoallowotherdeveloperstopassfourorfiveparameterstoourfunction?Wewouldhavetoaddtwoextradefaultoroptionalparameters.Andwhatifwewantedtoallowthemtopassasmanyparametersastheymayneed?Thesolutiontothispossiblescenarioistheuseofrestparameters.Therestparametersyntaxallowsustorepresentanindefinitenumberofargumentsasanarray:
functionadd(...foo:number[]):number{
varresult=0;
for(vari=0;i<foo.length;i++){
result+=foo[i];
}
returnresult;
}
Aswecanseeinthefollowingcodesnippet,wehavereplacedthefunctionparametersfoo,bar,andfoobarwithjustoneparameter:foo.Notethatthenameoftheparameterfooisprecededbyanellipsis(asetofthreeperiods—nottheactualellipsischaracter).Arestparametermustbeofanarraytypeorwewillgetacompilationerror.Wecannowinvoketheaddfunctionwithasmanyparametersaswemayneed:
add();//returns0
add(2);//returns2
add(2,2);//returns4
add(2,2,2);//returns6
add(2,2,2,2);//returns8
add(2,2,2,2,2);//returns10
add(2,2,2,2,2,2);//returns12
Althoughthereisnospecificlimittothetheoreticalmaximumnumberofargumentsthatafunctioncantake,thereare,ofcourse,practicallimits.Theselimitsareentirelyimplementation-dependentand,mostlikely,willalsodependexactlyonhowwearecallingthefunction.
JavaScriptfunctionshaveabuilt-inobjectcalledtheargumentsobject.Thisobjectisavailableasalocalvariablenamedarguments.Theargumentsvariablecontainsanobjectsimilartoanarray,whichcontainstheargumentsusedwhenthefunctionwasinvoked.
Note
Theargumentsobjectexposessomeofthemethodsandpropertiesprovidedbyastandardarray,butnotallofthem.Refertothecompletereferenceathttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/argumentstolearnmoreaboutitspeculiarities.
IfweexaminetheJavaScriptoutput,wewillnoticethatTypeScriptiteratestheargumentsobjectinordertoaddthevaluestothefoovariable:
functionadd(){
varfoo=[];
for(var_i=0;_i<arguments.length;_i++){
foo[_i-0]=arguments[_i];
}
varresult=0;
for(vari=0;i<foo.length;i++){
result+=foo[i];
}
returnresult;
}
Wecanarguethatthisisanextra,unnecessaryiterationoverthefunction'sparameters.Eventhoughishardtoimaginethisextraiterationbecomingaperformanceissue,ifyouthinkthatthiscouldbeaproblemfortheperformanceofyourapplication,youmaywanttoconsideravoidingusingrestparametersanduseanarrayastheonlyparameterofthefunctioninstead:
functionadd(foo:number[]):number{
varresult=0;
for(vari=0;i<foo.length;i++){
result+=foo[i];
}
returnresult;
}
Theprecedingfunctiontakesanarrayofnumbersasitsonlyparameter.TheinvocationAPIwillbealittledifferentfromtherestparameters,butwewilleffectivelyavoidtheextraiterationoverthefunction'sargumentlist:
add();//Suppliedparametersdonotmatchanysignature
add(2);//Suppliedparametersdonotmatchanysignature
add(2,2);//Suppliedparametersdonotmatchanysignature
add(2,2,2);//Suppliedparametersdonotmatchanysignature
add([]);//returns0
add([2]);//returns2
add([2,2]);//returns4
add([2,2,2]);//returns6
FunctionoverloadingFunctionoverloadingormethodoverloadingistheabilitytocreatemultiplemethodswiththesamenameandadifferentnumberofparametersortypes.InTypeScript,wecanoverloadafunctionbyspecifyingallfunctionsignaturesofafunction,followedbyasignatureknownastheimplementationsignature.Let'stakealookatanexample:
functiontest(name:string):string;//overloadedsignature
functiontest(age:number):string;//overloadedsignature
functiontest(single:boolean):string;//overloadedsignature
functiontest(value:(string|number|boolean)):string;{//implementation
signature
switch(typeofvalue){
case"string":
return`Mynameis${value}.`;
case"number":
return`I'm${value}yearsold.`;
case"boolean":
returnvalue?"I'msingle.":"I'mnotsingle.";
default:
console.log("InvalidOperation!");
}
}
Note
Youmightnotbefamiliarwiththesyntaxusedinsomeofthestringsintheprecedingcodesnippet.ThissyntaxisknownasTemplateStrings.Templatestringsareenclosedbytheback-tick(``)characterinsteadofdoubleorsinglequotes.Templatestringscancontainplaceholders.Theseareindicatedbythedollarsignandcurlybraces(${expression}).Theexpressionsintheplaceholdersandthetextbetweenthemgetpassedtoafunction.Thedefaultfunctionjustconcatenatesthepartsintoasinglestring.
Aswecanseeintheprecedingexample,wehaveoverloadedthefunctiontestthreetimesbyaddingasignaturethattakesastringasitsonlyparameter,anotherfunctionthattakesanumber,andafinalsignaturethattakesaBooleanasitsuniqueparameter.Itisimportanttonotethatallfunctionsignaturesmustbecompatible;soif,forexample,oneofthesignaturestriestoreturnanumberwhileanothertriestoreturnastring,wewillgetacompilationerror.
Theimplementationsignaturemustbecompatiblewithalltheoverloadedsignatures,alwaysbethelastinthelist,andtakeanyorauniontypeasthetypeofitsparameters.
Invokingtheimplementationsignaturedirectlywillcauseacompilationerror:
test("Remo");//returns"MynameisRemo."
test(26);//returns"I'm26yearsold.";
test(false);//returns"I'mnotsingle.";
test({custom:"custom"});//error
SpecializedoverloadingsignaturesWecanuseaspecializedsignaturetocreatemultiplemethodswiththesamenameandnumberofparametersbutadifferentreturntype.Tocreateaspecializedsignature,wemustindicatethetypeoffunctionparameterusingastring.Thestringliteralisusedtoidentifywhichofthefunctionoverloadsisinvoked:
interfaceDocument{
createElement(tagName:"div"):HTMLDivElement;//specialized
createElement(tagName:"span"):HTMLSpanElement;//specialized
createElement(tagName:"canvas"):HTMLCanvasElement;//specialized
createElement(tagName:string):HTMLElement;//non-specialized
}
Intheprecedingexample,wehavedeclaredthreespecializedoverloadedsignaturesandonenon-specializedsignatureforthefunctionnamedcreateElement.
Whenwedeclareaspecializedsignatureinanobject,itmustbeassignabletoatleastonenon-specializedsignatureinthesameobject.Thiscanbeobservedintheprecedingexample,asthecreateElementpropertybelongstoatypethatcontainsthreespecializedsignatures,allofwhichareassignabletothenon-specializedsignatureinthetype.
Whenwritingoverloadeddeclarations,wemustlistthenon-specializedsignaturelast.
Note
Rememberthat,asseeninChapter1,IntroducingTypeScript,wecanalsouseuniontypestocreateamethodwiththesamenameandnumberofparametersbutadifferenttype.
FunctionscopeLow-levellanguagessuchasChavelow-levelmemorymanagementfeatures.InprogramminglanguageswithahigherlevelofabstractionsuchasTypeScript,valuesareallocatedwhenvariablesarecreatedandautomaticallyclearedfrommemorywhentheyarenotusedanymore.TheprocessthatcleansthememoryisknownasgarbagecollectionandisperformedbytheJavaScriptruntimegarbagecollector.
Thegarbagecollectorgenerallydoesagreatjob,butitisamistaketoassumethatitwillalwayspreventusfromfacingamemoryleak.Thegarbagecollectorwillclearavariablefromthememorywheneverthevariableisoutofthescope.IsimportanttounderstandhowtheTypeScriptscopeworkssoweunderstandthelifecycleofthevariables.
Someprogramminglanguagesusethestructureoftheprogramsourcecodetodeterminewhatvariableswearereferringto(lexicalscoping),whileothersusetheruntimestateoftheprogramstacktodeterminewhatvariablewearereferringto(dynamicscoping).Themajorityofmodernprograminglanguagesuselexicalscoping(includingTypeScript).Lexicalscopingtendstobedramaticallyeasiertounderstandforbothhumansandanalysistoolsthandynamicscoping.
Whileinmostlexicalscopedprogramminglanguages,variablesarescopedtoablock(asectionofcodedelimitedbycurlybraces{}),inTypeScript(andJavaScript),variablesarescopedtoafunction:
functionfoo():void{
if(true){
varbar:number=0;
}
alert(bar);
}
foo();//shows0
Theprecedingfunctionnamedfoocontainsanifstructure.Wehavedeclaredanumericvariablenamedbarinsidetheifstructure,andlaterwehaveattemptedtoshowthevalueofthevariablebarusingthealertfunction.
Wemightthinkthattheprecedingcodesamplewouldthrowanerrorinthefifthlinebecausethebarvariableshouldbeoutofthescopewhenthealertfunctionisinvoked.However,ifweinvokethefoofunction,thealertfunctionwillbeabletodisplaythevariablebarwithouterrorsbecauseallthevariablesinsideafunctionwillbeinthescopeoftheentirefunctionbody,eveniftheyareinsideanotherblockofcode(exceptafunctionblock).
Thismightseemreallyconfusing,butitiseasytounderstandonceweknowthat,atruntime,allthevariabledeclarationsaremovedtothetopofafunctionbeforethefunctionisexecuted.Thisbehavioriscalledhoisting.
Note
TypeScriptiscompiledtoJavaScriptandthenexecuted—thismeansthataTypeScriptapplicationisaJavaScriptapplicationatruntime,andforthisreason,whenwerefertotheTypeScriptruntime,wearetalkingabouttheJavaScriptruntime.WewilllearnindepthabouttheruntimeinChapter5,Runtime.
So,beforetheprecedingcodesnippetisexecuted,theruntimewillmovethedeclarationofthevariablebartothetopofourfunction:
functionfoo():void{
varbar:number;
if(true){
bar=0;
}
alert(bar);
}
Thismeansthatwecanuseavariablebeforeitisdeclared.Let'stakealookatanexample:
functionfoo2():void{
bar=0;
varbar:number;
alert(bar);
}
foo2();
Intheprecedingcodesnippet,wehavedeclaredafunctionfoo2,andinitsbody,wehaveassignedthevalue0toavariablenamedbar.Atthispoint,thevariablehasnotbeendeclared.Inthesecondline,weareactuallydeclaringthevariablebaranditstype.Inthelastline,wearedisplayingthevalueofbarusingthealertfunction.
Becausedeclaringavariableanywhereinsideafunction(exceptanotherfunction)isequivalenttodeclaringitatthetopofthefunction,thefoo2functionistransformedintothefollowingatruntime:
functionfoo2():void{
varbar:number;
bar=0;
alert(bar);
}
foo2();
BecausedeveloperswithaJavaorC#backgroundarenotusedtothefunctionscope,itisoneofthemostcriticizedcharacteristicsofJavaScript.ThepeopleinchargeofthedevelopmentoftheECMAScript6specificationareawareofthisand,asaresult,theyhaveintroducedthekeywordsletandconst.
Theletkeywordallowsustosetthescopeofavariabletoablock(if,while,for…)ratherthanafunctionblock.Wecanupdatethefirstexampleinthissectiontoshowcasehowletworks:
functionfoo():void{
if(true){
letbar:number=0;
bar=1;
}
alert(bar);//error
}
Thebarvariableisnowdeclaredusingtheletkeywordand,asaresult,itisonlyaccessibleinsidetheifblock.Thevariableisnothoistedtothetopofthefoofunctionandcannotbeaccessedbythealertfunctionoutsidetheifstatement.
Whilevariablesdefinedwithconstfollowthesamescoperulesasvariablesdeclaredwithlet,theycan'tbereassigned:
functionfoo():void{
if(true){
constbar:number=0;
bar=1;//error
}
alert(bar);//error
}
Ifweattempttocompiletheprecedingcodesnippet,wewillgetanerrorbecausethebarvariableisnotaccessibleoutsidetheifstatement(justlikewhenweusedtheletkeyword),andanewerroroccurswhenwetrytoassignanewvaluetothebarvariable.Theseconderroriscausedbecauseitisnotpossibletoassignavaluetoaconstantvariableoncethevariablehasalreadybeeninitialized.
ImmediatelyinvokedfunctionsAnimmediatelyinvokedfunctionexpression(IIFE)isadesignpatternthatproducesalexicalscopeusingfunctionscoping.IIFEcanbeusedtoavoidvariablehoistingfromwithinblocksortopreventusfrompollutingtheglobalscope.Forexample:
varbar=0;//global
(function(){
varfoo:number=0;//inscopeofthisfunction
bar=1;//inglobalscope
console.log(bar);//1
console.log(foo);//0
})();
console.log(bar);//1
console.log(foo);//error
Intheprecedingexample,wehavewrappedthedeclarationoftwovariables(fooandbar)withanIIFE.ThefoovariableisscopedtotheIIFEfunctionandisnotavailableintheglobalscope,whichexplainstheerrorwhentryingtoaccessitinthelastline.
WecanalsopassavariabletotheIIFEtohavebettercontroloverthecreationofvariablesoutsideitsownscope:
varbar=0;//global
(function(global){
varfoo:number=0;//inscopeofthisfunction
bar=1;//inglobalscope
console.log(global.bar);//1
console.log(foo);//0
})(this);
console.log(bar);//1
console.log(foo);//error
Thistime,theIIFEtakesthethisoperatorasitsonlyargument,whichpointstotheglobalscope,becausewearenotinvokingthethisoperatorfromwithinafunction.InsidetheIIFE,thethisoperatorispassedasaparameternamedglobal.Wecanthenachievemuchbettercontrolovertheobjectswewanttodeclareintheglobalscope(bar)andthosewedon't(foo).
Furthermore,IIFEcanhelpustosimultaneouslyallowpublicaccesstomethodswhileretainingprivacyforvariablesdefinedwithinthefunction.Let'stakealookatanexample:
classCounter{
private_i:number;
constructor(){
this._i=0;
}
get():number{
returnthis._i;
}
set(val:number):void{
this._i=val;
}
increment():void{
this._i++;
}
}
varcounter=newCounter();
console.log(counter.get());//0
counter.set(2);
console.log(counter.get());//2
counter.increment();
console.log(counter.get());//3
console.log(counter._i);//Error:Property'_i'isprivate
Note
Byconvention,TypeScriptandJavaScriptdevelopersusuallynameprivatevariableswithnamesprecededbyanunderscore(_).
WehavedefinedaclassnamedCounterthathasaprivatenumericattributenamed_i.Theclassalsohasmethodstogetandsetthevalueoftheprivateproperty_i.WehavealsocreatedaninstanceoftheCounterclassandinvokedthemethodsset,get,andincrementtoobservethateverythingisworkingasexpected.Ifweattempttoaccessthe_ipropertyinaninstanceofCounter,wewillgetanerrorbecausethevariableisprivate.
IfwecompiletheprecedingTypeScriptcode(onlytheclassdefinition)andexaminethegeneratedJavaScriptcode,wewillseethefollowing:
varCounter=(function(){
functionCounter(){
this._i=0;
}
Counter.prototype.get=function(){
returnthis._i;
};
Counter.prototype.set=function(val){
this._i=val;
};
Counter.prototype.increment=function(){
this._i++;
};
returnCounter;
})();
ThisgeneratedJavaScriptcodewillworkperfectlyinmostscenarios,butifweexecuteitinabrowserandtrytocreateaninstanceofCounterandaccessitsproperty_i,wewillnotgetanyerrorsbecauseTypeScriptwillnotgenerateruntimeprivatepropertiesforus.Sometimeswewillneedtowriteourfunctionsinsuchawaythatsomepropertiesareprivateatruntime,forexample,ifwereleasealibrarythatwillbeusedbyJavaScriptdevelopers.Wecanuse
IIFEtosimultaneouslyallowpublicaccesstomethodswhileretainingprivacyforvariablesdefinedwithinthefunction:
varCounter=(function(){
var_i:number=0;
functionCounter(){
}
Counter.prototype.get=function(){
return_i;
};
Counter.prototype.set=function(val:number){
_i=val;
};
Counter.prototype.increment=function(){
_i++;
};
returnCounter;
})();
Intheprecedingexample,everythingisalmostidenticaltoTypeScript'sgeneratedJavaScript,exceptthatthevariable_ibeforewasanattributeoftheCounterclass,andnowitisanobjectintheCounterclosure.
Note
Closuresarefunctionsthatrefertoindependent(free)variables.Inotherwords,thefunctiondefinedintheclosurerememberstheenvironment(variablesinthescope)inwhichitwascreated.WewilldiscovermoreaboutclosuresinChapter5,Runtime.
Ifwerunthegeneratedoutputinabrowserandtrytoinvokethe_ipropertydirectly,wewillnoticethatthepropertyisnowprivateatruntime:
varcounter=newCounter();
console.log(counter.get());//0
counter.set(2);
console.log(counter.get());//2
counter.increment();
console.log(counter.get());//3
console.log(counter._i);//undefined
Note
Insomecases,wewillneedtohavereallyprecisecontroloverscopeandclosures,andourcodewillenduplookingmuchmorelikeJavaScript.Justrememberthat,aslongaswewriteourapplicationcomponents(classes,modules,andsoon)tobeconsumedbyotherTypeScriptcomponents,wewillrarelyhavetoworryaboutimplementingruntimeprivateproperties.WewilllookindepthattheTypeScriptruntimeinChapter5,Runtime.
GenericsAndyHuntandDaveThomasformulatedthedon'trepeatyourself(DRY)principleinthebookThePragmaticProgrammer.TheDRYprincipleaimstoreducetherepetitionofinformationofallkinds.WewillnowtakealookatanexamplethatwillhelpustounderstandwhatgenericsfunctionsareandhowtheycanhelpusfollowtheDRYprinciple.
WewillstartbydeclaringareallysimpleUserclass:
classUser{
name:string;
age:number;
}
NowthatwehaveourUserclassinplace,let'swriteafunctionnamedgetUsersthatwillrequestalistofusersviaAJAX:
functiongetUsers(cb:(users:User[])=>void):void{
$.ajax({
url:"/api/users",
method:"GET",
success:function(data){
cb(data.items);
},
error:function(error){
cb(null);
}
});
}
Note
WewillusejQueryinthisexample.Remembertocreateapackage.jsonfileandinstallthejQuerypackageusingnpm.YouwillalsoneedtoinstallthejQuerytypedefinitionsfileusingtsd.RefertoChapter1,IntroducingTypescriptandChapter2,AutomatingYourDevelopmentWorkflowifyouneedadditionalhelp.
ThegetUsersfunctiontakesafunctionasaparameterthatwillbeinvokediftheAJAXrequesthasbeensuccessful.Itcanbeinvokedasfollows:
getUsers(function(users:User[]){
for(vari;users.length;i++){
console.log(users[i].name);
}
});
Nowlet'simaginethatweneedanalmostidenticaloperation.Butthistime,wewilluseanOrderentityinstead:
classOrder{
id:number;
total:number;
items:any[]
}
ThegetOrdersfunctionisalmostidenticaltothegetUsersfunction.ItusesadifferentURLanditwillpassanarrayofOrdersinsteadofaUserarray:
functiongetOrders(cb:(orders:Order[])=>void):void{
$.ajax({
url:"/api/orders",
method:"GET",
success:function(data){
cb(data.items);
},
error:function(error){
cb(null);
}
});
}
getOrders(function(orders:Orders[]){
for(vari;orders.length;i++){
console.log(orders[i].total);
}
});
Wecanusegenericstoavoidthiskindofrepetition.Genericprogrammingisastyleofcomputerprogramminginwhichalgorithmsarewrittenintermsoftypestobespecifiedlater.Thesetypesaretheninstantiatedwhenneededforspecifictypesprovidedasparameters.WearegoingtowriteagenericfunctionnamedgetEntitiesthattakestwoparameters:
functiongetEntities<T>(url:string,cb:(list:T[])=>void):void{
$.ajax({
url:url,
method:"GET",
success:function(data){
cb(data.items);
},
error:function(error){
cb(null);
}
});
}
Wehaveaddedanglebrackets(<>)afterthenameofourfunctionstoindicatethatitisagenericfunction.EnclosedintheanglebracketsisthecharacterT,whichisusedtorefertoatype.Thefirstparameterisnamedurlandisastring;thesecondparameterisafunctionnamedcb,whichtakesaparameterlistoftypeTasitsonlyparameter.
WecannowusethisgenericfunctiontoindicatewhattypeTwillrepresent:
getEntities<User>("/api/users",function(users:Users[]){
for(vari;users.length;i++){
console.log(users[i].name);
}
});
getEntities<Order>("/api/orders",function(orders:Orders[]){
for(vari;orders.length;i++){
console.log(orders[i].total);
}
});
TagfunctionsandtaggedtemplatesWehavealreadyseenhowtoworkwithtemplatestringssuchasthefollowing:
varname='remo';
varsurname=jansen;
varhtml=`<h1>${name}${surname}</h1>`;
However,thereisoneuseoftemplatestringsthatwedeliberatelyskippedbecauseitiscloselyrelatedtotheuseofaspecialkindoffunctionknownastagfunction.
Wecanuseatagfunctiontoextendormodifythestandardbehavioroftemplatestrings.Whenweapplyatagfunctiontoatemplatestring,thetemplatestringbecomesataggedtemplate.
WearegoingtoimplementatagfunctionnamedhtmlEscape.Touseatagfunction,wemustusethenameofthefunctionfollowedbyatemplatestring:
varhtml=htmlEscape`<h1>${name}${surname}</h1>`;
Atagtemplatemustreturnastringandtakethefollowingarguments:
Anarraywhichcontainsallthestaticliteralsinthetemplatestring(<h1>and</h1>intheprecedingexample)ispassedasthefirstargument.Arestparameterispassedasthesecondparameter.Therestparametercontainsallthevaluesinthetemplatestring(nameandsurnameintheprecedingexample).
Wenowknowthesignatureofatagfunction.
tag(literals:string[],...values:any[]):string
Let'simplementthehtmlEscapetagfunction:
functionhtmlEscape(literals,...placeholders){
letresult="";
for(leti=0;i<placeholders.length;i++){
result+=literals[i];
result+=placeholders[i]
.replace(/&/g,'&')
.replace(/"/g,'"')
.replace(/'/g,''')
.replace(/</g,'<')
.replace(/>/g,'>');
}
result+=literals[literals.length-1];
returnresult;
}
TheprecedingfunctioniteratesthroughtheliteralsandvaluesandensuresthattheHTMLcodeisescapedfromthevaluestoavoidpossiblecodeinjectionattacks.
Themainbenefitofusingataggedfunctionisthatitallowsustocreatecustomtemplate
stringprocessors.
Note
ThisfeaturewillbeavailableintheTypeScript1.6release.
AsynchronousprogramminginTypeScriptNowthatwehaveseenhowtoworkwithfunctions,wewillexplorehowwecanusethem,togetherwithsomenativeobjects,towriteasynchronousapplications.
Callbacksandhigher-orderfunctionsInTypeScript,functionscanbepassedasargumentstoanotherfunction.Thefunctionpassedtoanotherasanargumentisknownasacallback.Functionscanalsobereturnedbyanotherfunction.Thefunctionsthatacceptfunctionsasparameters(callbacks)orreturnfunctionsasanargumentareknownashigher-orderfunctions.Callbacksareusuallyanonymousfunctions.
varfoo=function(){//callback
console.log('foo');
}
functionbar(cb:()=>void){//higherorderfunction
console.log('bar');
cb();
}
bar(foo);//prints'bar'thenprints'foo'
ArrowfunctionsInTypeScript,wecandeclareafunctionusingafunctionexpressionoranarrowfunction.Anarrowfunctionexpressionhasashortersyntaxcomparedtofunctionexpressionsandlexicallybindsthevalueofthethisoperator.
ThethisoperatorbehavesalittledifferentlyinTypeScriptcomparedtootherlanguages.WhenwedefineaclassinTypeScript,wecanusethethisoperatortorefertotheclass'sownproperties.Let'stakealookatanexample:
classPerson{
name:string;
constructor(name:string){
this.name=name;
}
greet(){
alert(`Hi!Mynameis${this.name}`);
}
}
varremo=newPerson("Remo");
remo.greet();//"Hi!MynameisRemo"
WehavedefinedaPersonclassthatcontainsapropertyoftypestringcalledname.Theclasshasaconstructorandamethodgreet.Wehavecreatedaninstancenamedremoandinvokedthemethodnamedgreet,whichinternallyusesthethisoperatortoaccesstheremoproperty'sname.Insidethegreetmethod,thethisoperatorpointstotheobjectthatenclosesthegreetmethod.
Wemustbecarefulwhenusingthethisoperatorbecauseinsomescenariositcanpointtothewrongvalue.Let'saddanextramethodtothepreviousexample:
classPerson{
name:string;
constructor(name:string){
this.name=name;
}
greet(){
alert(`Hi!Mynameis${this.name}`);
}
greetDelay(time:number){
setTimeout(function(){
alert(`Hi!Mynameis${this.name}`);
},time);
}
}
varremo=newPerson("remo");
remo.greet();//"Hi!Mynameisremo"
remo.greetDelay(1000);//"Hi!Mynameis"
InthegreetDelaymethod,weperformanalmostidenticaloperationtotheoneperformedbythegreetmethod.Thistimethefunctiontakesaparameternamedtime,whichisusedtodelay
thegreetmessage.
Inordertodelaythemessage,weusethesetTimeoutfunctionandacallback.Assoonaswedefineananonymousfunction(thecallback),thethiskeywordchangesitsvalueandstartspointingtotheanonymousfunction.ThisexplainswhythenameremoisnotdisplayedbythegreetDelaymessage.
Asmentioned,anarrowfunctionexpressionlexicallybindsthevalueofthethisoperator.Thismeansthatitallowsustoaddafunctionwithoutalteringthevalueofthisoperator.Let'sreplacethefunctionexpressionfromthepreviousexamplewithanarrowfunction:
classPerson{
name:string;
constructor(name:string){
this.name=name;
}
greet(){
alert(`Hi!Mynameis${this.name}`);
}
greetDelay(time:number){
setTimeout(()=>{
alert(`Hi!Mynameis${this.name}`);
},time);
}
}
varremo=newPerson("remo");
remo.greet();//"Hi!Mynameisremo"
remo.greetDelay(1000);//"Hi!Mynameisremo"
Byusinganarrowfunction,wecanensurethatthethisoperatorstillpointstothePersoninstanceandnottothesetTimeoutcallback.IfweexecutethegreetDelayfunction,thenamepropertywillbedisplayedasexpected.
ThefollowingpieceofcodewasgeneratedbytheTypeScriptcompiler.Whencompilinganarrowfunction,theTypeScriptcompilerwillgenerateanaliasforthethisoperatornamed_this.Thealiasisusedtoensurethatthethisoperatorpointstotherightobject.
Person.prototype.greetDelay=function(time){
var_this=this;
setTimeout(function(){
alert("Hi!Mynameis"+_this.name);
},time);
};
CallbackhellWehaveseenthatcallbacksandhigherorderfunctionsaretwopowerfulandflexibleTypeScriptfeatures.However,theuseofcallbackscanleadtoamaintainabilityissueknownascallbackhell.Wewillnowwriteareal-lifeexampletoshowcasewhatacallbackhellisandhoweasilywecanendupdealingwithit.
Note
Rememberthatyoucanfindthecompletesourcecodeforthisdemointhecompanionsourcecode.
WearegoingtoneedhandlebarsandjQuerylibraries,solet'sinstallthesetwolibrariesandtheirrespectivetypedefinitionfilesusingnpmandtsd.Wecanthenimporttheirtypedefinitions:
///<referencepath="../typings/handlebars/handlebars.d.ts"/>
///<referencepath="../typings/jquery/jquery.d.ts"/>
Tomakeourcodeeasiertoread,wewillcreateanaliasforthecallbacktype:
typecb=(json:any)=>void;
NowweneedtodeclareourViewclass.TheViewclasshassomepropertiesthatallowustosetthefollowingproperties:
Container:TheDOMselectorwherewewantourviewtobeinsertedTemplateURL:TheURLthatwillreturnahandlebarstemplateServiceURL:TheURLofawebservicethatwillreturnsomeJSONdataArguments:Thedatatobesendtotheservice
WecanseetheViewclassimplementationasfollows:
classView{
private_container:string;
private_templateUrl:string;
private_serviceUrl:string;
private_args:any;
constructor(config){
this._container=config.container;
this._templateUrl=config.templateUrl;
this._serviceUrl=config.serviceUrl;
this._args=config.args;
}
//...
Afterdefiningtheclassconstructoranditsproperties,wewilladdaprivatemethodnamed_loadJsontoourclass.ThismethodtakestheserviceURL,thearguments,asuccesscallback,andanerrorcallbackasitsarguments.Insidethemethod,wewillsendajQueryAJAXrequestusingtheserviceURLandargumentsettings:
private_loadJson(url:string,args:any,cb:cb,errorCb:cb){
$.ajax({
url:url,
type:"GET",
dataType:"json",
data:args,
success:(json)=>{
cb(json);
},
error:(e)=>{
errorCb(e);
}
});
}
//...
Note
HandlebarsisalibrarythatallowsustocompileandrenderHTMLtemplatesinabrowser.ThesetemplateshelpwithJSON-to-HTMLtransformations.Wewillmentionthislibrarylateracoupleoftimes,butdon'tworryifyouhaveneveruseditbefore;thissectionisnotabouthandlebars.
Thissectionisaboutasetoftasksandhowwecancontroltheexecutionflowofthosetasksusingcallbacks.Ifyouwanttolearnmoreabouthandlebars,visithttp://handlebarsjs.com/.
Thisfunctionisalmostidenticaltothepreviousone,butinsteadofloadingsomeJSON,wewillloadahandlebarstemplate:
private_loadHbs(url:string,cb:cb,errorCb:cb){
$.ajax({
url:url,
type:"GET",
dataType:"text",
success:(hbs)=>{
cb(hbs);
},
error:(e)=>{
errorCb(e);
}
});
}
//...
Thisfunctiontakesahandlebartemplatecodeasinputandtriestocompileitusingthehandlebarscompilefunction.Justlikeinthepreviousexample,weusecallbacks,whichwillbeinvokedafterthesuccessorfailureoftheoperation:
private_compileHbs(hbs:string,cb:cb,errorCb:cb){
try
{
vartemplate=Handlebars.compile(hbs);
cb(template);
}
catch(e){
errorCb(e);
}
}
//...
Inthisfunction,wetakethealreadycompiledtemplateandthealreadyloadedJSONdataandputthemtogethertotransformJSONintoHTMLfollowingthetemplateformattingrules.Justlikeinthepreviousexample,weusecallbacksthatwillbeinvokedafterthesuccessorfailureoftheoperation:
private_jsonToHtml(template:any,json:any,cb:cb,errorCb:cb){
try
{
varhtml=template(json);
cb(html);
}
catch(e){
errorCb(e);
}
}
//...
ThefollowingfunctiontakestheHTMLgeneratedbythe_jsonToHtmlfunctionandappendsittoaDOMelement:
private_appendHtml=function(html:string,cb:cb,errorCb:cb){
try
{
if($(this._container).length===0){
thrownewError("Containernotfound!");
}
$(this._container).html(html);
cb($(this._container));
}
catch(e){
errorCb(e);
}
}
//...
Nowthatwehaveafewfunctionsthatusecallbacks,wewilluseallofthemtogetherinonesinglefunctionnamedrender.Therendermethodcontrolstheexecutionflowofthetasks,andexecutestheminthefollowingorder:
1. LoadstheJSONdata.2. Loadsthetemplate.3. Compilesthetemplate.4. TransformsJSONintoHTML.5. AppendsHTMLtotheDOM.
Eachtasktakesasuccesscallback,whichinvokesthefollowingtasksinthelistifitissuccessful,andanerrorcallback,whichisinvokedwhensomethinggoeswrong:
publicrender(cb:cb,errorCb:cb){
try
{
this._loadJson(this._serviceUrl,this._args,(json)=>{
this._loadHbs(this._templateUrl,(hbs)=>{
this._compileHbs(hbs,(template)=>{
this._jsonToHtml(template,json,(html)=>{
this._appendHtml(html,cb);
},errorCb);
},errorCb);
},errorCb);
},errorCb);
}
catch(e){
errorCb(e);
}
}
}
Ingeneral,youshouldtrytoavoidnestingcallbackslikeintheprecedingexamplebecauseitwill:
MakethecodehardertounderstandMakethecodehardertomaintain(refactor,reuse,andsoon)Makeexceptionhandlingmoredifficult
PromisesAfterseeinghowtheuseofcallbackscanleadtosomemaintainabilityproblems,wewillnowlookatpromisesandhowtheycanbeusedtowritebetterasynchronouscode.Thecoreideabehindpromisesisthatapromiserepresentstheresultofanasynchronousoperation.Promisemustbeinoneofthethreefollowingstates:
Pending:TheinitialstateofapromiseFulfilled:ThestateofapromiserepresentingasuccessfuloperationRejected:Thestateofapromiserepresentingafailedoperation
Onceapromiseisfulfilledorrejected,itsstatecanneverchangeagain.Let'stakealookatthebasicsyntaxofapromise:
functionfoo(){
returnnewPromise((fulfill,reject)=>{
try
{
//dosomething
fulfill(value);
}
catch(e){
reject(reason);
}
});
}
foo().then(function(value){console.log(value);})
.catch(function(e){console.log(e);});
Note
Atry…catchstatementisusedheretoshowcasehowwecanexplicitlyfulfillorrejectapromise.Thetry…catchstatementisnotreallyneededinaPromisefunctionbecausewhenanerroristhrowninapromise,thepromisewillautomaticallyberejected.
Theprecedingcodesnippetdeclaresafunctionnamedfoothatreturnsapromise.Thepromisecontainsamethodnamedthen,whichacceptsafunctiontobeinvokedwhenthepromiseisfulfilled.Promisesalsoprovideamethodnamedcatch,whichisinvokedwhenapromiseisrejected.
Wewillnowreturntothecallbackhellexampleandmakesomechangesinthecodetousepromisesinsteadofcallbacks.
Justlikebefore,wearegoingtoneedhandlebarsandjQuery;solet'simporttheirtypedefinitions.Inaddition,thistime,wewillalsoneedthedeclarationsofalibraryknownasQ:
///<referencepath="../typings/handlebars/handlebars.d.ts"/>
///<referencepath="../typings/jquery/jquery.d.ts"/>
///<referencepath="../typings/q/q.d.ts"/>
Note
WewillusethePromiseobjectfromalibraryinsteadofthenativeobjectbecausethelibrariesimplementfallbackssoourcodecanworkinoldbrowsers.WewilluseapromiseslibraryknownasQ(version1.0.1)inthisexample.Ifyouwanttolearnmoreaboutit,visithttps://github.com/kriskowal/q.
TheclassnamehaschangedfromViewtoViewAsyncbuteverythingelseisstillidenticaltothepreviousexample:
classViewAsync{
private_container:string;
private_templateUrl:string;
private_serviceUrl:string;
private_args:any;
constructor(config){
this._container=config.container;
this._templateUrl=config.templateUrl;
this._serviceUrl=config.serviceUrl;
this._args=config.args;
}
//...
Note
ManydevelopersappendthewordAsynctothenameofafunctionasacodeconvention,whichisusedtoindicatethatafunctionisanasynchronousfunction.
Wewilluseourfirstpromiseinthefunction_loadJsonAsync.Thisfunctionwasnamed_loadJsoninthecallbackexample.Wehaveremovedthecallbacksforsuccessanderrorpreviouslydeclaredinthefunctionsignature.Finally,wehavewrappedthefunctionwithapromiseobjectandinvokedtheresolveandrejectmethodswhenthepromisesucceedsorfailsrespectively.
private_loadJsonAsync(url:string,args:any){
returnQ.Promise(function(resolve,reject){
$.ajax({
url:url,
type:"GET",
dataType:"json",
data:args,
success:(json)=>{
resolve(json);
},
error:(e)=>{
reject(e);
}
});
});
}
//...
Wewillthenrefactor(rename,removecallbacks,wraplogicwithapromise,andsoon)eachoftheclassfunctions(_loadHbsAsync,compileHbsAsync,and_appendHtmlAsync):
private_loadHbsAsync(url:string){
returnQ.Promise(function(resolve,reject){
$.ajax({
url:url,
type:"GET",
dataType:"text",
success:(hbs)=>{
resolve(hbs);
},
error:(e)=>{
reject(e);
}
});
});
}
private_compileHbsAsync(hbs:string){
returnQ.Promise(function(resolve,reject){
try
{
vartemplate:any=Handlebars.compile(hbs);
resolve(template);
}
catch(e){
reject(e);
}
});
}
private_jsonToHtmlAsync(template:any,json:any){
returnQ.Promise(function(resolve,reject){
try
{
varhtml=template(json);
resolve(html);
}
catch(e){
reject(e);
}
});
}
private_appendHtmlAsync(html:string,container:string){
returnQ.Promise((resolve,reject)=>{
try
{
var$container:any=$(container);
if($container.length===0){
thrownewError("Containernotfound!");
}
$container.html(html);
resolve($container);
}
catch(e){
reject(e);
}
});
}
//...
TheRenderAsyncmethod(previouslynamedrender)willpresentsomesignificantdifferences.
Inthefollowingfunction,westartbywrappingthefunction'slogicwithapromise,invokethefunction_loadJsonAsync,andassignitsreturnvaluetothevariablegetJson.Ifwereturntothe_loadJsonAsyncfunction,wewillnoticethatthereturntypeisapromise.Therefore,thegetJsonvariableisapromisethatoncefulfilledwillreturntheJSONdatarequiredtorenderourview.
Thistime,wewillinvokethethenmethod,whichbelongstothepromisereturnedbythe_loadHbsAsyncmethod.Thiswillallowustopasstheoutputofthefunction_loadHbsAsyncto_compileHbsAsyncwhenthepromise'sstatechangestofulfilled.
publicrenderAsync(){
returnQ.Promise((resolve,reject)=>{
try
{
//assignpromisetogetJson
vargetJson=this._loadJsonAsync(this._serviceUrl,this._args);
//assignpromisetogetTemplate
vargetTemplate=this._loadHbsAsync(this._templateUrl)
.then(this._compileHbsAsync);
//executepromisesinparallel
Q.all([getJson,getTemplate]).then((results)=>{
varjson=results[0];
vartemplate=results[1];
this._jsonToHtmlAsync(template,json)
.then((html:string)=>{
returnthis._appendHtmlAsync(html,this._container);
})
.then(($container:any)=>{resolve($container);});
});
}
catch(error){
reject(error);
}
});
}
}
OncewehavedeclaredthegetJsonandgetTemplatevariables(eachcontainingapromiseasavalue)wewillusetheallmethodfromtheQlibrarytoexecutethegetJsonandgetTemplatepromisesinparallel.
Q'sallmethodtakesalistofpromisesandacallbackasinput.Onceallthepromisesinthelist
havebeenfulfilled,thecallbackisinvokedandanarraynamedresultsispassedtothefulfilmentcallback.Thearraycontainstheresultsofeachofthepromisesinthesameorderthattheywerepassedtotheallmethod.
InsideQ'sallmethodcallback,wewillusetheloadedJSONandthecompiledtemplateandargumentswheninvokingthe_jsonToHtmlAsyncpromise.Wewillfinallyusethethenmethodtocallthe_appendHtmlAsyncmethodandresolvethepromise.
Asobservedintheexample,usingpromisesgivesusbettercontrolovertheexecutionflowofeachoftheoperationsinourrendermethod.Rememberthatyoucanusefourdifferenttypesofasynchronousflowcontrol:
Concurrent:Thetasksareexecutedinparallel.WesawthisintheexamplewhenweusedtheallmethodinthegetJsonandgetTemplatepromises.Series:Agroupoftasksisexecutedinsequencebuttheprecedingtasksdonotpassargumentstothenexttask.Waterfall:Agroupoftasksisexecutedinsequenceandeachtaskpassesargumentstothenexttask.Thisapproachisusefulwhenthetaskshavedependenciesoneachother.Intheprecedingexample,wefindthisasynchronousflowcontrolapproachwhenthe_loadHbsAsyncpromisepassesitsoutputtothe_compileHbsAsyncpromise.Composite:Thisisanycombinationofthepreviousconcurrent,series,andwaterfallapproaches.Therendermethodintheexampleusesacombinationofalltheasynchronousflowcontrolapproachesinthislist.
GeneratorsIfweinvokeafunctioninTypeScript,wecanassumethatoncethefunctionstartsrunning,itwillalwaysruntocompletionbeforeanyothercodecanrun.Thishasbeenthecaseuntilnow.However,anewkindoffunctionwhichmaybepausedinthemiddleofexecution—oneormanytimes—andresumedlater,allowingothercodetorunduringthesepausedperiods,isabouttoarriveinTypeScriptandES6.Thesenewkindsoffunctionsareknownasgenerators.
Ageneratorrepresentsasequenceofvalues.Theinterfaceofageneratorobjectisajustaniterator.Thenext()functioncanbeinvokeduntilitrunsoutofvalues.
Wecandefinetheconstructorofageneratorbyusingthefunctionkeywordfollowedbyanasterisk(*).Theyieldkeywordisusedtostoptheexecutionofthefunctionandreturnavalue.Let'stakealookatanexample:
function*foo(){
yield1;
yield2;
yield3;
yield4;
return5;
}
varbar=newfoo();
bar.next();//Object{value:1,done:false}
bar.next();//Object{value:2,done:false}
bar.next();//Object{value:3,done:false}
bar.next();//Object{value:4,done:false}
bar.next();//Object{value:5,done:true}
bar.next();//Object{done:true}
Asyoucansee,thisiteratorhasfivesteps.Thefirsttimewecallnext,thefunctionwillbeexecuteduntilitreachesthefirstyieldstatement,andthenitwillreturnthevalue1andstoptheexecutionofthefunctionuntilweinvokethegenerator'snextmethodagain.Aswecansee,wearenowabletostopthefunction'sexecutionatagivenpoint.Thisallowsustowriteinfiniteloopswithoutcausingastackoverflowasinthefollowingexample:
function*foo(){
vari=1;
while(true){
yieldi++;
}
}
varbar=newfoo();
bar.next();//Object{value:1,done:false}
bar.next();//Object{value:2,done:false}
bar.next();//Object{value:3,done:false}
bar.next();//Object{value:4,done:false}
bar.next();//Object{value:5,done:false}
bar.next();//Object{value:6,done:false}
bar.next();//Object{value:7,done:false}
//...
Generatorswillopenpossibilitiesforsynchronicityaswecancallagenerator'snextmethodaftersomeasynchronouseventhasoccurred.
Asynchronousfunctions–asyncandawaitAsynchronousfunctionsareaTypeScriptfeaturethatisscheduledtoarrivewiththeupcomingTypeScriptreleases.Anasynchronousfunctionisafunctionthatisexpectedtobeinvokedinasynchronousoperation.Developerscanusetheawaitkeywordtowaitforthefunctionresultswithoutblockingthenormalexecutionoftheprogram.
AsynchronousfunctionswillbeimplementedusingpromiseswhentargetingES6,andpromisefallbackswhentargetingES3andES5.
Usingasynchronousfunctionsgenerallyhelpstoincreasethereadabilityofapieceofcodewhencomparedwiththeuseofpromises;buttechnicallywecanachievethesamefeaturesusingbothpromisesandsynchronouscode.
Let'stakeasneak-peekatthisupcomingfeature:
varp:Promise<number>=/*...*/;
asyncfunctionfn():Promise<number>{
vari=awaitp;
return1+i;
}
Theprecedingcodesnippetdeclaresapromisenamedp.Thispromiseisthepieceofcodethatwillwaittobeexecuted.Whilewaiting,theprogramexecutionwillnotbeblockedbecausewewillwaitfromanasynchronousfunctionnamedfn.Aswecansee,thefnfunctionisprecededbytheasynckeyword,whichisusedtoindicatetothecompilerthatitisanasynchronousfunction.
Insidethefunction,theawaitkeywordisusedtosuspendexecutionuntilpissettled.Aswecansee,thesyntaxismuchmoreminimalisticandcleanerthanitwouldbeifweusedthepromisesAPI(thenandcatchmethodsandcallbacks).
Note
RefertotheTypeScriptroadmaptolearnmoreaboutthestagesofdevelopmentofthisfeature.
SummaryInthischapter,wesawhowtoworkwithfunctionsindepth.Westartedwithaquickrecapofsomebasicconceptsandthenmovedtosomelesserknownfunctionfeaturesandusecases.
Oncewesawhowtoworkwithfunctions,wefocusedontheusageofcallbacks,promises,andgeneratorstotakeadvantageoftheasynchronousprogrammingcapabilitiesofTypescript.
Inthenextchapter,wewilllookathowtoworkwithclasses,interfaces,andotherobject-orientedprogrammingfeaturesoftheTypeScriptprogramminglanguage.
Chapter4.Object-OrientedProgrammingwithTypeScriptInthepreviouschapter,weexploredtheuseoffunctionsandsomeasynchronoustechniques.Inthischapter,wewillseehowtogroupourfunctionsinreusablecomponents,suchasclassesormodules.Thischapterisdividedintotwomainsections.Thefirstpartwillcoverthefollowingtopics:
SOLIDprinciplesClassesAssociation,aggregation,andcompositionInheritanceMixinsGenericclassesGenericconstraintsInterfaces
Inthesecondpart,wewillfocusonthedeclarationanduseofnamespacesandexternalmodules.Thesecondpartwillcoverthefollowingtopics:
Namespaces(internalmodules)ExternalmodulesAsynchronousmoduledefinition(AMD)CommonJSmodulesES6modulesBrowserifyanduniversalmoduledefinition(UMD)Circulardependencies
SOLIDprinciplesIntheearlydaysofsoftwaredevelopment,developersusedtowritecodewithproceduralprograminglanguages.Inproceduralprogramminglanguages,theprogramsfollowatop-to-bottomapproachandthelogiciswrappedwithfunctions.
Newstylesofcomputerprogramming,suchasmodularprogrammingorstructuredprogramming,emergedwhendevelopersrealizedthatproceduralcomputerprogramscouldnotprovidethemwiththedesiredlevelofabstraction,maintainability,andreusability.
Thedevelopmentcommunitycreatedaseriesofrecommendedpracticesanddesignpatternstoimprovethelevelofabstractionandreusabilityofproceduralprogramminglanguages,butsomeoftheseguidelinesrequiredacertainlevelofexpertise.Inordertofacilitateadherencetotheseguidelines,anewstyleofcomputerprogrammingknownasobject-orientedprogramming(OOP)wascreated.
DevelopersquicklynoticedsomecommonOOPmistakesandcameupwithfiverulesthateveryOOPdevelopershouldfollowtocreateasystemthatiseasytomaintainandextendovertime.ThesefiverulesareknownastheSOLIDprinciples.SOLIDisanacronymintroducedbyMichaelFeathers,whichstandsforthefollowingprinciples:
Singleresponsibilityprinciple(SRP):Thisprinciplestatesthatasoftwarecomponent(function,class,ormodule)shouldfocusononeuniquetask(haveonlyoneresponsibility).Open/closedprinciple(OCP):Thisprinciplestatesthatsoftwareentitiesshouldbedesignedwithapplicationgrowth(newcode)inmind(shouldbeopentoextension),buttheapplicationgrowthshouldrequirethefewerpossiblenumberofchangestotheexistingcode(beclosedformodification).Liskovsubstitutionprinciple(LSP):Thisprinciplestatesthatweshouldbeabletoreplaceaclassinaprogramwithanotherclassaslongasbothclassesimplementthesameinterface.Afterreplacingtheclass,nootherchangesshouldberequired,andtheprogramshouldcontinuetoworkasitdidoriginally.Interfacesegregationprinciple(ISP):Thisprinciplestatesthatweshouldsplitinterfacesthatareverylarge(general-purposeinterfaces)intosmallerandmorespecificones(manyclient-specificinterfaces)sothatclientswillonlyneedtoknowaboutthemethodsthatareofinteresttothem.Dependencyinversionprinciple(DIP):Thisprinciplestatesthatentitiesshoulddependonabstractions(interfaces)asopposedtodependingonconcretion(classes).
Inthischapter,wewillseehowtowriteTypeScriptcodethatadherestotheseprinciplessothatourapplicationsareeasytomaintainandextendovertime.
ClassesWeshouldalreadybefamiliarwiththebasicsaboutTypeScriptclasses,aswehavedeclaredsomeoftheminpreviouschapters.SowewillnowlookatsomedetailsandOOPconceptsthroughexamples.Let'sstartbydeclaringasimpleclass:
classPerson{
publicname:string;
publicsurname:string;
publicemail:string;
constructor(name:string,surname:string,email:string){
this.email=email;
this.name=name;
this.surname=surname;
}
greet(){
alert("Hi!");
}
}
varme:Person=newPerson("Remo","Jansen","[email protected]");
Weuseclassestorepresentthetypeofanobjectorentity.Aclassiscomposedofaname,attributes,andmethods.TheclassintheprecedingexampleisnamedPersonandcontainsthreeattributesorproperties(name,surname,andemail)andtwomethods(constructorandgreet).Classattributesareusedtodescribetheobject'scharacteristics,whileclassmethodsareusedtodescribeitsbehavior.
Aconstructorisaspecialmethodusedbythenewkeywordtocreateinstances(alsoknownasobjects)ofourclass.Wehavedeclaredavariablenamedme,whichholdsaninstanceofthePersonclass.ThenewkeywordusesthePersonclass'sconstructortoreturnanobjectwhosetypeisPerson.
Aclassshouldadheretothesingleresponsibilityprinciple(SRP).ThePersonclassintheprecedingexamplerepresentsaperson,includingalltheircharacteristics(attributes)andbehaviors(methods).Nowlet'saddsomeemailasvalidationlogic:
classPerson{
publicname:string;
publicsurname:string;
publicemail:string;
constructor(name:string,surname:string,email:string){
this.surname=surname;
this.name=name;
if(this.validateEmail(email)){
this.email=email;
}
else{
thrownewError("Invalidemail!");
}
}
validateEmail(){
varre=/\S+@\S+\.\S+/;
returnre.test(this.email);
}
greet(){
alert("Hi!I'm"+this.name+".Youcanreachmeat"+this.email);
}
}
Whenanobjectdoesn'tfollowtheSRPanditknowstoomuch(hastoomanyproperties)ordoestoomuch(hastoomanymethods),wesaythattheobjectisaGodobject.ThePersonclasshereisaGodobjectbecausewehaveaddedamethodnamedvalidateEmailthatisnotreallyrelatedtothePersonclass'sbehavior.
Decidingwhichattributesandmethodsshouldorshouldnotbepartofaclassisarelativelysubjectivedecision.Ifwespendsometimeanalyzingouroptions,weshouldbeabletofindawaytoimprovethedesignofourclasses.
WecanrefactorthePersonclassbydeclaringanEmailclass,responsiblefore-mailvalidation,anduseitasanattributeinthePersonclass:
classEmail{
publicemail:string;
constructor(email:string){
if(this.validateEmail(email)){
this.email=email;
}
else{
thrownewError("Invalidemail!");
}
}
validateEmail(email:string){
varre=/\S+@\S+\.\S+/;
returnre.test(email);
}
}
NowthatwehaveanEmailclass,wecanremovetheresponsibilityofvalidatingtheemailsfromthePersonclassandupdateitsemailattributetousethetypeEmailinsteadofstring:
classPerson{
publicname:string;
publicsurname:string;
publicemail:Email;
constructor(name:string,surname:string,email:Email){
this.email=email;
this.name=name;
this.surname=surname;
}
greet(){
alert("Hi!");
}
}
Makingsurethataclasshasasingleresponsibilitymakesiteasiertoseewhatitdoesandhowwecanextend/improveit.WecanfurtherimproveourPersonandEmailclassesbyincreasingthelevelofabstractionofourclasses.Forexample,whenweusetheEmailclass,wedon'treallyneedtobeawareoftheexistenceofthevalidateEmailmethod;sothismethodcouldbeinvisiblefromoutsidetheEmailclass.Asaresult,theEmailclasswouldbemuchsimplertounderstand.
Whenweincreasethelevelofabstractionofanobject,wecansaythatweareencapsulatingtheobject'sdataandbehavior.Encapsulationisalsoknownasinformationhiding.Forexample,theEmailclassallowsustouseemailswithouthavingtoworryaboute-mailvalidationbecausetheclasswilldealwithitforus.Wecanmakethisclearerbyusingaccessmodifiers(publicorprivate)toflagasprivatealltheclassattributesandmethodsthatwewanttoabstractfromtheuseoftheEmailclass:
classEmail{
privateemail:string;
constructor(email:string){
if(this.validateEmail(email)){
this.email=email;
}
else{
thrownewError("Invalidemail!");
}
}
privatevalidateEmail(email:string){
varre=/\S+@\S+\.\S+/;
returnre.test(email);
}
get():string{
returnthis.email;
}
}
WecanthensimplyusetheEmailclasswithoutneedingtoexplicitlyperformanykindofvalidation:
varemail=newEmail("[email protected]");
InterfacesThefeaturethatwewillmissthemostwhendevelopinglarge-scalewebapplicationswithJavaScriptisprobablyinterfaces.WehaveseenthatfollowingtheSOLIDprinciplescanhelpustoimprovethequalityofourcode,andwritinggoodcodeisamustwhenworkingonalargeproject.TheproblemisthatifweattempttofollowtheSOLIDprincipleswithJavaScript,wewillsoonrealizethatwithoutinterfaces,wewillneverbeabletowriteSOLIDOOPcode.Fortunatelyforus,TypeScriptfeaturesinterfaces.
Traditionally,inOOP,wesaythataclasscanextendanotherclassandimplementoneormoreinterfaces.Aninterfacecanimplementoneormoreinterfacesandcannotextendanotherclassorinterface.Wikipedia'sdefinitionofinterfacesinOOPisasfollows:
Inobject-orientedlanguages,theterminterfaceisoftenusedtodefineanabstracttypethatcontainsnodataorcode,butdefinesbehaviorsasmethodsignatures.
Implementinganinterfacecanbeunderstoodassigningacontract.Theinterfaceisacontract,andwhenwesignit(implementit),wemustfollowitsrules.Theinterfacerulesarethesignaturesofthemethodsandproperties,andwemustimplementthem.
Wewillseemanyexamplesofinterfaceslaterinthischapter.
InTypeScript,interfacesdon'tstrictlyfollowthisdefinition.ThetwomaindifferencesarethatinTypeScript:
AninterfacecanextendanotherinterfaceorclassAninterfacecandefinedataandbehaviorsasopposedtoonlybehaviors
Association,aggregation,andcompositionInOOP,classescanhavesomekindofrelationshipwitheachother.Now,wewilltakealookatthethreedifferenttypesofrelationshipsbetweenclasses.
AssociationWecallassociationthoserelationshipswhoseobjectshaveanindependentlifecycleandwherethereisnoownershipbetweentheobjects.Let'stakeanexampleofateacherandstudent.Multiplestudentscanassociatewithasingleteacher,andasinglestudentcanassociatewithmultipleteachers,butbothhavetheirownlifecycles(bothcanbecreateanddeleteindependently);sowhenateacherleavestheschool,wedon'tneedtodeleteanystudents,andwhenastudentleavestheschool,wedon'tneedtodeleteanyteachers.
AggregationWecallaggregationthoserelationshipswhoseobjectshaveanindependentlifecycle,butthereisownership,andchildobjectscannotbelongtoanotherparentobject.Let'stakeanexampleofacellphoneandacellphonebattery.Asinglebatterycanbelongtoaphone,butifthephonestopsworking,andwedeleteitfromourdatabase,thephonebatterywillnotbedeletedbecauseitmaystillbefunctional.Soinaggregation,whilethereisownership,objectshavetheirownlifecycle.
CompositionWeusethetermcompositiontorefertorelationshipswhoseobjectsdon'thaveanindependentlifecycle,andiftheparentobjectisdeleted,allchildobjectswillalsobedeleted.
Let'stakeanexampleoftherelationshipbetweenquestionsandanswers.Singlequestionscanhavemultipleanswers,andanswerscannotbelongtomultiplequestions.Ifwedeletequestions,answerswillautomaticallybedeleted.
Objectswithadependentlifecycle(answers,intheexample)areknownasweakentities.
Sometimes,itcanbeacomplicatedprocesstodecideifweshoulduseassociation,aggregation,orcomposition.Thisdifficultyiscausedinpartbecauseaggregationandcompositionaresubsetsofassociation,meaningtheyarespecificcasesofassociation.
InheritanceOneofthemostfundamentalobject-orientedprogrammingfeaturesisitscapabilitytoextendexistingclasses.Thisfeatureisknownasinheritanceandallowsustocreateanewclass(childclass)thatinheritsallthepropertiesandmethodsfromanexistingclass(parentclass).Childclassescanincludeadditionalpropertiesandmethodsnotavailableintheparentclass.Let'sreturntoourpreviouslydeclaredPersonclass.WewillusethePersonclassastheparentclassofachildclassnamedTeacher:
classPerson{
publicname:string;
publicsurname:string;
publicemail:Email;
constructor(name:string,surname:string,email:Email){
this.name=name;
this.surname=surname;
this.email=email;
}
greet(){
alert("Hi!");
}
}
Note
Thisexampleisincludedinthecompanionsourcecode.
Oncewehaveaparentclassinplace,wecanextenditbyusingthereservedkeywordextends.Inthefollowingexample,wedeclareaclasscalledTeacher,whichextendsthepreviouslydefinedPersonclass.ThismeansthatTeacherwillinheritalltheattributesandmethodsfromitsparentclass:
classTeacherextendsPerson{
teach(){
alert("Welcometoclass!");
}
}
NotethatwehavealsoaddedanewmethodnamedteachtotheclassTeacher.IfwecreateinstancesofthePersonandTeacherclasses,wewillbeabletoseethatbothinstancessharethesameattributesandmethodswiththeexceptionoftheteachmethod,whichisonlyavailablefortheinstanceoftheTeacherclass:
varteacher=newTeacher("remo","jansen",new
Email("[email protected]"));
varme=newPerson("remo","jansen",newEmail("[email protected]"));
me.greet();
teacher.greet();
me.teach();//Error:Property'teach'doesnotexistontype'Person'
teacher.teach();
Sometimes,wewillneedachildclasstoprovideaspecificimplementationofamethodthatisalreadyprovidedbyitsparentclass.Wecanusethereservedkeywordsuperforthispurpose.Imaginethatwewanttoaddanewattributetolisttheteacher'ssubjects,andwewanttobeabletoinitializethisattributethroughtheteacherconstructor.Wewillusethesuperkeywordtoexplicitlyreferencetheparentclassconstructorinsidethechildclassconstructor.Wecanalsousethesuperkeywordwhenwewanttoextendanexistingmethod,suchasgreet.ThisOOPlanguagefeaturethatallowsasubclassorchildclasstoprovideaspecificimplementationofamethodthatisalreadyprovidedbyitsparentclassesisknownasmethodoverriding.
classTeacherextendsPerson{
publicsubjects:string[];
constructor(name:string,surname:string,email:Email,subjects:
string[]){
super(name,surname,email);
this.subjects=subjects;
}
greet(){
super.greet();
alert("Iteach"+this.subjects);
}
teach(){
alert("WelcometoMathsclass!");
}
}
varteacher=newTeacher("remo","jansen",new
Email("[email protected]"),["math","physics"]);
Wecandeclareanewclassthatinheritsfromaclassthatisalreadyinheritingfromanother.Inthefollowingcodesnippet,wedeclareaclasscalledSchoolPrincipalthatextendstheTeacherclass,whichextendsthePersonclass:
classSchoolPrincipalextendsTeacher{
manageTeachers(){
alert("Weneedtohelpstudentstogetbetterresults!");
}
}
IfwecreateaninstanceoftheSchoolPrincipalclass,wewillbeabletoaccessallthepropertiesandmethodsfromitsparentclasses(SchoolPrincipal,Teacher,andPerson):
varprincipal=newSchoolPrincipal("remo","jansen",new
Email("[email protected]"),["math","physics"]);
principal.greet();
principal.teach();
principal.manageTeachers();
Itisnotrecommendedtohavetoomanylevelsintheinheritancetree.Aclasssituatedtoodeeplyintheinheritancetreewillberelativelycomplextodevelop,test,andmaintain.Unfortunately,wedon'thaveaspecificrulethatwecanfollowwhenweareunsurewhether
weshouldincreasethedepthofinheritancetree(DIT).
Weshoulduseinheritanceinsuchawaythatithelpsustoreducethecomplexityofourapplicationandnottheopposite.WeshouldtrytokeeptheDITbetween0and4becauseavaluegreaterthan4wouldcompromiseencapsulationandincreasecomplexity.
MixinsSometimes,wewillfindscenariosinwhichitwouldbeagoodideatodeclareaclassthatinheritsfromtwoormoreclassessimultaneously(knownasmultipleinheritance).
Let'stakealookatanexample.Wewillnotaddanycodetothemethodsinthisexamplebecausewewanttoavoidthepossibilityofgettingdistractedbyit;weshouldfocusontheinheritancetree:
classAnimal{
eat(){
//...
}
}
WestartedbydeclaringaclassnamedAnimal,whichonlyhasonemethodnamedeat.Now,let'sdeclaretwonewclasses:
classMammalextendsAnimal{
breathe(){
//...
}
}
classWingedAnimalextendsAnimal{
fly(){
//...
}
}
WehavedeclaredtwonewclassesnamedWingedAnimalandMammal.BothclassesinheritfromtheAnimalclass.
Nowthatwehaveourclassesready,wearegoingtotrytoimplementaclassnamedBat.Batsaremammalsandhavewings—creatinganewclassnamedBat,whichwillextendboththeMammalandWingedAnimalclasses,seemslogical.However,ifweattempttodoso,wewillencounteracompilationerror:
//Error:Classescanonlyextendasingleclass.
classBatextendsWingedAnimal,Mammal{
//...
}
ThiserroristhrownbecauseTypeScriptdoesn'tsupportmultipleinheritance.Thismeansthataclasscanonlyextendoneclass.ThedesignersofprogramminglanguagessuchasC#orTypeScriptdecidedtonotsupportmultipleinheritancebecauseitcanpotentiallyincreasethecomplexityofapplications.
Sometimes,aclassinheritancediagramcantakeadiamond-likeshape(asseeninthefollowingfigure).Thiskindofclassinheritancediagramcanpotentiallyleadustodesign
issueknownasthediamondproblem.
Wewillnotfaceanyproblemsifwecallamethodthatisexclusivetoonlyoneoftheclassesintheinheritancetree:
varbat=newBat();
bat.fly();
bat.eat();
bat.breathe();
ThediamondproblemtakesplacewhenwetrytoinvokeoneoftheBatclass'sparent'smethods,anditisunclearorambiguouswhichoftheparent'simplementationsofthatmethodshouldbeinvoked.IfweaddamethodnamedmovetoboththeMammalandtheWingedAnimalclassandtrytoinvokeitfromaninstanceofBat,wewillgetanambiguouscallerror.
Nowthatweknowwhymultipleinheritancecanbepotentiallydangerous,wewillintroduceafeatureknownasmixin.Mixinsarealternativestomultipleinheritance,butthisfeaturehassomelimitations.
Let'sreturntotheBatclassexampletoshowcasetheusageofmixins:
classMammal{
breathe():string{
return"I'malive!";
}
}
classWingedAnimal{
fly():string{
return"Icanfly!";
}
}
Note
Thisexampleisincludedinthecompanionsourcecode.
Thetwoclassespresentedintheprecedingexamplearenotmuchdifferentfromthepreviousexample;wehaveaddedsomelogictothebreatheandflymethods,sowecanhavesomeoutputtohelpustounderstandthisdemonstration.Also,notethattheclassesnolongerextendtheAnimalclass:
classBatimplementsMammal,WingedAnimal{
breathe:()=>string;
fly:()=>string;
}
TheBatclasshassomeimportantadditions.Wehaveusedthereservedkeywordimplements(asopposedtoextends)toindicatethatBatwillimplementthefunctionalitydeclaredinboththeMammalandWingedAnimalclasses.WehavealsoaddedthesignatureofeachofthemethodsthattheBatclasswillimplement.
Weneedtocopythefollowingfunctionsomewhereinourcodetobeabletoapplymixins:
functionapplyMixins(derivedCtor:any,baseCtors:any[]){
baseCtors.forEach(baseCtor=>{
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name=>{
if(name!=='constructor'){
derivedCtor.prototype[name]=baseCtor.prototype[name];
}
});
});
}
Note
Theprecedingfunctionisawell-knownpatternandcanbefoundinmanybooksandonlinereferences,includingtheofficialTypeScripthandbook.
Thisfunctionwilliterateeachpropertyoftheparentclasses(containedinanarraynamedbaseCtors)andcopytheimplementationtoachildclass(derivedCtor).
Weonlyneedtodeclarethisfunctiononceinourapplication.Oncewehavedoneit,wecan
useitasfollows:
applyMixins(Bat,[Mammal,WingedAnimal]);
Thechildclass(Bat)willthencontaintheimplementationofeachpropertyandmethodofthetwoparentclasses(WingedAnimalandMammal):
varbat=newBat();
bat.breathe();//I'malive!
bat.fly();//Icanfly!
Aswesaidatthebeginningofthissection,mixinshavesomelimitations.Thefirstlimitationisthatwecanonlyinheritthepropertiesandmethodsfromonelevelintheinheritancetree.NowwecanunderstandwhyweremovedtheAnimalclasspriortoapplyingthemixin.Thesecondlimitationisthat,iftwoormoreoftheparentclassescontainamethodwiththesamename,themethodthatisgoingtobeinheritedwillbetakenfromthelastclasspassedinthebaseCtorsarraytotheapplyMixinsfunction.Wewillnowseeanexamplethatpresentsboththeselimitations.
Inordertoshowthefirstlimitation,wewilldeclaretheAnimalclass:
classAnimal{
eat():string{
return"Delicious!";
}
}
WewillthendeclaretheMammalandWingedAnimalclasses,butthistime,theywillextendtheAnimalclass:
classMammalextendsAnimal{
breathe():string{
return"I'malive!";
}
move():string{
return"Icanmovelikeamammal!";
}
}
classWingedAnimalextendsAnimal{
fly():string{
return"Icanfly!";
}
move():string{
return"Icanmovelikeabird!";
}
}
WewillthendeclareaBatclassbutwewillnameitBat1.ThisclasswillimplementboththeMammalandWindgedAnimalclasses:
classBat1implementsMammal,WingedAnimal{
eat:()=>string;
breathe:()=>string;
fly:()=>string;
move:()=>string;
}
WearereadytoinvoketheapplyMixinsfunction.NoticehowwepassMammalbeforeWingedAnimalinthearray:
applyMixins(Bat1,[Mammal,WingedAnimal]);
WecannowcreateaninstanceofBat1,andwewillbeabletoobservethattheeatmethodhasnotbeeninheritedfromtheAnimalclassduetothefirstlimitation:
varbat1=newBat();
bat1.eat();//Error:notafunction
Eachoftheparentclass'smethodshasbeeninheritedwithoutissues:
bat1.breathe();//I'malive!
bat1.fly();//Icanfly!"
Exceptthemovemethodbecauseaccordingtothesecondlimitation,onlytheimplementationofthelastparentclasspassedtotheapplyMixinsmethodwillbeimplemented.Inthiscase,theimplementationisinheritedfromtheWingedAnimalclass:
bat1.move();//Icanmovelikeabird
Tofinalize,wewillseetheeffectofswitchingtheorderoftheparentclasseswheninvokingtheapplyMixinsmethod:
classBat2implementsWingedAnimal,Mammal{
eat:()=>string;
breathe:()=>string;
fly:()=>string;
move:()=>string;
}
NoticehowwehavepassedWingedAnimalbeforeMammalinthearray:
applyMixins(Bat2,[WingedAnimal,Mammal]);
varbat2=newBat2();
bat2.eat();//Error:notafunction
bat2.breathe();//I'malive!
bat2.fly();//Icanfly!
bat2.move()//Icanmovelikeamammal
GenericclassesInthepreviouschapter,wesawhowtoworkwithgenericfunctions.Wewillnowtakealookathowtoworkwithgenericclasses.
Justlikewithgenericfunctions,genericclassescanhelpustoavoidtheduplicationofcode.Let'stakealookatanexample:
classUser{
publicname:string;
publicpassword:string;
}
Note
Thisexampleisincludedinthecompanionsourcecode.
WehavedeclaredaUserclass,whichcontainstwoproperties:nameandpassword.WewillnowdeclareaclassnamedNotGenericUserRepositorywithoutusinggenerics.ThisclasstakesaURLviaitsconstructorandhasamethodnamedgetAsync.ThegetAsyncmethodwillrequestalistofusersstoredinaJSONfileusingAJAX:
classNotGenericUserRepository{
private_url:string;
constructor(url:string){
this._url=url;
}
publicgetAsync(){
returnQ.Promise((resolve:(users:User[])=>void,reject)=>{
$.ajax({
url:this._url,
type:"GET",
dataType:"json",
success:(data)=>{
varusers=<User[]>data.items;
resolve(users);
},
error:(e)=>{
reject(e);
}
});
});
}
}
OncewehavefinisheddeclaringtheNotGenericUserRepositoryuserrepository,wecancreateaninstanceandinvokethegetAsyncmethod:
varnotGenericUserRepository=new
NotGenericUserRepository("./demos/shared/users.json");
notGenericUserRepository.getAsync()
.then(function(users:User[]){
console.log('notGenericUserRepository=>',users);
});
IfwealsoneedtorequestanotherlistofentitiesdifferentfromUser,wecouldendupduplicatingalotofcode.Imaginethatwealsoneedtorequestalistofconferencetalks.WecouldcreateanentitynamedTalkandanalmostidenticalrepositoryclass:
classTalk{
publictitle:string;
publicdescription:string;
publiclanguage:string;
publicurl:string;
publicyear:string;
}
classNotGenericTalkRepository{
private_url:string;
constructor(url:string){
this._url=url;
}
publicgetAsync(){
returnQ.Promise((resolve:(talks:Talk[])=>void,reject)=>{
$.ajax({
url:this._url,
type:"GET",
dataType:"json",
success:(data)=>{
varuserstalks=<Talk[]>data.items;
resolve(userstalks);
},
error:(e)=>{
reject(e);
}
});
});
}
}
Ifthenumberofentitiesgrows,wewillcontinuetorepeatedlyduplicatecode.Wemaythinkthatwecouldusetheanytypetoavoidthisproblem,butthenwewouldbelosingthesecurityprovidedbythecheckingtypeperformedbyTypeScriptatcompilationtime.AmuchbettersolutionistocreateaGenericrepository:
classGenericRepository<T>{
private_url:string;
constructor(url:string){
this._url=url;
}
publicgetAsync(){
returnQ.Promise((resolve:(entities:T[])=>void,reject)=>{
$.ajax({
url:this._url,
type:"GET",
dataType:"json",
success:(data)=>{
varlist=<T[]>data.items;
resolve(list);
},
error:(e)=>{
reject(e);
}
});
});
}
}
TherepositorycodeisidenticaltoNotGenericUserRepository,exceptfortheentitytype.WehaveremovedthehardcodedreferencetotheUserandTalkentitiesandreplacedthemwiththegenerictypeT.Wecannowdeclareasmanyrepositoriesaswewishwithoutduplicatingasinglelineofcode:
varuserRepository=newGenericRepository<User>("./demos/shared/users.json");
userRepository.getAsync()
.then((users:User[])=>{
console.log('userRepository=>',users);
});
vartalkRepository=newGenericRepository<Talk>("./demos/shared/talks.json");
talkRepository.getAsync()
.then((talks:Talk[])=>{
console.log('talkRepository=>',talks);
});
GenericconstraintsSometimes,wemightneedtorestricttheuseofagenericclass.Takethegenericrepositoryfromtheprevioussectionasanexample.Wehaveanewrequirement:weneedtoaddsomechangestovalidatetheentitiesloadedviaAJAX,andwewillreturnonlythevalidentities.
OnepossiblesolutionistousethetypeofoperatortoidentifythetypeofthegenerictypeparameterTwithinagenericclassorfunction:
//...
success:(data)=>{
varlist:T[];
varitems=<T[]>data.items;
for(vari=0;i<items.length;i++){
if(items[i]instanceofUser){
//validateuser
}
if(items[i]instanceofTalk){
//validatetalk
}
}
resolve(list);
}
//...
TheproblemisthatwewillhavetomodifyourGenericRepositoryclasstoaddextralogicwitheachnewentity.WewillnotaddthevalidationrulesintotheGenericRepositoryrepositoryclassbecauseagenericclassshouldnotbeawareofthetypeusedasthegenerictype.
AbettersolutionistoaddamethodnamedisValidtotheentities,whichwillreturntrueiftheentityisvalid:
//...
success:(data)=>{
varlist:T[];
varitems=<T[]>data.items;
for(vari=0;i<items.length;i++){
if(items[i].isValid()){//error
//...
}
}
resolve(list);
}
//...
ThesecondapproachfollowsthesecondSOLIDprinciple,theopen/closeprinciple,aswecancreatenewentitiesandthegenericrepositorywillcontinuetowork(openforextension),butnoadditionalchangestoitwillberequired(closedformodification).Theonlyproblemwiththisapproachisthat,ifweattempttoinvokeanentity'sisValidmethodinsidethegeneric
repository,wewillgetacompilationerror.
Theerroristhrownbecauseweareallowedtousethegenericrepositorywithanytype,butnotalltypeshaveamethodnamedisValid.Fortunately,thisissuecaneasilyberesolvedbyusingagenericconstraint.ConstraintswillrestrictthetypesthatweareallowedtouseasthegenerictypeparameterT.Wearegoingtodeclareaconstraint,soonlytypesthatimplementaninterfacenamedValidatableInterfacecanbeusedwiththegenericmethod.
Let'sstartbydeclaringaninterface:
interfaceValidatableInterface{
isValid():boolean;
}
Note
Thisexampleisincludedinthecompanionsourcecode.
Nowwecanproceedtoimplementtheinterface.Inthiscase,wemustimplementtheisValidmethod:
classUserimplementsValidatableInterface{
publicname:string;
publicpassword:string;
publicisValid():boolean{
//uservalidation...
returntrue;
}
}
classTalkimplementsValidatableInterface{
publictitle:string;
publicdescription:string;
publiclanguage:string;
publicurl:string;
publicyear:string;
publicisValid():boolean{
//talkvalidation...
returntrue;
}
}
Now,let'sdeclareagenericrepositoryandaddatypeconstraintsoonlytypesderivedfromValidatableInterfacewillbeacceptedasthegenerictypeparameterT:
classGenericRepositoryWithConstraint<TextendsValidatableInterface>{
private_url:string;
constructor(url:string){
this._url=url;
}
publicgetAsync(){
returnQ.Promise((resolve:(talks:T[])=>void,reject)=>{
$.ajax({
url:this._url,
type:"GET",
dataType:"json",
success:(data)=>{
varitems=<T[]>data.items;
for(vari=0;i<items.length;i++){
if(items[i].isValid()){
list.push(items[i]);
}
}
resolve(list);
},
error:(e)=>{
reject(e);
}
});
});
}
}
Note
Eventhoughwehaveusedaninterface,weusedtheextendskeywordandnottheimplementskeywordtodeclaretheconstraintintheprecedingexample.Thereisnospecialreasonforthat.ThisisjustthewaytheTypeScriptconstraintsyntaxworks.
Wecanthencreateasmanyrepositoriesaswewant:
varuserRepository=new
GenericRepositoryWithConstraint<User>("./users.json");
userRepository.getAsync()
.then(function(users:User[]){
console.log(users);
});
vartalkRepository=new
GenericRepositoryWithConstraint<Talk>("./talks.json");
talkRepository.getAsync()
.then(function(talks:Talk[]){
console.log(talks);
});
Ifweattempttouseaclassthatdoesn'timplementtheValidatableInterfaceofthegenerictypeparameterT,wewillgetacompilationerror.
MultipletypesingenerictypeconstraintsWecanonlyrefertoonetypewhendeclaringagenerictypeconstraint.Let'simaginethatweneedagenericclasstobeconstrained,soitonlyallowstypesthatimplementthefollowingtwointerfaces:
interfaceIMyInterface{
doSomething();
};
interfaceIMySecondInterface{
doSomethingElse();
};
Wemaythinkthatwecandefinetherequiredgenericconstraintasfollows:
classExample<TextendsIMyInterface,IMySecondInterface>{
privategenericProperty:T;
useT(){
this.genericProperty.doSomething();
this.genericProperty.doSomethingElse();//error
}
}
However,thiscodesnippetwillthrowacompilationerror.Wecannotspecifymultipletypeswhendeclaringagenerictypeconstraint.However,wecanworkaroundthisissuebytransformingIMyInterface,IMySecondInterfaceinsuper-interfaces:
interfaceIChildInterfaceextendsIMyInterface,IMySecondInterface{
}
IMyInterfaceandIMySecondInterfacearenowsuper-interfacesbecausetheyaretheparentinterfacesoftheIChildInterfaceinterface.WecanthendeclaretheconstraintusingtheIChildInterfaceinterface:
classExample<TextendsIChildInterface>{
privategenericProperty:T;
useT(){
this.genericProperty.doSomething();
this.genericProperty.doSomethingElse();
}
}
ThenewoperatoringenerictypesTocreateanewobjectwithingenericcode,weneedtoindicatethatthegenerictypeThasaconstructorfunction.Thismeansthatinsteadofusingtype:T,weshouldusetype:{new():T;}asfollows:
functionfactoryNotWorking<T>():T{
returnnewT();//compileerrorcouldnotfindsymbolT
}
functionfactory<T>():T{
vartype:{new():T;};
returnnewtype();
}
varmyClass:MyClass=factory<MyClass>();
ApplyingtheSOLIDprinciplesAswehavepreviouslymentioned,interfacesarefundamentalfeatureswhenitcomestofollowingtheSOLIDprinciples,andwehavealreadyputthefirsttwoSOLIDprinciplesintopractice.
Wehavealreadydiscussedthesingleresponsibilityprinciple.Now,wewillseerealexamplesofthethreeremainingprinciples.
TheLiskovsubstitutionprincipleTheLiskovsubstitutionprinciple(LSP)states,Subtypesmustbesubstitutablefortheirbasetypes.Let'stakealookatanexampletounderstandwhatthismeans.
WewilldeclareaclassnamedPersistanceService,theresponsibilityofwhichistopersistsomeobjectintosomesortofstorage.Wewillstartbydeclaringthefollowinginterface:
interfacePersistanceServiceInterface{
save(entity:any):number;
}
AfterdeclaringthePersistanceServiceInterfaceinterface,wecanimplementit.Wewillusecookiesasthestoragefortheapplication'sdata:
classCookiePersitanceServiceimplementsPersistanceServiceInterface{
save(entity:any):number{
varid=Math.floor((Math.random()*100)+1);
//Cookiepersistancelogic...
returnid;
}
}
WewillcontinuebydeclaringaclassnamedFavouritesController,whichhasadependencyonPersistanceServiceInterface:
classFavouritesController{
private_persistanceService:PersistanceServiceInterface;
constructor(persistanceService:PersistanceServiceInterface){
this._persistanceService=persistanceService;
}
publicsaveAsFavourite(articleId:number){
returnthis._persistanceService.save(articleId);
}
}
WecanfinallycreateaninstanceofFavouritesControllerandpassaninstanceofCookiePersitanceServiceviaitsconstructor:
varfavController=newFavouritesController(newCookiePersitanceService());
TheLSPallowsustoreplaceadependencywithanotherimplementationaslongasbothimplementationsarebasedinthesamebasetype;so,ifwedecidetostopusingcookiesasstorageandusetheHTML5localstorageAPIinstead,wecandeclareanewimplementation:
classLocalStoragePersitanceServiceimplementsPersistanceServiceInterface{
save(entity:any):number{
varid=Math.floor((Math.random()*100)+1);
//Localstoragepersistancelogic...
returnid;
}
}
WecanthenreplaceitwithouthavingtoaddanychangestotheFavouritesControllercontrollerclass.
varfavController=newFavouritesController(new
LocalStoragePersitanceService());
TheinterfacesegregationprincipleInterfacesareusedtodeclarehowtwoormoresoftwarecomponentscooperateandexchangeinformationwitheachother.Thisdeclarationisknownasapplicationprogramminginterface(API).Inthepreviousexample,ourinterfacewasPersistanceServiceInterface,anditwasimplementedbytheclassesLocalStoragePersitanceServiceandCookiePersitanceService.TheinterfacewasconsumedbytheFavouritesControllerclass;sowesaythatthisclassisaclientofthePersistanceServiceInterface'sAPI.
Theinterfacesegregationprinciple(ISP)statesthatnoclientshouldbeforcedtodependonmethodsitdoesnotuse.ToadheretotheISP,weneedtokeepinmindthatwhenwedeclaretheAPI(howtwoormoresoftwarecomponentscooperateandexchangeinformationwitheachother)ofourapplication'scomponents,thedeclarationofmanyclient-specificinterfacesisbetterthanthedeclarationofonegeneral-purposeinterface.Let'stakealookatanexample.
IfwedesignanAPItocontrolalltheelementsinavehicle(engine,radio,heating,navigation,lights…),wecouldhaveonegeneral-purposeinterface,whichallowsustocontroleverysingleelementofthevehicle:
interfaceVehicleInterface{
getSpeed():number;
getVehicleType:string;
isTaxPayed():boolean;
isLightsOn():boolean;
isLightsOff():boolean;
startEngine():void;
acelerate():number;
stopEngine():void;
startRadio():void;
playCd:void;
stopRadio():void;
}
Note
Thisexampleisincludedinthecompanionsourcecode.
Ifaclasshasadependency(client)intheVehicleInterfaceinterfacebutitonlywantstousetheradiomethods,wewillbefacingaviolationoftheISPbecause,aswehavealreadyseen,noclientshouldbeforcedtodependonmethodsitdoesnotuse.
ThesolutionistosplittheVehicleInterfaceinterfaceintomanyclient-specificinterfacessothatourclasscanadheretotheISPbydependingonlyontheRadioInterfaceinterface:
interfaceVehicleInterface{
getSpeed():number;
getVehicleType:string;
isTaxPayed():boolean;
isLightsOn():boolean;
}
interfaceLightsInterface{
isLightsOn():boolean;
isLightsOff():boolean;
}
interfaceRadioInterface{
startRadio():void;
playCd:void;
stopRadio():void;
}
interfaceEngineInterface{
startEngine():void;
acelerate():number;
stopEngine():void;
}
ThedependencyinversionprincipleThedependencyinversion(DI)principlestates,Dependuponabstractions.Donotdependuponconcretions.Intheprevioussection,weimplementedFavouritesControllerandwewereabletoreplaceanimplementationofPersistanceServiceInterfacewithanotherwithouthavingtoperformanyadditionalchangetoFavouritesController.ThiswaspossiblebecausewefollowedtheDIprinciple,asFavouritesControllerhasadependencyuponPersistanceServiceInterface(abstractions)ratherthanLocalStoragePersitanceServiceorCookiePersitanceService(concretions).
Note
Dependingonyourbackground,youmaywonderifthereareanyInversionofControl(IoC)containersavailableforTypeScript.WecanindeedfindsomeIoCcontainersavailableonline.However,becauseTypescript'sruntimedoesn'tsupportreflectionorinterfaces,theycanarguablybeconsideredpseudoIoCcontainersratherthanrealIoCcontainers.
Ifyouwanttolearnmoreaboutinversionofcontrol,Ihighlyrecommendthearticle,InversionofControlContainersandtheDependencyInjectionpattern,byMartinFowler,availableathttp://martinfowler.com/articles/injection.html.
NamespacesTypeScriptfeaturesnamespaces(previouslyknownasinternalmodules).Namespacesaremainlyusedtoorganizeourcode.
Ifweareworkingonalargeapplication,asthecodebasegrowswewillneedtointroducesomekindoforganizationschemetoavoidnamingcollisionsandmakeourcodeeasiertofollowandunderstand.
Wecanusenamespacestoencapsulateinterfaces,classes,andobjectsthataresomehowrelated.Forexample,wecouldwrapallourapplicationmodelsinsideaninternalmodulenamedmodel:
namespaceapp{
exportclassUserModel{
//...
}
}
Whenwedeclareanamespace,allitsentitiesareprivatebydefault.Wecanusetheexportkeywordtodeclarewhatpartsofournamespacewewishtomakepublic.
Weareallowedtonestanamespaceinsideanother.Let'screateafilenamedmodels.tsandaddthefollowingcodesnippettoit:
namespaceapp{
exportnamespacemodels{
exportclassUserModel{
//...
}
exportclassTalkModel{
//...
}
}
}
Intheprecedingexample,wehavedeclaredanamespacenamedapp,andinsideit,wehavedeclaredapublicnamespacenamedmodels,whichcontainstwopublicclasses:UserModelandTalkModel.WecanthencallthenamespacefromanotherTypeScriptfilebyindicatingthefullnamespacename:
varuser=newapp.models.UserModel();
vartalk=newapp.models.TalkModel();
Ifaninternalmodulebecomestoobig,itcanbedividedintomultiplefilestoincreaseitsmaintainability.Ifwetaketheprecedingexample,wecouldaddmorecontentstotheinternalmodulenamedappbyreferencingitinanotherfile.
Let'screateanewfilenamedvalidation.tsandaddthefollowingcodetoit:
namespaceapp{
exportnamespacevalidation{
exportclassUserValidator{
//...
}
exportclassTalkValidator{
//...
}
}
}
Let'screateafilenamedmain.tsandaddthefollowingcodetoit:
varuser=newapp.models.UserModel();
vartalk=newapp.models.TalkModel();
varuserValidator=newapp.validation.UserValidator();
vartalkValidator=newapp.validation.TalkValidator();
Eventhoughthenamespaces'modelsandvalidationareintwodifferentfiles,weareabletoaccessthemfromathirdfile.
Namespacecancontainperiods.Forexample,insteadofnestingthenamespaces(validationandmodels)insidetheappmodule,wecouldhaveusedperiodsinthevalidationandmodelinternalmodulenames:
namespaceapp.validation{
//...
}
namespaceapp.models{
//...
}
Theimportkeywordcanbeusedwithinaninternalmoduletoprovideanaliasforanothermodule:
importTalkValidatorAlias=app.validation.TalkValidator;
vartalkValidator=newTalkValidatorAlias();
Oncewehavefinisheddeclaringournamespaces,wecandecideifwewanttocompileeachoneintoJavaScriptorifweprefertoconcatenateallthefilesintoonesinglefile.
Wecanusethe--outflagtocompilealltheinputfilesintoasingleJavaScriptoutputfile:
tsc--outoutput.jsinput.ts
Thecompilerwillautomaticallyordertheoutputfilebasedonthereferencetagspresentinthefiles.WecanthenimportourfilesorfileusinganHTML<script>tag.
ModulesTypeScriptalsohastheconceptofexternalmodulesorjustmodules.Themaindifferencebetweenusingmodules(insteadofnamespaces)isthatafterdeclaringallourmodules,wewillnotimportthemusinganHTML<script>tagandwewillbeabletouseamoduleloaderinstead.
Amoduleloaderisatoolthatallowsustohavebettercontroloverthemoduleloadingprocess.Thisallowsustoperformtaskssuchasloadingfilesasynchronouslyorcombiningmultiplemodulesintoasinglehighlyoptimizedfilewithease.
Usingthe<script>tagisnotrecommendedbecausewhenawebbrowserfindsa<script>tag,itdownloadsthefileusingasynchronousrequests.Weshouldattempttoloadasmanyfilesaspossibleusingasynchronousrequestsbecausedoingsowillsignificantlyimprovethenetworkperformanceofawebapplication.
Note
WewilldiscovermoreaboutnetworkperformanceinChapter6,ApplicationPerformance.
TheJavaScriptversionspriortoECMAScript6(ES6)don'tincludenativemodulesupport.Developerswereforcedtodeveloptheirownmoduleloaders.Theopensourcecommunitytriedtocomeupwithimprovedsolutionsovertheyears.Asaresult,todaythereareseveralmoduleloadersavailable,andeachoneusesadifferentmoduledefinitionsyntax.Themostpopularonesareasfollows:
RequireJS:RequireJSusesasyntaxknownasasynchronousmoduledefinition(AMD)Browserify:BrowserifyusesasyntaxknownasCommonJS.SystemJS:SystemJSisauniversalmoduleloader,whichmeansthatitsupportsalltheavailablemodulesyntaxes(ES6,CommonJS,AMD,andUMD).
Note
Node.jsapplicationsalsousetheCommonJSsyntax.
Fortunately,TypeScriptallowsustochoosewhichkindofmoduledefinitionsyntax(ES6,CommonJS,AMD,SystemJS,orUMD)wewanttouseatruntime.
Wecanindicateourpreferencebyusingthe--moduleflagwhencompiling:
tsc--modulecommonjsmain.ts//useCommonJS
tsc--moduleamdmain.ts//useAMD
tsc--moduleumdmain.ts//useUMD
tsc--modulesystemmain.ts//useSytemJS
Whilewecanselectfourdifferentmoduledefinitionsyntaxesatruntime.However,onlytwoareavailableatdesigntime:
Externalmodulesyntax(ThedefaultmodulesyntaxintheTypeScriptversionspriorto1.5)ES6modulesyntax(TherecommendedexternalmodulesyntaxinTypeScript1.5orhigher)
Itisimportanttounderstandthatwecanuseonekindofmoduledefinitionsyntaxatdesigntime(ES6,CommonJS,AMD,SystemJS,orUMD)andanotheratruntime(externalmodulesorES6).
SincethereleaseofTypeScript1.5,itisrecommendedyouusetheECMAScript6moduledefinitionsyntaxbecauseitisbasedonstandards,andinthefuture,wewillbeabletousethissyntaxatbothdesigntimeandruntime.
Wewillnowtakealookateachoftheavailablemoduledefinitionsyntaxes.
ES6modules–runtimeanddesigntimeTypeScript1.5introducessupportfortheES6modulesyntax.Let'sdefineanexternalmoduleusingit:
classUserModel{
//...
}
export{UserModel};
Wehavedefinedanexternalmodule.Wedon'tneedtousethenamespacekeyword,butwemustcontinuetousetheexportkeyword.Weusedtheexportkeywordatthebottomofthemodule,butitisalsopossibletouseitjustbeforetheclasskeywordlikewedidintheinternalmoduleexample:
exportclassUserModel{
//...
}
Wecanalsoexportanentityusinganalias:
classUserModel{
//...
}
export{UserModelasUser};//UserModelexportedasUser
Anexportdeclarationexportsallmeaningsofaname:
interfaceUserModel{
//...
}
classUserModel{
//...
}
export{UserModel};//Exportsbothinterfaceandfunction
Toimportamodule,wemustusetheimportkeywordasfollows:
import{UserModel}from"./models";
Theimportkeywordcreatesavariableforeachimportedcomponent.Intheprecedingcodesnippet,anewvariablenamedUserModelisdeclaredanditsvaluecontainsareferencetotheUserModelclass,whichwasdeclaredandexportedinthemodels.tsfile.
Wecanusetheexportkeywordtoimportmultipleentitiesfromonemodule:
classUserValidator{
//...
}
classTalkValidator{
//...
}
export{UserValidator,TalkValidator};
Furthermore,wecanusetheimportkeywordtoimportmultipleentitiesfromasinglemoduleasfollows:
import{UserValidator,TalkValidator}from"./validation.ts"
Note
Throughouttherestofthisbook,wewillusetheES6syntaxatdesign-timeandtheCommonJSsyntaxatruntime.
Externalmodules–designtimeonlyBeforeTypeScript1.5,modulesweredeclaredusingakindofmodulesyntaxknownasexternalmodulesyntax.Thiskindofsyntaxwasusedatdesigntime(TypeScriptcode).However,oncecompiledintoJavaScript,itwastransformedandexecuted(runtime)intoAMD,CommonJS,UMD,orSystemJSmodules.
WeshouldtrytoavoidusingthissyntaxandusethenewES6syntaxinstead.However,wewilltakeaquicklookattheexternalmodulesyntaxbecausewemayhavetoworkonoldapplicationsoroutdateddocumentation.
Wecanimportamoduleusingtheimportkeyword:
importUser=require("./user_class");
TheprecedingcodesnippetdeclaresanewvariablenamedUser.TheUservariabletakestheexportedcontentoftheuser_classmoduleasitsvalue.
Toexportamodule,weneedtousetheexportkeyword.Wecanapplytheexportkeyworddirectlytoaclassorinterface:
exportclassUser{
//…
}
Wecanalsousetheexportkeywordonitsownbyassigningtoitthevaluethatwedesiretoexport:
classUser{
//…
}
export=User;
Externalmodulescanbecompiledintoanyoftheavailablemoduledefinitionsyntaxes(AMD,CommonJS,SystemJS,orUMD).
AMDmodules–runtimeonlyIfwecompiletheinitialexternalmoduleintoanAMDmodule(usingtheflag--compileamd),wewillgeneratethefollowingAMDmodule:
define(["require","exports"],function(require,exports){
varUserModel=(function(){
functionUserModel(){
}
returnUserModel;
})();
returnUserModel;
});
Thedefinefunctiontakesanarrayasitsfirstargument.Thisarraycontainsalistofthenamesofthemoduledependencies.Thesecondargumentisacallbackthatwillbeinvokedonceallthemoduledependencieshavebeenloaded.ThecallbacktakeseachofthemoduledependenciesasitsparametersandcontainsallthelogicfromourTypeScriptcomponent.Noticehowthereturntypeofthecallbackmatchesthecomponentsthatwedeclaredaspublicbyusingtheexportkeyword.AMDmodulescanthenbeloadedusingtheRequireJSmoduleloader.
Note
WewillnotdiscussAMDandRequireJSfurtherinthisbook,butifyouwanttolearnmoreaboutthem,youcandosobyvisitinghttp://requirejs.org/docs/start.html.
CommonJSmodules–runtimeonlyWebeginbycompilingourexternalmoduleintoaCommonJSmodule(usingtheflag--compilecommonjs).Wewillcompilethefollowingcodesnippet:
classUser{
//…
}
export=User;
Asaresult,thefollowingCommonJSmoduleisgenerated:
varUserModel=(function(){
functionUserModel(){
//…
}
returnUserModel;
})();
module.exports=UserModel;
Aswecanseeintheprecedingcodesnippet,theCommonJSmoduledefinitionsyntaxisalmostidenticaltothedeprecatedTypeScript(1.4orprior)externalmodulesyntax.
TheprecedingCommonJSmodulecanbeloadedbyaNode.jsapplicationwithoutanyadditionalchangesusingtheimportkeywordandtherequirefunction:
importUserModel=require('./UserModel');
varuser=newUserModel();
However,ifweattempttousetherequirefunctioninawebbrowser,anexceptionwillbethrownbecausetherequirefunctionisundefined.WecaneasilysolvethisproblembyusingBrowserify.
Allthatweneedtofollowisthreesimplesteps:
1. InstallBrowserifyusingnpm:
npminstall-gbrowserify
2. UseBrowserifytobundleallyourCommonJSmodulesintoaJavaScriptfilethatyoucanimportusinganHTML<script>tag.Wecandothisbyexecutingthefollowingcommand:
browserifymain.js-obundle.js
Intheprecedingcommand,main.jsisthefilethatcontainstherootmodulewithinourapplication'sdependencytree.Thebundle.jsfileistheoutputfilethatwewillbeabletoimportusingaHTMLscripttag.
3. Importthebundle.jsfileusingaHTML<script>tag.
Note
IfyouneedmoreinformationaboutBrowserify,visittheofficialdocumentationathttps://github.com/substack/node-browserify#usage.
UMDmodules–runtimeonlyIfwewanttoreleaseaJavaScriptlibraryorframework,wewillneedtocompileourTypeScriptapplicationintobothCommonJSandAMDmodules.OurlibraryshouldalsoallowdeveloperstoloaditdirectlyinawebbrowserusingaHTMLscripttag.
Thewebdevelopmentcommunityhasdevelopedthefollowingcodesnippettohelpustoachieveuniversalmoduledefinition(UMD)support:
(function(root,factory){
if(typeofexports==='object'){
//CommonJS
module.exports=factory(require('b'));
}elseif(typeofdefine==='function'&&define.amd){
//AMD
define(['b'],function(b){
return(root.returnExportsGlobal=factory(b));
});
}else{
//GlobalVariables
root.returnExportsGlobal=factory(root.b);
}
}(this,function(b){
//Youractualmodule
return{};
}));
Thiscodesnippetisgreat,butwewanttoavoidmanuallyaddingittoeverysinglemoduleinourapplication.Fortunately,thereareafewoptionsavailabletoachieveUMDsupportwithease.
Thefirstoptionistousetheflag--compileumdtogenerateoneUMDmoduleforeachmoduleinourapplication.ThesecondoptionistocreateonesingleUMDmodulethatwillcontainallthemodulesintheapplicationusingamoduleloaderknownasBrowserify.
Note
RefertotheofficialBrowserifyprojectwebsiteathttp://browserify.org/tolearnmoreaboutBrowserify.RefertotheBrowserify-standaloneoptiontolearnmoreaboutthegenerationofoneuniqueoptimizedfile.
SystemJSmodules–runtimeonlyWhileUMDgivesyouawaytooutputasinglemodulethatworksinbothAMDandCommonJS,SystemJSwillallowyoutouseES6modulesclosertotheirnativesemanticswithoutrequiringanES6-compatiblebrowserengine.
SytemJSisusedbyAngular2.0,whichistheupcomingversionofapopularwebapplicationdevelopmentframework.
Note
RefertotheofficialSytemJSprojectwebsiteathttps://github.com/systemjs/systemjstolearnmoreaboutSystemJS.
Thereisafreelistofcommonmodulemistakesavailableonlineathttp://www.typescriptlang.org/Handbook#modules-pitfalls-of-modules.
CirculardependenciesAcirculardependencyisanissuethatwecanencounterwhenworkingwithmultiplecomponentsanddependencies.Sometimes,itispossibletoreachapointinwhichonecomponent(A)hasadependencyonasecondcomponent(B),whichdependsonthefirstcomponent(A).Inthefollowinggraph,eachnodeisacomponent,andwecanobservethatthenodescircular1.tsandcircular2.tshaveacirculardependency.ThenodenameddoesNotDependOnAnything.tsdoesn'thavedependenciesandthenodenamedonlyDependsOnOtherStuff.tshasadependencyoncircular1.tsbutdoesn'thavecirculardependencies.
Thecirculardependenciesdon'tneedtonecessarilyinvolvejusttwocomponents.Wecanencounterscenariosinwhichacomponentdependsonanothercomponent,whichdependsonothercomponents,andsomeofthecomponentsinthedependencytreeenduppointingtooneoftheirparentcomponentsinthetree.
Identifyingacirculardependencyisverytimeconsuming.Fortunately,Atomincludesacommand-linetoolthatwillgenerateadependencytreegraphforusliketheprecedingone.InordertoaccesstheAtomcommandline,weneedtonavigatetoView(inthetopmenu)andthentoToggleCommandPalette.
AfteropeningtheToggleCommandPalette,weneedtotypeTypeScript:DependencyViewtodisplaythegraph:
Note
Ifyouwanttolearnmoreaboutdependencygraphs,youcanvisititsofficialdocumentationathttps://github.com/TypeStrong/atom-typescript/blob/master/docs/dependency-view.md.
SummaryInthischapter,wesawhowtoworkwithclasses,interfaces,andmodulesindepth.Wewereabletoreducethecomplexityofourapplicationbyusingtechniquessuchasencapsulationandinheritance.
WewerealsoabletocreateexternalmodulesandmanageourapplicationdependenciesusingtoolssuchasRequireJSorBrowserify.
Inthenextchapter,wewilldiscusstheTypeScriptruntime.
Chapter5.RuntimeAftercompletingthisbook,youwillprobablybeeagertostartanewprojecttoputintopracticeallyournewknowledge.Asthenewprojectgrowsandyoudevelopmorecomplexfeatures,youmightencountersomeruntimeissues.
Weshouldbeabletoresolvedesign-timeissueswitheasebecauseinthepreviouschapter,welookedatthemainTypeScriptfeatures.
However,wehavenotlearnedmuchabouttheTypeScriptruntime.Thegoodnewsisthat,dependingonyourbackground,youmayalreadyknowalotaboutit,astheTypeScriptruntimeistheJavaScriptruntime.TypeScriptisonlyusedatdesigntime;theTypeScriptcodeisthencompiledintoJavaScriptandfinallyexecuted.TheJavaScriptruntimeisinchargeoftheexecution.IsimportanttounderstandthatweneverexecuteTypeScriptcodeandwealwaysexecuteJavaScriptcode.Forthisreason,whenwerefertotheTypeScriptruntime,wewill,infact,betalkingabouttheJavaScriptruntime.
WhenwecompileourTypeScriptcode,wewillgenerateJavaScriptcode,whichwillbeexecutedontheserverside(withNode.js)orontheclientside(inawebbrowser).Itisthenthatwemayencountersomechallengingruntimeissues.
Inthischapter,wewillcoverthefollowingtopics:
TheenvironmentTheeventloopThethisoperatorPrototypesClosures
Let'sstartbylearningabouttheenvironment.
TheenvironmentTheruntimeenvironmentisoneofthefirstthingsthatwemustconsiderbeforewecanstartdevelopingaTypeScriptapplication.OncewehavecompiledourTypeScriptcode,itcanbeexecutedinmanydifferentJavaScriptengines.Whilethemajorityofthoseengineswillbewebbrowsers,suchasChrome,InternetExplorer,orFirefox,wemightalsowanttobeabletorunourcodeontheserversideorinadesktopapplicationinenvironmentssuchasNode.jsorRingoJS.
Itisimportanttokeepinmindthattherearesomevariablesandobjectsavailableatruntimethatareenvironment-specific.Forexample,wecouldcreatealibraryandaccessthedocument.layersvariable.WhiledocumentispartoftheW3CDocumentObjectModel(DOM)standard,thelayerspropertyisonlyavailableinInternetExplorerandisnotpartoftheW3CDOMstandard.
TheW3CdefinestheDOMasfollows:
TheDocumentObjectModelisaplatform-andlanguage-neutralinterfacethatwillallowprogramsandscriptstodynamicallyaccessandupdatethecontent,structureandstyleofdocuments.Thedocumentcanbefurtherprocessedandtheresultsofthatprocessingcanbeincorporatedbackintothepresentedpage.
Inasimilarmanner,wecanalsoaccessasetofobjectsknownastheBrowserObjectModel(BOM)fromawebbrowserruntimeenvironment.TheBOMconsistsoftheobjectsnavigator,history,screen,location,anddocument,whicharepropertiesofthewindowobject.
YouneedtorealizethattheDOMispartofthewebbrowsersbutnotpartofJavaScript.Ifwewanttorunourapplicationinawebbrowser,wewillbeabletoaccesstheDOMandBOM.However,inenvironmentslikeNode.jsorRingoJS,theywillnotbeavailable,sincetheyarestandaloneJavaScriptenvironmentscompletelyindependentofawebbrowser.Wecanalsofindotherobjectsontheserver-sideenvironments(suchasprocess.stdininNode.js)thatwillnotbeavailableifweattempttoexecuteourcodeinawebbrowser.
Asifthiswasn'tenoughwork,wealsoneedtokeepinmindtheexistenceofmultipleversionsoftheseJavaScriptenvironments.WewillhavetosupportmultiplebrowsersandmultipleversionsofNode.js.Therecommendedpracticewhendealingwiththisproblemistoaddlogicthatlooksfortheavailabilityoffeaturesratherthantheavailabilityofaparticularenvironmentorversion.
Note
Areallygoodlibraryisavailablethatcanhelpustoimplementfeaturedetectionwhendevelopingforwebbrowsers.ThelibraryiscalledModernizrandcanbedownloadedathttp://modernizr.com/.
TheruntimeTheTypeScriptruntime(JavaScript)hasaconcurrencymodelbasedonaneventloop.ThismodelisquitedifferenttothemodelsinotherlanguagessuchasCorJava.Beforewefocusontheeventloopitself,youmustunderstandsomeruntimeconcepts.
Whatfollowsisavisualrepresentationofsomeimportantruntimeconcepts:heap,stack,queue,andframe:
Wewillnowlookattheroleofeachoftheseruntimeconcepts.
FramesAframeisasequentialunitofwork.Intheprecedingdiagram,theframesarerepresentedbytheblocksinsidethestack.
WhenafunctioniscalledinJavaScript,theruntimecreatesaframeinthestack.Theframeholdsthatparticularfunction'sargumentsandlocalvariables.Whenthefunctionreturns,theframeispoppedoutofthestack.Let'stakealookatanexample:
functionfoo(b){
vara=12;
returna+b+35;
}
functionbar(x){
varm=4;
returnfoo(m*x);
}
Afterdeclaringthefooandbarfunctions,weinvokethebarfunction:
bar(21);
Whenbarisexecuted,theruntimewillcreateanewframecontainingtheargumentsofbarandallthelocalvariables.Theframe(representedasasquareintheprecedingdiagram)isthenaddedtothetopofthestack.
Internally,barinvokesfoo.Whenfooisinvoked,anewframeiscreatedandallocatedinthetopofthestack.Whentheexecutionoffooisfinished(foohasreturned),thetopframeisremovedfromthestack.Whentheexecutionofbarisalsocomplete,itisremovedfromthestackaswell.
Now,let'simaginewhatwouldhappenifthefoofunctioninvokedthebarfunction.Wewouldcreateanever-endingfunctioncallloop.Witheachfunctioncall,anewframewouldbeaddedtothestack,andeventually,therewouldbenomorespaceinthestack,andanerrorwouldbethrown.Mostdevelopersarefamiliarwiththiserror,knownasastackoverflowerror.
StackThestackcontainsthesequentialsteps(frames)thatamessageneedstoexecute.AstackisadatastructurethatrepresentsasimpleLastInFirstOut(LIFO)collectionofobjects.Therefore,whenaframeisaddedtothestack,itisalwaysaddedtothetopofthestack.
SincethestackisaLIFOcollection,theeventloopprocessestheframesstoredinitfromtoptobottom.Thedependenciesofaframeareaddedtothetopofitinthestacktoensurethatallthedependenciesofeachoftheframesaremet.
QueueThequeuecontainsalistofmessageswaitingtobeprocessed.Eachmessageisassociatedwithafunction.Whenthestackisempty,amessageistakenoutofthequeueandprocessed.Theprocessingconsistsofcallingtheassociatedfunctionandaddingtheframestothestack.Themessageprocessingendswhenthestackbecomesemptyagain.
Inthepreviousruntimediagram,theblocksinsidethequeuerepresentthemessages.
HeapTheheapisamemorycontainerthatisnotawareoftheorderoftheitemsstoredinit.Theheapcontainsallthevariablesandobjectscurrentlyinuse.Itmayalsocontainframesthatarecurrentlyoutofscopebuthavenotyetbeenremovedfrommemorybythegarbagecollector.
TheeventloopConcurrencyistheabilityfortwoormoreoperationstobeexecutedsimultaneously.Theruntimeexecutiontakesplaceononesinglethread,whichmeansthatwecannotachieverealconcurrency.
Theeventloopfollowsarun-to-completionapproach,whichmeansthatitwillprocessamessagefrombeginningtoendbeforeanyothermessageisprocessed.
Note
AswediscussedinChapter3,WorkingwithFunctions,wecanusetheyieldkeywordandgeneratorstopausetheexecutionofafunction.
Everytimeafunctionisinvoked,anewmessageisaddedtothequeue.Ifthestackisempty,thefunctionisprocessed(theframesareaddedtothestack).
Whenalltheframeshavebeenaddedtothestack,thestackisclearedfromtoptobottom.Attheendoftheprocess,thestackisemptyandthenextmessageisprocessed.
Note
Webworkerscanperformancebackgroundtasksinadifferentthread.Theyhavetheirownqueue,heap,andstack.
Oneoftheadvantagesoftheeventloopisthattheexecutionorderisquitepredictableandeasytofollow.Anotherimportantadvantageoftheeventloopapproachisthatitfeaturesnon-blockingI/O.Thismeansthatwhentheapplicationiswaitingforaninputandoutput(I/O)operationtofinish,itcanstillprocessotherthings,suchasuserinput.
Adisadvantageofthisapproachisthatifamessagetakestoolongtocomplete,theapplicationbecomesunresponsive.Goodpracticeistomakemessageprocessingshort,andifpossible,splitonemessagefunctionintoseveralmessagesfunctions.
ThethisoperatorInJavaScript,thethisoperatorbehavesalittledifferentlythanotherlanguages.Thevalueofthethisoperatorisoftendeterminedbythewayafunctionisinvoked.Itsvaluecannotbesetbyassignmentduringexecution,anditmaybedifferenteachtimeafunctionisinvoked.
Note
Thethisoperatoralsohassomedifferenceswhenusingthestrictandnonstrictmodes.Tolearnmoreaboutthestrictmode,refertohttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode.
ThethisoperatorintheglobalcontextIntheglobalcontext,thethisoperatorwillalwayspointtotheglobalobject.Inawebbrowser,thewindowobjectistheglobalobject:
console.log(this===window);//true
this.a=37;
console.log(window.a);//37
console.log(this.document===document===window.document);//true
ThethisoperatorinafunctioncontextThevalueofthisinsideafunctiondependsonhowthefunctionisinvoked.Ifwesimplyinvokeafunctioninthenonstrictmode,thevalueofthiswithinthefunctionwillpointtotheglobalobject:
functionf1(){
returnthis;
}
f1()===window;//true
However,ifweinvokeafunctioninthestrictmode,thevalueofthiswithinthefunction'sbodywillpointtoundefined:
console.log(this);//global(window)
functionf2(){
"usestrict";
returnthis;//undefined
}
console.log(f2());//undefined
console.log(this);//window
However,thevalueofthethisoperatorinsideafunctioninvokedasaninstancemethodpointstotheinstance.Inotherwords,thevalueofthethisoperatorwithinafunctionthatispartofaclasspointstothatclass:
varp={
age:37,
getAge:function(){
returnthis.age;//thispointstotheclassinstance(p)
}
};
console.log(p.getAge());//37
Intheprecedingexample,wehaveusedobjectliteralnotationtodefineanobjectnamedp,butthesameapplieswhendeclaringobjectsusingprototypes:
functionPerson(){}
Person.prototype.age=37;
Person.prototype.getAge=function(){
returnthis.age;
}
varp=newPerson();
p.age;//37
p.getAge();//37
Whenafunctionisusedasaconstructor(withthenewkeyword),thethisoperatorpointstotheobjectbeingconstructed:
functionPerson(){//functionusedasaconstructor
this.age=37;
}
varp=newPerson();
console.log(p.age);//logs37
Thecall,apply,andbindmethodsAllthefunctionsinheritthecall,apply,andbindmethodsfromFunction.prototype.Wecanusethesemethodstosetthevalueofthethisoperatorwhenitisusedinsidethebodyofafunction.
Thecallandapplymethodsarealmostidentical;bothmethodsallowustoinvokeafunctionandsetthevalueofthethisoperatorwithinthefunction.Themaindifferencebetweencallandapplyisthatwhileapplyletsusinvokethefunctionwithargumentsasanarrayandcallrequiresthefunctionparameterstobelistedexplicitly.
Tip
AusefulmnemonicisA(apply)forarrayandC(call)forcomma.
Let'stakealookatanexample.WewillstartbydeclaringaclassnamedPerson.Thisclasshastwoproperties(nameandsurname)andonemethod(greet).Thegreetmethodusesthethisoperatortoaccessthenameandsurnameinstanceproperties:
classPerson{
publicname:string;
publicsurname:string;
constructor(name:string,surname:string){
this.name=name;
this.surname=surname;
}
publicgreet(city:string,country:string){
//weusethethisoperatortoaccessnameandsurname
varmsg=`Hi,mynameis${this.name}${this.surname}.`;
msg+=`I'mfrom${city}(${country}).`;
console.log(msg);
}
}
AfterdeclaringthePersonclass,wewillcreateaninstance:
varperson=newPerson("remo","jansen");
Ifweinvokethegreetmethod,itwillworkasexpected:
person.greet.("Seville","Spain");
//Hi,mynameisremojansen.I'mfromSeville(Spain).
Alternatively,wecaninvokethemethodusingthecallandapplyfunctions.Wehavesuppliedthepersonobjectasthefirstparameterofbothfunctionsbecausewewantthethisoperator(insidethegreetmethod)totakepersonasitsvalue:
person.greet.call(person,"seville","spain");
person.greet.apply(person,["seville","spain"]);
Ifweprovideadifferentvaluetobeusedasthevalueofthis,wewillnotbeabletoaccessthenameandsurnamepropertieswithinthegreetfunction:
person.greet.call(null,"seville","spain");
person.greet.apply(null,["seville","spain"]);
//Hi,mynameisundefined.I'mfromsevillespain.
Thetwoprecedingexamplesmayseemuselessbecausethefirstoneinvokedthefunctiondirectlyandthesecondonecausedanunexpectedbehavior.Theapplyandcallmethodsmakesenseonlywhenwewantthethisoperatortotakeadifferentvaluewhenafunctionisinvoked:
varvalueOfThis={name:"anakin",surname:"skywalker"};
person.greet.call(valueOfThis,"mosespa","tatooine");
person.greet.apply(valueOfThis,["mosespa","tatooine"]);
//Hi,mynameisanakinskywalker.I'mfrommosespatatooine.
Thebindmethodcanbeusedtosetthevalueofthethisoperator(withinafunction)regardlessofhowitisinvoked.
Whenweinvokeafunction'sbindmethod,itreturnsanewfunctionwiththesamebodyandscopeastheoriginalfunction,butthethisoperator(withinthebodyfunction)ispermanentlyboundtothefirstargumentofbind,regardlessofhowthefunctionisinvoked.
Let'stakealookatanexample.WewillstartbycreatinganinstanceofthePersonclassthatwedeclaredinthepreviousexample:
varperson=newPerson("remo","jansen");
Then,wecanusebindtosetthegreetfunctiontobeanewfunctionwiththesamescopeandbody:
vargreet=person.greet.bind(person);
Ifwetrytoinvokethegreetfunctionusingbindandapply,justlikewedidinthepreviousexample,wewillbeabletoobservethatthistimethethisoperatorwillalwayspointtotheobjectinstanceindependentofhowthefunctionisinvoked:
greet.call(person,"seville","spain");
greet.apply(person,["seville","spain"]);
//Hi,mynameisremojansen.I'mfromsevillespain.
greet.call(null,"seville","spain");
greet.apply(null,["seville","spain"]);
//Hi,mynameisremojansen.I'mfromsevillespain.
varvalueOfThis={name:"anakin",surname:"skywalker"};
greet.call(valueOfThis,"mosespa","tatooine");
greet.apply(valueOfThis,["mosespa","tatooine"]);
//Hi,mynameisremojansen.I'mfrommosespatatooine.
Note
Usingtheapply,call,andbindfunctionsisnotrecommendedunlessyoureallyknowwhatyouaredoing,becausetheycanleadtocomplexruntimeissuesforotherdevelopers.
Oncewebindanobjecttoafunctionwithbind,wecannotoverrideit:
varvalueOfThis={name:"anakin",surname:"skywalker"};
vargreet=person.greet.bind(valueOfThis);
greet.call(valueOfThis,"mosespa","tatooine");
greet.apply(valueOfThis,["mosespa","tatooine"]);
//Hi,mynameisremojansen.I'mfrommosespatatooine.
Note
Theuseofthebind,apply,andcallmethodsisoftendiscouragedbecauseitcanleadtoconfusion.Modifyingthedefaultbehaviorofthethisoperatorcanleadtoreallyunexpectedresults.Remembertousethesemethodsonlywhenstrictlynecessaryandtodocumentyourcodeproperlytoreducetheriskcausedbypotentialmaintainabilityissues.
PrototypesWhenwecompileaTypeScriptprogram,allclassesandobjectsbecomeJavaScriptobjects.Sometimes,wewillencounterourapplicationbehavingunexpectedlyatruntime,andwewillnotbeabletoidentifyandunderstandtherootcauseofthisbehaviorwithoutagoodunderstandingofhowinheritanceworksinJavaScript.Thisunderstandingwillallowustohavemuchbettercontroloverourapplicationatruntime.
Theruntimeinheritancesystemusesaprototypalinheritancemodel.Inaprototypalinheritancemodel,objectsinheritfromobjects,andtherearenoclassesavailable.However,wecanuseprototypestosimulateclasses.Let'sseehowitworks.
Atruntime,almosteveryJavaScriptobjecthasaninternalpropertycalledprototype.Thevalueoftheprototypeattributeisanobject,whichcontainssomeattributes(data)andmethods(behavior).
InTypeScript,wecanuseaclass-basedinheritancesystem:
classPerson{
publicname:string;
publicsurname:string;
publicage:number=0;
constructor(name:string,surname:string){
this.name=name;
this.surname=surname;
}
greet(){
varmsg=`Hi!mynameis${this.name}${this.surname}`;
msg+=`I'm${this.age}`;
}
}
WehavedefinedaclassnamedPerson.Atruntime,thisclassisdeclaredusingprototypesinsteadofclasses:
varPerson=(function(){
functionPerson(name,surname){
this.age=0;
this.name=name;
this.surname=surname;
}
Person.prototype.greet=function(){
varmsg="Hi!mynameis"+this.name+
""+this.surname;
msg+="I'm"+this.age;
};
returnPerson;
})();
TheTypeScriptcompilerwrapstheobjectdefinition(wewillnotreferitastheclass
definitionbecausetechnically,itisnotaclass)withanimmediatelyinvokedfunctionexpression(IIFE).InsidetheIIFE,wecanfindafunctionnamedPerson.IfweexaminethefunctionandcompareittotheTypeScriptclass,wewillnoticethatittakesthesameparameters,liketheconstructorintheTypeScriptclass.ThisfunctionisusedtocreatenewinstancesofthePersonclass.
Aftertheconstructor,wecanseethedefinitionofthegreetmethod.Asyoucansee,theprototypeattributeisusedtoattachthegreetmethodtothePersonclass.
InstancepropertiesversusclasspropertiesAsJavaScriptisadynamicprogramminglanguage,wecanaddpropertiesandmethodstoaninstanceofanobjectatruntime;andtheydon'tneedtobepartoftheobject(class)itself.Let'stakealookatanexample:
functionPerson(name,surname){
//instanceproperties
this.name=name;
this.surname=surname;
}
varme=newPerson("remo","jansen");
me.email="[email protected]";
Here,wedefinedaconstructorfunctionforanobjectnamedPerson,whichtakestwovariables(nameandsurname)asarguments.Then,wehavecreatedaninstanceofthePersonobjectandaddedanewpropertynamedemailtoit.Wecanuseafor…instatementtocheckthepropertiesofmeatruntime:
for(varpropertyinme){
console.log("property:"+property+",value:'"+me[property]+"'");
}
//property:name,value:'remo'
//property:surname,value:'jansen'
//property:email,value:'[email protected]'
//property:greet,value:'function(city,country){
//varmsg="Hi,mynameis"+this.name+""+//this.surname;
//msg+="\nI'mfrom"+city+""+country;
//console.log(msg);
//}'
Allthesepropertiesareinstancepropertiesbecausetheyholdavalueforeachnewinstance.If,forexample,wecreateanewinstanceofPerson,bothinstanceswillholdtheirownvalues:
varhero=newPerson("John","117");
hero.name;//"John"
me.name;//"remo"
Wehavedefinedtheseinstancepropertiesusingthethisoperator,becauseintheclassconstructor,thethisoperatorpointstotheobject'sprototype.Thisexplainswhywecanalternativelydefineinstancepropertiesthroughtheobject'sprototype:
Person.prototype.name=name;//instanceproperty
Person.prototype.name=surname;//instanceproperty
Wecanalsodeclareclasspropertiesandmethods.Themaindifferenceisthatthevalueofclasspropertiesandmethodsissharedbetweenalltheinstancesofanobject.Classpropertiesandmethodsaresometimescalledstaticpropertiesandmethods.
Classpropertiesareoftenusedtostorestaticvalues:
functionMathHelper(){
/*...*/
}
//classproperty
MathHelper.PI=3.14159265359;
Classmethodsarealsooftenusedasutilityfunctionsthatperformcalculationsuponsuppliedparametersandreturnaresult:
functionMathHelper(){/*...*/}
//classmethod
MathHelper.areaOfCircle=function(radius){
returnradius*radius*this.PI;
}
//classproperty
MathHelper.PI=3.14159265359;
Intheprecedingexample,wehaveaccessedaclassattribute(PI)fromaclassmethod(areaOfCircle).Wecanaccessclasspropertiesfrominstancemethods,butwecannotaccessinstancepropertiesormethodsfromclasspropertiesormethods.WecandemonstratethisbydeclaringPIasaninstancepropertyinsteadofaclassproperty:
functionMathHelper(){
//instanceproperty
this.PI=3.14159265359;
}
IfwethenattempttoaccessPIfromaclassmethod,itwillbeundefined:
//classmethod
MathHelper.areaOfCircle=function(radius){
returnradius*radius*this.PI;//this.PIisundefined
}
MathHelper.areaOfCircle(5);//NaN
Wearenotsupposedtoaccessclassmethodsorpropertiesfrominstancemethods,butthereisawaytodoit.Wecanachieveitusingtheprototype'sconstructorproperty.Wecanalsodemonstratethisasfollows:
functionMathHelper(){/*...*/}
//classproperty
MathHelper.PI=3.14159265359;
//instancemethod
MathHelper.prototype.areaOfCircle=function(radius){
returnradius*radius*this.constructor.PI;
}
varmath=newMathHelper();
console.log(MathHelper.areaOfCircle(5));//78.53981633975
WecanaccessPI(theclassproperty)fromareaOfCircle(theinstancemethod)usingtheprototype'sconstructorpropertybecausethispropertyreturnsareferencetotheobject'sconstructor.
InsideareaOfCircle,thethisoperatorreturnsareferencetotheobject'sprototype:
this===MathHelper.prototype//true
Wemaydeducethatthis.constructorisequaltoMathHelper.prototype.constructorand,therefore,MathHelper.prototype.constructorisequaltoMathHelper.
PrototypalinheritanceYoumightbewonderinghowtheextendskeywordworks.Let'screateanewTypeScriptclass,whichinheritsfromthePersonclass,tohelpyouunderstandit:
classSuperHeroextendsPerson{
publicsuperpower:string;
constructor(name:string,surname:string,superpower:string){
super(name,surname);
this.superpower=superpower;
}
userSuperPower(){
return`I'musingmy${this.superpower}`
}
}
TheprecedingclassisnamedSuperHeroandextendsthePersonclass.Ithasoneextraattribute(superpower)andmethod(useSuperPower).Ifwecompilethecode,wewillnoticethefollowingpieceofcode:
var__extends=this.__extends||function(d,b){
for(varpinb)if(b.hasOwnProperty(p))d[p]=b[p];
function__(){this.constructor=d;}
__.prototype=b.prototype;
d.prototype=new__();
};
ThispieceofcodeisgeneratedbyTypeScript.Eventhoughitisareallysmallpieceofcode,itshowcasesalmosteveryconceptcontainedinthischapter,andunderstandingitcanbequitechallenging.Wemightneedtoexamineitmultipletimestounderstandit,buttheeffortisworthit.Let'stakealookatthefunction.
Beforethefunctionexpressionisevaluatedforthefirsttime,thethisoperatorpointstotheglobalobject,whichdoesnotcontainamethodnamed__extends.Thismeansthatthe__extendsvariableisundefinedatthispoint:
console.log(this.__extends);//undefined
Whenthefunctionexpressionisevaluatedforthefirsttime,thevalueofthefunctionexpression(ananonymousfunction)isassignedtothe__extendspropertyintheglobalscope:
console.log(this.__extends);//extends(n,e,t);
TypeScriptgeneratesthefunctionexpressiononceforeachTypeScriptfilecontainingtheextendskeyword.However,thefunctionexpressionisonlyevaluatedonce(whenthe__extendsvariableisundefined).Thisbehaviorisimplementedinthefirstlineofcode:
var__extends=this.__extends||function(d,b){//...
Thefirsttimethislineofcodeisexecuted,thefunctionexpressionisevaluated.Thevalueofthefunctionexpressionisananonymousfunction,whichisassignedtothe__extendsvariableintheglobalscope.Asweareintheglobalscope,var__extendsandthis._extendsrefertothesamevariableatthispoint.
Whenanewfileisexecuted,the__extendsvariableisalreadyavailableintheglobalscopeandthefunctionexpressionisnotevaluated.Thismeansthatthevalueofthefunctionexpressionisonlyassignedtothe__extendsvariableonce.
Asyoualreadyknow,thevalueofthefunctionexpressionisananonymousfunction.Let'snowfocusonit:
function(d,b){
for(varpinb)if(b.hasOwnProperty(p))d[p]=b[p];
function__(){this.constructor=d;}
__.prototype=b.prototype;
d.prototype=new__();
}
Thisfunctiontakestwoargumentsnameddandb.Whenweinvokeit,weshouldpassaderivedobjectconstructor(d)andabaseobjectconstructor(b).
Thefirstlineinsidetheanonymousfunctioniterateseachclasspropertyandmethodfromthebaseclassandcreatestheircopyinthederivedclass:
for(varpinb)if(b.hasOwnProperty(p))d[p]=b[p];
Note
Whenweuseafor…instatementtoiterateaninstanceofanobject,itwilliteratetheobject'sinstanceproperties.However,ifweuseafor…instatementtoiteratethepropertiesofanobject'sconstructor,thestatementwilliterateitsclassproperties.Intheprecedingexample,thefor…instatementisusedtoinherittheobject'sclasspropertiesandmethods.Toinherittheinstanceproperties,wewillcopytheobject'sprototype.
Thesecondlinedeclaresanewconstructorfunctionnamed__,andinsideit,thethisoperatorisusedtoaccessitsprototype:
function__(){this.constructor=d;}
Theprototypecontainsaspecialpropertynamedconstructor,whichreturnsareferencetotheobject'sconstructor.Thefunctionnamed__andthis.constructorarepointingtothesamevariableatthispoint.Thevalueofthederivedobjectconstructor(d)isthenassignedtothe__constructor.
Inthethirdline,thevalueoftheprototypeobjectfromthebaseobjectconstructorisassignedtotheprototypeofthe__objectconstructor:
__.prototype=b.prototype;
Inthelastline,anew__()isinvoked,andtheresultisassignedtothederivedclass(d)prototype.Byperformingallthesesteps,wehaveachievedallthatweneedtoinvokethefollowing:
varinstance=newd():
Upondoingso,wewillgetanobjectthatcontainsallthepropertiesfromboththederivedclass(d)andthebaseclass(b).Furthermore,theinstanceofoperatorwillworkaswewouldexpect:
varsuperHero=newSuperHero();
console.log(superHeroinstanceofPerson);//true
console.log(superHeroinstanceofSuperHero);//true
WecanseethefunctioninactionbyexaminingtheruntimecodethatdefinestheSuperHeroclass:
varSuperHero=(function(_super){
__extends(SuperHero,_super);
functionSuperHero(name,surname,superpower){
_super.call(this,name,surname);
this.superpower=superpower;
}
SuperHero.prototype.userSuperPower=function(){
return"I'musingmy"+superpower;
};
returnSuperHero;
})(Person);
WecanseeanIIFEhereagain.Thistime,theIIFEtakesthePersonobjectconstructorastheargument.Insidethefunction,wewillrefertothisargumentusingthename_super.InsidetheIIFE,the__extendsfunctionisinvokedandtheSuperHero(derivedclass)and_super(baseclass)argumentsarepassedtoit.
Inthenextline,wecanfindthedeclarationoftheSuperHeroobjectconstructorandtheuseSuperPowerfunction.WecanuseSuperHeroasanargumentof__extendbeforeitisdeclared,becausefunctionsdeclarationsarehoistedtothetopofthescope.
Note
Functionexpressionsarenothoisted.Whenweassignafunctiontoavariableinafunctionexpression,thevariableishoisted,butitsvalue(thefunctionitself)isnothoisted.
InsidetheSuperHeroconstructor,thebaseclass(Person)constructorisinvokedusingthecallmethod:
_super.call(this,name,surname);
Aswediscussedpreviouslyinthischapter,wecanusecalltosetthevalueofthethisoperatorinafunctioncontext.Inthiscase,wearepassingthethisoperator,whichpointsto
theinstanceofSuperHerobeingcreated:
functionPerson(name,surname){
//thispointstotheinstanceofSuperHerobeingcreated
this.name=name;
this.surname=surname;
}
TheprototypechainWhenwetrytoaccessapropertyormethodofanobject,theruntimewillsearchforthatpropertyormethodintheobject'sownpropertiesandmethods.Ifitisnotfound,theruntimewillcontinuesearchingthroughtheobject'sinheritedpropertiesbynavigatingtheentireinheritancetree.Asaderivedobjectislinkedtoitsbaseobjectthroughtheprototypeproperty,werefertothisinheritancetreeastheprototypechain.
Let'stakealookatanexample.WewilldeclaretwosimpleTypeScriptclassesnamedBaseandDerived:
classBase{
publicmethod1(){return1;};
publicmethod2(){return2;};
}
classDerivedextendsBase{
publicmethod2(){return3;};
publicmethod3(){return4;};
}
Now,wewillexaminetheJavaScriptcodegeneratedbyTypeScript:
varBase=(function(){
functionBase(){
}
Base.prototype.method1=function(){return1;};
;
Base.prototype.method2=function(){return2;};
;
returnBase;
})();
varDerived=(function(_super){
__extends(Derived,_super);
functionDerived(){
_super.apply(this,arguments);
}
Derived.prototype.method2=function(){return3;};
;
Derived.prototype.method3=function(){return4;};
;
returnDerived;
})(Base);
WecanthencreateaninstanceoftheDerivedclass:
varderived=newDerived();
Ifwetrytoaccessthemethodnamedmethod1,theruntimewillfinditintheinstance'sownproperties:
console.log(derived.method1());//1
Theinstancealsohasitsownpropertynamedmethod2(withvalue2),butthereisalsoaninheritedpropertynamedmethod2(withvalue3).Theobject'sownproperty(method2withvalue3)preventsaccesstotheprototypeproperty(method2withvalue2).Thisisknownaspropertyshadowing:
console.log(derived.method2());//3
Theinstancedoesnothaveitsownpropertynamedmethod3,butithasapropertynamedmethod3initsprototype:
console.log(derived.method3());//4
Boththeinstanceandtheobjectsintheprototypechain(theBaseclass)don'thaveapropertynamedmethod4:
console.log(derived.method4());//error
AccessingtheprototypeofanobjectPrototypescanbeaccessedinthreedifferentways:
Person.prototype:WecanaccesstheprototypeofafunctiondirectlyusingtheprototypeattributePerson.getPrototypeOf(person):WewantthisfunctiontoaccesstheprototypeofaninstanceofanobjectwecanusethegetPrototypeOffunctionperson.__proto__:Thisisapropertythatexposestheinternalprototypeoftheobjectthroughwhichitisaccessed
Note
Theuseof__proto__iscontroversialandhasbeendiscouragedbymany.ItwasneveroriginallyincludedintheECMAScriptlanguagespec,butmodernbrowsersdecidedtoimplementitanyway.Today,the__proto__propertyhasbeenstandardizedintheECMAScript6languagespecificationandwillbesupportedinthefuture,butitisstillaslowoperationthatshouldbeavoidedifperformanceisaconcern.
ThenewoperatorWecanusethenewoperatortogenerateaninstanceofPerson:
varperson=newPerson("remo","jansen");
Theruntimedoesnotfollowaclass-basedinheritancemodel.Whenweusethenewoperator,theruntimecreatesanewobjectthatinheritsfromthePersonclassprototype.
Wemayconcludethatthebehaviorofthenewoperatoratruntime(JavaScript)isnotreallydifferentfromtheextendskeywordatdesigntime(TypeScript).
ClosuresClosuresareoneofthemostpowerfulfeaturesavailableatruntime,buttheyarealsooneofthemostmisunderstood.TheMozilladevelopernetworkdefinesclosuresasfollows:
"Closuresarefunctionsthatrefertoindependent(free)variables.Inotherwords,thefunctiondefinedintheclosure'remembers'theenvironmentinwhichitwascreated."
Weunderstandindependent(free)variablesasvariablesthatpersistbeyondthelexicalscopefromwhichtheywerecreated.Let'stakealookatanexample:
functionmakeArmy(){
varshooters=[]
for(vari=0;i<10;i++){
varshooter=function(){//ashooterisafunction
alert(i)//whichshouldalertit'snumber
}
shooters.push(shooter)
}
returnshooters;
}
WehavedeclaredafunctionnamedmakeArmy.Insidethefunction,wehavecreatedanarrayoffunctionsnamedshooters.Eachfunctionintheshootersarraywillalertanumber,thevalueofwhichwassetfromthevariableiinsideaforstatement.WewillnowinvokethemakeArmyfunction:
vararmy=makeArmy();
Thearmyvariableshouldnowcontainthearrayoffunctionsshooters.However,wewillnoticeaproblemifweexecutethefollowingpieceofcode:
army[0]();//10(expected0)
army[5]();//10(expected5)
Theprecedingcodesnippetdoesnotworkasexpectedbecausewemadeoneofthemostcommonmistakesrelatedtoclosures.WhenwedeclaredtheshooterfunctioninsidethemakeArmyfunction,wecreatedaclosurewithoutknowingit.
Thereasonforthisisthatthefunctionsassignedtoshooterareclosures;theyconsistofthefunctiondefinitionandthecapturedenvironmentfromthemakeArmyfunction'sscope.Tenclosureshavebeencreated,buteachonesharesthesamesingleenvironment.Bythetimetheshooterfunctionsareexecuted,theloophasrunitscourseandtheivariable(sharedbyalltheclosures)hasbeenleftpointingtothelastentry(10).
Onesolutioninthiscaseistousemoreclosures:
functionmakeArmy(){
varshooters=[]
for(vari=0;i<10;i++){
(function(i){
varshooter=function(){
alert(i);
}
shooters.push(shooter)
})(i);
}
returnshooters;
}
vararmy=makeArmy();
army[0]();//0
army[5]();//5
Thisworksasexpected.Ratherthantheshooterfunctionssharingasingleenvironment,theimmediatelyinvokedfunctioncreatesanewenvironmentforeachone,inwhichireferstothecorrespondingvalue.
StaticvariableswithclosuresIntheprevioussection,wesawthatwhenavariableisdeclaredinaclosurecontextitcanbesharedbetweenmultipleinstancesofaclass,orinotherwords,thevariablebehavesasastaticvariable.
Wewillnowseehowwecancreatevariablesandmethodsthatbehavelikestaticvariables.Let'sstartbydeclaringaTypeScriptclassnamedCounter:
classCounter{
privatestatic_COUNTER=0;
constructor(){}
private_changeBy(val){
Counter._COUNTER+=val;
}
publicincrement(){
this._changeBy(1);
}
publicdecrement(){
this._changeBy(-1);
}
publicvalue(){
returnCounter._COUNTER;
}
}
Theprecedingclasscontainsastaticmembernamed_COUNTER.TheTypeScriptcompilertransformsitintothefollowingresultingcode:
varCounter=(function(){
functionCounter(){
}
Counter.prototype._changeBy=function(val){
Counter._COUNTER+=val;
};
Counter.prototype.increment=function(){
this._changeBy(1);
};
Counter.prototype.decrement=function(){
this._changeBy(-1);
};
Counter.prototype.value=function(){
returnCounter._COUNTER;
};
Counter._COUNTER=0;
returnCounter;
})();
Asyoucanobserve,thestaticvariableisdeclaredbytheTypeScriptcompilerasaclassproperty(asopposedtoaninstanceproperty).Thecompilerusesaclasspropertybecauseclasspropertiesaresharedacrossallinstancesofaclass.
Alternatively,wecouldwritesomeJavaScript(rememberthatallvalidJavaScriptisvalidTypeScript)codetoemulatestaticpropertiesusingclosures:
varCounter=(function(){
//closurecontext
var_COUNTER=0;
functionchangeBy(val){
_COUNTER+=val;
}
functionCounter(){};
Counter.prototype.increment=function(){
changeBy(1);
};
Counter.prototype.decrement=function(){
changeBy(-1);
};
Counter.prototype.value=function(){
return_COUNTER;
};
returnCounter;
})();
TheprecedingcodesnippetdeclaresaclassnamedCounter.Theclasshassomemethodsusedtoincrement,decrement,andreadthevariablenamed_COUNTER.The_COUNTERvariableitselfisnotpartoftheobjectprototype.
TheCounterconstructorfunctionispartofaclosure.Asaresult,alltheinstancesoftheCounterclasswillsharethesameclosurecontext,whichmeansthatthecontext(thevariablecounterandthefunctionchangeBy)willbehaveasasingleton.
Note
Thesingletonpatternrequiresanobjecttobedeclaredasastaticvariabletoavoidtheneedtocreateitsinstancewheneveritisrequired.Theobjectinstanceis,therefore,sharedbyallthecomponentsintheapplication.Thesingletonpatternisfrequentlyusedinscenarioswhereitisnotbeneficial,whichintroducesunnecessaryrestrictionsinsituationswhereauniqueinstanceofaclassisnotactuallyrequired,andintroducesglobalstatesintoanapplication.
So,younowknowthatitispossibletouseclosurestoemulatestaticvariables:
varcounter1=newCounter();
varcounter2=newCounter();
console.log(counter1.value());//0
console.log(counter2.value());//0
counter1.increment();
counter1.increment();
console.log(counter1.value());//2
console.log(counter2.value());//2(expected0)
counter1.decrement();
console.log(counter1.value());//1
console.log(counter2.value());//1(expected0)
PrivatememberswithclosuresWehaveseenthattheclosurefunctioncanaccessvariablesthatpersistbeyondthelexicalscopefromwhichtheywerecreated.Thesevariablesarenotpartofthefunctionprototypeorbody,buttheyarepartoftheclosurefunctioncontext.
Asthereisnowaytodirectlyaccessthecontextofaclosurefunction,thecontextvariablesandmethodscanbeusedtoemulateprivatemembers.Themainadvantageofusingclosurestoemulateprivatemembers(insteadoftheTypeScriptprivateaccessmodifier)isthatclosureswillpreventaccesstoprivatemembersatruntime.
TypeScriptavoidsemulatingprivatepropertiesatruntime.TheTypeScriptcompilerwillthrowanerroratcompilationtimeifweattempttoaccessaprivatemember.
However,TypeScriptavoidstheuseofclosurestoemulateprivatememberstoimprovetheapplicationperformance.Ifweaddorremoveanaccessmodifiertoorfromoneofourclasses,theresultingJavaScriptcodewillnotchangeatall.Thismeansthatprivatemembersofaclassbecomepublicmembersatruntime.
However,itispossibletouseclosurestoemulateprivatepropertiesatruntime.Justlikewhenweemulatedastaticvariableusingclosures,wecanonlyachievethiskindofadvancedcontroloverthebehaviorofclosuresbywritingpureJavaScript.Let'stakealookatanexample:
functionmakeCounter(){
//closurecontext
var_COUNTER=0;
functionchangeBy(val){
_COUNTER+=val;
}
functionCounter(){};
Counter.prototype.increment=function(){
changeBy(1);
};
Counter.prototype.decrement=function(){
changeBy(-1);
};
Counter.prototype.value=function(){
return_COUNTER;
};
returnnewCounter();
};
Theprecedingclassisalmostidenticaltotheclassthatwepreviouslydeclaredtodemonstratehowtoemulatestaticvariablesatruntimeusingclosures.
Thistime,anewclosurecontextiscreatedeverytimeweinvokethemakeCounterfunction,soeachnewinstanceofCounterwillrememberanindependentcontext(counterandchangeBy):
varcounter1=makeCounter();
varcounter2=makeCounter();
console.log(counter1.value());//0
console.log(counter2.value());//0
counter1.increment();
counter1.increment();
console.log(counter1.value());//2
console.log(counter2.value());//0(expected0)
counter1.decrement();
console.log(counter1.value());//1
console.log(counter2.value());//0(expected0)
Sincethecontextcannotbeaccesseddirectly,wecansaythatthevariablecounterandthechangeByfunctionareprivatemembers:
console.log(counter1.counter);//undefined
counter1.changeBy(2);//changeByisnotafunction
console.log(counter1.value());//1
SummaryInthischapter,wediscoveredhowtounderstandtheruntime,whichallowsusnotonlytoresolveruntimeissueswitheasebutalsotobeabletowritebetterTypeScriptcode.Adeepunderstandingofclosuresandprototypeswillallowyoutodevelopsomecomplexfeaturesthatitwouldhavenotbeenpossibletodevelopwithoutthisknowledge.
Inthenextchapter,wewillfocusonperformance,memorymanagement,andexceptionhandling.
Chapter6.ApplicationPerformanceInthischapter,wewilltakealookathowcanwemanageavailableresourcesinanefficientmannertoachievegreatperformance.Youwillunderstandthedifferenttypesofresource,performancefactors,performanceprofilingandautomation.
Thechapterbeginsbyintroducingsomecoreperformanceconcepts,suchaslatencyorbandwidth,andcontinuesbyshowcasinghowtomeasureandmonitorperformanceaspartoftheautomatedbuildprocess.
Aswediscussedinpreviouschapters,wecanuseTypeScripttogenerateJavaScriptcodethatcanbeexecutedinmanydifferentenvironments(webbrowsers,Node.js,mobiledevices,andsoon).Inthischapter,wewillexploreperformanceoptimization,whichismainlyapplicabletothedevelopmentofwebapplications.Thefollowingtopicswillbecoveredinthischapter:
PerformanceandresourcesAspectsofperformanceMemoryprofilingNetworkProfilingCPUandGPUprofilingPerformancetestingPerformancerecommendationsPerformanceautomation
PrerequisitesBeforewegetstarted,weneedtoinstallGoogleChromebecausewewilluseitsdevelopertoolstoperformwebperformanceanalysis.
PerformanceandresourcesBeforewegetourhandsdirtydoingsomeperformanceanalysis,monitoring,andautomation,wemustfirstspendsometimeunderstandingsomecoreconceptsandaspectsaboutperformance.
Agoodapplicationisonethathasasetofdesirablecharacteristics,whichincludesfunctionality,reliability,usability,reusability,efficiency,maintainability,andportability.Overthecourseofthisbooksofar,wehaveunderstoodalotaboutmaintainabilityandreusability.Inthischapter,wewillfocusonperformance,whichiscloselyrelatedtoreliabilityandmaintainability.
Thetermperformancereferstotheamountofusefulworkaccomplishedcomparedtothetimeandresourcesused.Aresourceisaphysical(CPU,RAM,GPU,HDD,andsoon)orvirtual(CPUtimes,RAMregions,files,andsoon)componentwithlimitedavailability.Astheavailabilityofaresourceislimited,eachresourceissharedbetweenprocesses.Whenaprocessfinishesusingaresource,itmustreleasetheresourcebeforeanyotherprocesscanuseit.Managingavailableresourcesinanefficientmannerwillhelptoreducethetimeotherprocessesspendwaitingfortheresourcestobecomeavailable.
Whenweworkonawebapplication,weneedtokeepinmindthatthefollowingresourceswillhavelimitedavailability:
CentralProcessingUnit(CPU):Thiscarriesouttheinstructionsofacomputerprogrambyperformingthebasicarithmetic,logical,control,andinput/output(I/O)operationsspecifiedbytheinstructions.GraphicsProcessorUnit(GPU):Thisisaspecializedprocessorisusedinthemanipulationandalterationofmemorytoacceleratethecreationofimagesinaframebufferintendedforoutputtoadisplay.TheGPUisusedwhenwecreateapplicationsthatusetheWebGLAPIorwhenweusesomeCSS3animations.RandomAccessMemory(RAM):Thisallowsdataitemstobereadandwritteninapproximatelythesameamountoftimeregardlessoftheorderinwhichdataitemsareaccessed.Whenwedeclareavariable,itwillbestoredinRAMmemory;whenthevariableisoutofthescope,itwillberemovedfromRAMbythegarbagecollector.HardDiskDrive(HDD)andSolidStateDrive(SSD):Bothofthesearedatastoragedevicesusedtostoreandretrieveinformation.Whendevelopingclient-sidewebapplications,wewillnothavetoworryabouttheseresourcesreallyoftenbecausetheseapplicationsdon'tusuallyextensivelyusepersistentdatastorage.However,weshouldkeepinmindthat,wheneverwestoreanobjectinapersistentmanner(cookies,localstorage,IndexedDB,andsoon),theperformanceofourapplicationwillbeaffectedbytheavailabilityoftheHDDorSSD.Networkthroughput:Thisdetermineshowmuchactualdatacanbesentperunitoftimeacrossanetwork.Thenetworkthroughputisdeterminedbyfactorssuchasthenetworklatencyorbandwidth(wewilldiscussmoreaboutthesefactorslaterinthischapter).
Note
AlltheresourcespresentedintheprecedinglistarealsolimitedwhenworkingonaNode.jsapplicationorahybridapplication.However,itisnotreallycommontoextensivelyusetheGPUwhileworkingonaNode.jsapplication,butitisapossiblescenario.
PerformancemetricsAsperformanceisinfluencedbytheavailabilityofmultipletypesofphysicalandvirtualdevice,wecanfindafewdifferentperformancemetrics(factorstomeasureperformance).Somepopularperformancemetricsincludeavailability,responsetime,processingspeed,latency,bandwidth,andscalability.Thesemeasurementmechanismsareusuallydirectlyrelatedtooneofthegeneralresources(CPU,networkthroughput,andsoon)thatwerementionedintheprevioussection.Wewillnowlookateachoftheseperformancemetricsindetail.
AvailabilityTheavailabilityofasystemisrelatedtoitsperformance,becauseifthesystemisnotavailableatsomestage,wewillperceiveitasbadperformance.Theavailabilitycanbeimprovedbyimprovingthereliability,maintainability,andtestabilityofthesystem.Ifthesystemiseasytotestandmaintain,itwillbeeasytoincreaseitsreliability.
TheresponsetimeTheresponsetimeistheamountoftimethatittakestorespondtoarequestforaservice.Aserviceheredoesnotrefertoawebservice;aservicecanbeanyunitofwork.Theresponsetimecanbedividedintothreeparts:
Waittime:Thisistheamountoftimethattherequestswillspendwaitingforotherrequeststhattookplaceearliertobecompleted.Servicetime:Thisistheamountoftimethatittakesfortheservice(unitofwork)tobecompleted.Transmissiontime:Oncetheunitofworkhasbeencompleted,theresponsewillbesentbacktotherequestor.Thetimethatittakesfortheresponsetobetransmittedisknownasthetransmissiontime.
ProcessingspeedProcessingspeed(alsoknownasclockrate)referstothefrequencyatwhichaprocessingunit(CPUorGPU)runs.Anapplicationcontainsmanyunitsofwork.Eachunitofworkiscomposedofinstructionsfortheprocessor;usually,theprocessorscanperformaninstructionineachclocktick.Sinceafewclockticksarerequiredforanoperationtobecompleted,thehighertheclockrate(processingspeed),themoreinstructionswillbecompleted.
LatencyLatencyisatermwecanapplytomanyelementsinasystem;butwhenworkingonwebapplications,wewillusethistermtorefertonetworklatency.Networklatencyindicatesanykindofdelaythatoccursindatacommunicationoverthenetwork.
Highlatencycreatesbottlenecksinthecommunicationbandwidth.Theimpactoflatencyonnetworkbandwidthcanbetemporaryorpersistent,basedontherootcauseofthedelays.Highlatencycanbecausedbyproblemsinthemedium(cablesorwirelesssignals),problemswithroutersandgateways,andanti-virus,amongotherthings.
BandwidthJustlikeinthecaseoflatency,wheneverwementionbandwidthinthischapter,wewillbereferringtothenetworkbandwidth.Thebandwidth,ordatatransferrate,istheamountofdatathatcanbecarriedfromonepointtoanotherinagiventime.Thenetworkbandwidthisusuallyexpressedinbitspersecond.
Note
Networkperformancecanbeaffectedbymanyfactors.Someofthesefactorscandegradethenetworkthroughput.Forexample,ahighpacketloss,latency,andjitterwillreducethenetworkthroughput,whileahighbandwidthwillincreaseit.
ScalabilityScalabilityistheabilityofasystemtohandleagrowingamountofwork.Asystemwithgoodscalabilitywillbeabletopasssomeperformancetests,suchasspikeorstresstesting.
Wewilldiscovermoreaboutperformancetests(suchasspikeandstress)laterinthischapter.
PerformanceanalysisPerformanceanalysis(alsoknownasperformanceprofiling)istheobservationandstudyofresourceusagebyanapplication.Wewillperformprofilinginordertoidentifyperformanceissuesinourapplications.Adifferentperformanceprofilingprocesswillbecarriedoutforeachtypeofresourceusingspecifictools.WewillnowtakealookathowwecanuseGoogleChrome'sdevelopertoolstoperformnetworkprofiling.
NetworkperformanceanalysisWearegoingtostartbyanalyzingnetworkperformance.Notsolongago,inordertobeabletoanalyzethenetworkperformanceofanapplication,wewouldhavehadtowriteasmallnetworkloggingapplicationourselves.Today,thingsaremucheasierthankstothearrivaloftheperformancetimingAPI(http://www.w3.org/TR/resource-timing/).TheperformancetimingAPIallowsustoaccessdetailednetworktimingdataforeachloadedresource.
ThefollowingdiagramillustratesthenetworktimingdatapointsthattheAPIprovides:
WecanaccesstheperformancetimingAPIviatheglobalobject:
window.performance
Theperformanceattributeintheglobalobjecthassomeproperties(memory,navigation,andtiming)andmethods(clearMarks,clearMeasures,andgetEntries).WecanusethegetEntriesfunctiontogetanarraythatcontainsthetamingdatapointsofeachrequest:
window.performance.getEntries()
EachentityinthearrayisaninstanceofPerformanceResourceTiming,whichcontainsthefollowinginformation:
{
connectEnd:1354.525000002468
connectStart:1354.525000002468
domainLookupEnd:1354.525000002468
domainLookupStart:1354.525000002468
duration:179.89400000078604
entryType:"resource"
fetchStart:1354.525000002468
initiatorType:"link"
name:"https://developer.chrome.com/static/css/out/site.css"
redirectEnd:0
redirectStart:0
requestStart:1380.8379999827594
responseEnd:1534.419000003254
responseStart:1533.6550000065472
secureConnectionStart:0
startTime:1354.525000002468
}
Unfortunately,thetimingdatapointsintheprecedingformatmaynotbereallyuseful,buttherearetoolsthatcanhelpustoanalyzethemwithease.Thefirstofthesetoolsisabrowserextensioncalledperformance-bookmarklet.ThisextensionisopensourceandisavailableforChromeandFirefox.Theextensiondownloadlinkscanbefoundathttps://github.com/micmro/performance-bookmarklet.
Inthefollowingscreenshot,youcanseeoneofthegraphsgeneratedbytheextension.ThegraphsdisplaytheperformancetypingAPIinformationinamuchbetterway,allowingustospotperformanceissueswithease:
Alternatively,youcanusethenetworkpanelintheChromedevelopertoolstoperformnetworkperformanceprofiling.Toaccessthenetworkpanel,navigatetoView,Developer,andthenDeveloperTools:
Note
WindowsuserscanaccessthedevelopertoolsbypressingtheF12key.OSXuserscanaccessitusingtheAlt+Cmd+Ishortcut.
Oncethedevelopertoolsarevisible,youcanaccesstheNetworktabbyclickingonit:
ClickingontheNetworktabwillleadyoutoascreensimilartotheoneseenhere:
Asyoucanobserve,theinformationispresentedinatableinwhicheachfileloadedisdisplayedasarow.Ontheright-handside,youcanseethatoneofthecolumnsisthetimeline.ThetimelinedisplaystheperformancetimingAPIinasimilarwaytothewaythattheperformance-bookmarkletextensiondid.
Twoimportantelementsinthetimelinearetheredandbluelines.TheselinesletusknowwhentheDOMContentLoadedeventistriggered(theblueline),followingwhichtheloadeventistriggered(theredline):
Thesetwoeventsareimportantbecausewecanexaminewhichrequestswerecompletedwhentheeventwasfiredtogetanideaofwhichcontentswereavailablefortheuserwhentheytookplace:
TheDOMContentLoadedeventisfiredwhentheenginehascompletedparsingofthemaindocumentTheloadeventisfiredwhenallthepage'sresourceshavebeenloaded
Ifyouhoveroveroneofthecellsofthetimingcolumn,youwillbeabletoseeeachoftheperformancetimingAPIdatapoints:
ItisinterestingtoknowthatthisdevelopertoolactuallyreadsthisinformationusingtheperformancetimingAPI.Let'sunderstandthemeaningofeachofthedatapoints:
PerformancetimingAPIdatapoint
Description
Stalled/Blocking
Thisisthetimetherequestspentwaitingbeforeitcouldbesent;thereisamaximumnumberofopenTCPconnectionsforanorigin.Whenthelimitisreached,somerequestswilldisplayblockingtimeratherthanstalledtime.
ProxyNegotiation Thisisthetimespentnegotiatingaconnectionwithaproxyserver.
DNSLookup ThisisthetimespentresolvingaDNSaddress;resolvingaDNSrequiresafullround-triptotheDNSserverforeachdomaininthepage.
InitialConnection/ Thisisthetimeittooktoestablishaconnection.
Connecting
SSL ThisisthetimespentestablishinganSSLconnection.
RequestSent/Sending
Thisisthetimespentissuingthenetworkrequest,typicallyafractionofamillisecond.
Waiting(TTFB)
Thisisthetimespentwaitingfortheinitialbytetobereceived—thetimetofirstbyte(TTFB).TheTTFBcanbeusedtofindoutthelatencyofaround-triptotheserverinadditiontothetimespentwaitingfortheservertodelivertheresponse.
ContentDownload/Downloading
Thisisthetimetakenfortheresponsedatatobereceived.
NetworkperformanceanduserexperienceNowthatyouknowhowwecananalyzenetworkperformance,itistimetoidentifytheperformancegoalsweshouldaimfor.Numerousstudieshaveprovedthatitisreallyimportanttokeeploadingtimesaslowaspossible.TheAkamaistudy,publishedinSeptember2009,interviewed1,048onlineshoppersandfoundthefollowing:
47percentofpeopleexpectawebpagetoloadintwosecondsorless40percentwillabandonawebpageifittakesmorethanthreesecondstoload52percentofonlineshoppersclaimthatquickpageloadsareimportantfortheirloyaltytoasite14percentwillstartshoppingatadifferentsiteifpageloadsareslow;23percentwillstopshoppingorevenwalkawayfromtheircomputer64percentofshopperswhoaredissatisfiedwiththeirsitevisitwillgosomewhereelsetoshopnexttime
Note
YoucanreadthefullAkamaistudyathttp://www.akamai.com/html/about/press/releases/2009/press_091409.html.
Fromtheprecedingstudyconclusions,weshouldassumethatnetworkperformancematters.Ourfirstpriorityshouldbetotrytoimprovetheloadingspeed.
Ifwetrytoimprovetheperformanceofasitetomakesurethatitloadsinlessthantwoseconds,wemightmakeacommonmistake:tryingtogettheonLoadeventtobetriggeredinundertwoseconds.
WhiletriggeringtheonLoadeventasearlyaspossiblewillprobablyimprovethenetworkperformanceofanapplication,itdoesn'tmeanthattheuserexperiencewillbeequallyimproved.TheonLoadeventisinsufficienttodetermineperformance.WecandemonstratethisbycomparingtheloadingperformanceoftheTwitterandAmazonwebsites.Asyoucanseeinthefollowingscreenshot,usershavetheopportunitytoengagewithAmazonmuchsoonerthanwithTwitter.EventhoughtheonLoadeventisthesameonbothsites,theuserexperienceisdrasticallydifferent:
Thisexampledemonstratesthattoimprovetheuserexperience,wemusttrytoreducetheloadingtimes,butwemustalsotrytoloadthewebcontentsinsuchawaythattheuserengagementcanbeginasearlyaspossible.Toachievethis,weshouldloadallthesecondarycontentinanasynchronousmanner.
Note
RefertoChapter3,WorkingwithFunctionstolearnmoreaboutasynchronousprogrammingwithTypeScript.
Networkperformancebestpracticesandrules
Anothereasywaytoanalyzetheperformanceofawebapplicationisbyusingabest-practicestoolfornetworkperformance,suchastheGooglePageSpeedInsightsapplicationortheYahooYSlowapplication.
GooglePageSpeedInsightscanbeusedonlineorasaGoogleChromeextension.Totrythistool,youcanvisittheonlineversionathttps://developers.google.com/speed/pagespeed/insights/andinserttheURLofthewebapplicationthatyouwanttoanalyze.Injustafewseconds,youwillgetareportliketheoneinthefollowingscreenshot:
Thereportcontainssomeeffectiverecommendationsthatwillhelpustoimprovethenetworkperformanceandoveralluserexperienceofourwebapplications.GooglePageSpeedInsightsusesthefollowingrulestoratethespeedofawebapplication:
AvoidlandingpageredirectsEnablecompressionImproveserverresponsetimeLeveragebrowsercachingMinifyresourcesOptimizeimagesOptimizeCSSDeliveryPrioritizevisiblecontentRemoverender-blockingJavaScriptUseasynchronousscripts
Whenyouusethistool,ifyouclickonthescoreofeachrules,youcanseerecommendationsanddetailsthatwillhelpyoutounderstandwhatiswrongandwhatyouneedtodotoincrease
thescoreachievedforoneparticularrule.
Ontheotherhand,YahooYSlowisavailableasabrowserextension,aNode.jsmodule,andaPhantomJSplugin,amongothers.Wecanfindtherightversionforourneedsathttp://yslow.org/.WhenwerunYSlow,itwillgenerateareportthatwillprovideuswithageneralscoreandadetailedscoreofthewebsite,liketheoneinthefollowingscreenshot:
YSlowusesthefollowingsetofrulestoratethespeedofawebapplication:
MinimizeHTTPrequestsUseacontentdeliverynetworkAvoidemptysrcorhrefAddanexpiresoracache-controlheaderGzipcomponentsPutstylesheetsatthetopPutscriptsatthebottomAvoidCSSexpressionsMakeJavaScriptandCSSexternalReduceDNSlookupsMinifyJavaScriptandCSSAvoidredirectsRemoveduplicatescriptsConfigureETagsMakeAJAXcacheable
UseGETforAJAXrequestsReducethenumberofDOMelementsPrevent404errorsReducecookiesizeUsecookie-freedomainsforcomponentsAvoidfiltersDonotscaleimagesinHTMLMakefavicon.icosmallandcacheable
Justlikebefore,whenyouusethistool,ifyouclickoneachoftherulesscoredyoucanseerecommendationsanddetailsthatwillhelpyoutounderstandwhatiswrongandwhatyouneedtodotoincreasethescoreachievedforoneparticularrule.
Note
Ifyouwanttolearnmoreaboutnetworkperformanceoptimization,pleasetakealookatthebookHighPerformanceBrowserNetworkingbyIlyaGrigorik.
GPUperformanceanalysisTherenderingofsomeelementsinwebapplicationsisacceleratedbytheuseoftheGPU.TheGPUisspecializedintheprocessingofgraphics-relatedinstructionsandcan,therefore,delivermuchbetterperformancethantheCPUwhenitcomestographics.Forexample,CSS3animationsinmodernwebbrowsersareacceleratedbytheGPU,whiletheCPUperformsJavaScriptanimations.Inthepast,theonlywaytoachievesomeanimationswasviaJavaScript.Buttoday,weshouldavoidusingthemwhenpossibleanduseCSS3insteadbecauseitwillhelpustoachievegreatwebperformance.
Inrecentyears,accesstotheGPUhasbeenaddedtobrowsersviatheWebGLAPI.ThisAPIallowswebdeveloperstocreate3DgamesandotherhighlyvisualapplicationsbyusingthepoweroftheGPU.
Framespersecond(FPS)
Wewillnotgointomuchdetailabouttheperformanceof3Dapplicationsbecauseitisareallyextensivefieldandwecouldwriteanentirebooktalkingaboutit.However,wewillmentionanimportantconceptthatcanbeappliedtoanykindofwebapplication:framespersecond(FPS)orframerate.Whenawebapplicationisdisplayedonscreen,itisdoneatanumberofimages(frames)persecond.Alowframeratecanbedetrimentaltotheoveralluserexperiencewhenperceivedbytheusers.Alotofresearchhasbeencarriedoutonthistopic,and60framespersecondseemstobetheoptimumframerateforagreatuserexperience.
Wheneverwedevelopawebapplication,weshouldtakealookattheframerateandtrytopreventitfromdroppingbelow40FPS.Thisisespeciallyimportantduringanimationsanduseractions.
Anopensourcelibrarycalledstats.jscanhelpustoseetheframeratewhiledevelopingawebapplication.ThislibrarycanbedownloadedfromGitHubathttps://github.com/mrdoob/stats.js/.Weneedtodownloadthelibraryandloaditinawebpage.Wecanthenloadthefollowingcodesnippetbyaddinganewfileorjustexecuteitinthedeveloperconsole:
varstats=newStats();
stats.setMode(1);//0:fps,1:ms
//positionoftheframeratecounter(aligntop-left)
stats.domElement.style.position='absolute';
stats.domElement.style.left='0px';
stats.domElement.style.top='0px';
document.body.appendChild(stats.domElement);
varupdate=function(){
stats.begin();
//monitoredcodegoeshere
stats.end();
requestAnimationFrame(update);
};
requestAnimationFrame(update);
Ifeverythinggoeswell,wewillbeabletoseetheframeratecounterinthetop-leftcornerofthescreen.ClickingonitwillswitchfromtheFPSviewtothemillisecond(MS)view:
TheFPSviewdisplaystheframesrenderedinthelastsecond.Thehigherthisnumberis,thebetter.TheMSviewdisplaysthemillisecondsneededtorenderaframe.Thelowerthisnumberis,thebetter.
Note
SomeadvancedWebGLapplicationsmayrequireanin-depthperformanceanalysis.Forsuchcases,ChromeprovidestheTraceEventProfilingTool.Ifyouwishtolearnmoreaboutthistool,visittheofficialpageathttps://www.chromium.org/developers/how-tos/trace-event-profiling-tool.
CPUperformanceanalysisToanalyzetheusageoftheprocessingtime,wewilltakealookattheexecutionpathofourapplication.Wewillexamineeachofthefunctionsinvokedandhowlongittakestocompletetheirexecution.WecanaccessallthisinformationbyopeningtheChromedevelopertools'Profilestab:
Inthistab,wecanselectCollectJavaScriptCPUProfileandthenclickontheStartbuttontostartrecordingtheCPUusage.BeingabletoselectwhenwewanttostartandstoprecordingtheCPUusagehelpsusselectthespecificfunctionsthatwewanttoanalyze.If,forexample,wewanttoanalyzeafunctionnamedfoo,allweneedtodoisstartrecordingtheCPUusage,invokethefoofunctionandstoprecording.Atimelineliketheoneinthefollowingscreenshotwillthenbedisplayed:
Thetimelinedisplays(horizontally)thefunctionsinvokedinthechronologicalorder.Ifthe
functioninvokesotherfunctions,thefunction'scall-stackisdisplayedvertically.Whenwehoveroveroneofthesefunctions,wewillbeabletoseeitsdetailsinthebottom-leftcornerofthetimeline:
Thedetailsincludethefollowinginformation:
Name:Thenameofthefunction.Selftime:Thetimespentonthecompletionofthecurrentinvocationofthefunction.Wewilltakeintoaccountthetimespentintheexecutionofthestatementswithinthefunction,notincludinganyfunctionsthatitcalled.Totaltime:Thetotaltimespentonthecompletionofthecurrentinvocationofthefunction.Wewilltakeintoaccountthetimespentintheexecutionofthestatementswithinthefunction,includingfunctionsthatitcalled.Aggregatedselftime:Thetimeforallinvocationsofthefunctionacrosstherecording,notincludingfunctionscalledbythisfunction.Aggregatedtotaltime:Thetimeforallinvocationsofthefunctionacrosstherecording,includingfunctionscalledbythisfunction.
Aswesawinthepreviouschapter,alltheJavaScriptcodeisexecutedinonesinglethreadatruntime.Forthisreason,whenafunctionisexecuted,nootherfunctionwillbeexecuted.Sometimes,theexecutionofafunctiontakestoolongtobecompleted,andtheapplicationbecomesunresponsive.WecanusetheCPUprofilereporttoidentifywhichfunctionsareconsumingtoomuchprocessingtime.Oncewehaveidentifiedthesefunctions,wecanrefactorandthentotrytoimprovetheapplicationresponsiveness.Somecommon
improvementsincludeusinganasynchronousexecutionflowwhenpossibleandreducingthesizeofthefunctions.
MemoryperformanceanalysisWhenwedeclareavariable,itisallocatedintheRAM.Sometimeafterthevariableisoutofthescope,itisclearedfrommemorybythegarbagecollector.Sometimes,wecangenerateascenarioinwhichavariablenevergoesoutofscope.Ifthevariablenevergoesoutofscope,itwillneverbeclearedfrommemory.Thiscaneventuallyleadtosomeseriousmemoryleakingissues.Amemoryleakisthecontinuouslossofavailablememory.
Whendealingwithmemoryleaks,wecantakeadvantageoftheGoogleChromedevelopertoolstoidentifytherootcauseoftheproblemwithease.
Thefirstthingthatwemightwonderiswhetherourapplicationhasmemoryleaksornot.Wecanfindoutbyvisitingthetimelinetabandclickingonthetop-lefticontostartrecordingtheresourceusage.Oncewestoprecording,atimelinegraphliketheoneinthefollowingscreenshotwillbedisplayed:
Inthetimeline,wecanselectMemorytoseethememoryusage(UsedJSHeap)overtime(thebluelineintheimage).Intheprecedingexample,wecanseeanotabledroptowardstheendoftheline.Thisisagoodsignbecauseitindicatesthatthemajorityoftheusedmemoryhasbeenclearedwhenthepagehasfinishedloading.
Thememoryleakscanalsotakeplaceafterloading;inthatcase,wecanusetheapplicationforawhileandobservehowthememoryusagevariesinthegraphtoidentifythecauseoftheleak.
Analternativewaytodetectmemoryleaksisbyobservingthememoryallocations.WecanaccessthisinformationbyrecordingtheheapallocationsintheProfilestab:
Thereportwillbedisplayedafterwehaverecordedsomeusageoftheresources.WecandothisbyclickingontheStartandStopbuttons.Thememoryallocationreportwilldisplayatimelineliketheoneinthefollowingscreenshot.Eachofthebluelinesisamemoryallocationthattookplaceduringtherecordedperiod.Theheightofthelinerepresentstheamountofmemoryused.Asyoucansee,thememoryisalmostclearedcompletelyaroundtheeighthsecond:
Ifweclickononeofthebluelines,wewillbeabletonavigatethroughallthevariablesthatwerestoredinmemorywhentheallocationtookplaceandexaminetheirvalues.ItisalsopossibletotakeamemorysnapshotatanygivenpointfromtheProfilestab:
Thisfeatureisparticularlyusefulwhenwearedebuggingandwewanttoseethememory
usageataparticularbreakpoint.Thememorysnapshotworkslikethedetailsviewinthepreviouslyexplainedallocationsview:
Asyoucanseeintheprecedingscreenshot,thememorysnapshotallowsustonavigatethroughallthevariablesthatwerestoredinmemorywhenthesnapshotwastakenandexaminetheirvalues.
ThegarbagecollectorPrograminglanguageswithalowlevelofabstractionhavelow-levelmemorymanagementmechanisms.Ontheotherhand,inlanguageswithahigherlevelofabstraction,suchasC#orJavaScript,thememoryisautomaticallyallocatedandfreedbyaprocessknownasthegarbagecollector.
TheJavaScriptgarbagecollectordoesagreatjobwhenitcomestomemorymanagement,butitdoesn'tmeanthatwedon'tneedtocareaboutmemorymanagement.
Independentofwhichprogramminglanguageweareworkingwith,thememorylifecycleprettymuchfollowsthesamepattern:
AllocatethememoryyouneedUsethememory(read/write)Releasetheallocatedmemorywhenitisnotneededanymore
Thegarbagecollectorwilltrytoreleasetheallocatedmemorywhenisnotneededanymoreusingavariationofanalgorithmknownasthemark-and-sweepalgorithm.Thegarbagecollectorperformsperiodicalscanstoidentifyobjectsthatareoutofthescopeandcanbefreedfromthememory.Thescanisdividedintwophases:thefirstoneisknownasmarkbecausethegarbagecollectorwillflagormarktheitemsthatcanbefreedfromthememory.Duringthesecondphase,knownassweep,thegarbagecollectorwillfreethememoryconsumedbytheitemsmarkedinthepreviousphase.
Thegarbagecollectorisusuallyabletoidentifywhenanitemcanbeclearedfromthememory;butwe,asdevelopers,musttrytoensurethatobjectsgetoutofscopewhenwedon'tneedthemanymore.Ifavariablenevergetsoutofthescope,itwillbeallocatedinmemoryforever,potentiallyleadingtoaseverememoryleakissue.
Thenumberofreferencespointingtoaniteminmemorywillpreventitfrombeingfreedfrommemory.Forthisreason,mostcasesofmemoryleakscanbefixedbyensuringthattherearenopermanentreferencestovariables.Hereareafewrulesthatcanhelpustopreventpotentialmemoryleakissues:
Remembertoclearintervalswhenyoudon'tneedthemanymore.Remembertocleareventlistenerswhenyoudon'tneedthemanymore.Rememberthatwhenyoucreateaclosure,theinnerfunctionwillrememberthecontextinwhichitwasdeclared.Thismeansthattherewillbesomeextraitemsallocatedinmemory.Rememberthatwhenusingobjectcomposition,ifcircularreferencesarecreated,youcanenduphavingsomevariablesthatwillneverbeclearedfrommemory.
PerformanceautomationInthissectionwewillunderstandhowwecanautomatemanyoftheperformanceoptimizationtasks,fromconcatenationandcompressionofcontentstotheautomationoftheperformancemonitoringandperformancetestingprocesses.
PerformanceoptimizationautomationAfteranalyzingtheperformanceofourapplication,wewillstartworkingonsomeperformanceoptimizations.Manyoftheseoptimizationsinvolvetheconcatenationandcompressionofsomeoftheapplication'scomponents.Theproblemwithcompressedcomponentsisthattheyaremorecomplicatedtodebugandmaintain.Wewillalsohavetocreateanewversionoftheconcatenatedandcompressedcontentseverytimeoneoftheoriginalcomponents(notconcatenatedandnotcompressed)changes.Astheseincludemanyhighlyrepetitivetasks,wecanusethetaskrunnerGulptoperformmanyofthesetasksforus.Wecanfindonlinepluginsthatwillallowustoconcatenateandcompresscomponents,optimizeimages,generateacachemanifest,andperformmanyotherperformanceoptimizationtasks.
Note
IfyouwouldliketolearnmoreaboutGulp,refertoChapter2,AutomatingYourDevelopmentWorkflow.
PerformancemonitoringautomationWehaveseenthatwecanautomatemanyoftheperformanceoptimizationtasksusingtheGulptaskrunner.Inasimilarway,wecanalsoautomatetheperformancemonitoringprocess.
Inordertomonitortheperformanceofanexistingapplication,wewillneedtocollectsomedatathatwillallowustocomparetheapplicationperformanceovertime.Dependingonhowwecollectthedata,wecanidentifythreedifferenttypesofperformancemonitoring:
Realusermonitoring(RUM):Thisisatypeofsolutionusedtocaptureperformancedatafromrealuservisits.ThecollectionofdataisperformedbyasmallJavaScriptcodesnippetloadedinthebrowser.Thistypeofsolutioncanhelpustocollectdataanddiscoverperformancetrendsandpatterns.Simulatedbrowsers:Thistypeofsolutionisusedtocaptureperformancedatafromsimulatedbrowsers.Thisisthemosteconomicoption,butitislimitedbecausesimulatedbrowserscannotofferasaccuratearepresentationoftherealuserexperience.Real-browsermonitoring:Thisisusedtocapturetheperformancedataofrealbrowsers.Thisinformationprovidesamoreaccuraterepresentationoftherealuserexperience,asthedataiscollectedusingexactlywhatauserwouldseeiftheyvisitedthesitewiththegivenenvironment(browser,geographiclocation,andnetworkthroughput).
InChapter2,AutomatingYourDevelopmentWorkflow,wesawhowtoconfigureaGulptaskthatusedtheKarmatestrunnertoexecuteatestsuiteinaheadlessbrowserknownasPhantomJS.
PhantomJSisasimulatedbrowserthatcanbeconfiguredtogenerateHTTPArchive(HAR)files.AHARfileusesacommonformatforrecordingHTTPtracinginformation.Thisfilecontainsavarietyofinformation,butforourpurposes,ithasarecordofeachobjectbeingloadedbyabrowser.
TherearemultiplescriptsavailableonlinethatshowcasehowtocollectthedataandreformatitusingthePhantomJSAPI.Oneoftheexamples,netsniff.js,exportsthenetworktrafficinHARformat.Thenetsniff.jsfile(andotherexamples)canbefoundathttps://github.com/ariya/phantomjs/blob/master/examples/netsniff.js.
OncewehavegeneratedtheHARfiles,wecanuseanotherapplicationtoseethecollectedperformanceinformationonavisualtimeline.ThisapplicationiscalledHARviewer,anditcanbefoundathttps://github.com/janodvarko/harviewer.
Alternatively,wecouldwriteacustomscriptorGulptasktoreadtheHARfilesandbreaktheautomatedbuildiftheapplicationperformancedoesn'tmeetourneeds.
ItisalsopossibletoconfigurePhantomJStoruntheYSlowperformanceanalysisreportandintegrateitwiththeautomatedbuild.TolearnmoreaboutPhantomJSandperformancemonitoring,refertotheofficialdocumentationathttp://phantomjs.org/network-
monitoring.html.
Note
IfyouareconsideringusingRUM,takealookattheNewRelicsolutionsathttp://newrelic.com/,orGoogleAnalyticsathttp://www.google.com/analytics/.
PerformancetestingautomationAnotherwaytoimprovetheperformanceofanapplicationistowriteautomatedperformancetests.Thesetestscanbeusedtoguaranteethatthesystemmeetsasetofperformancegoals.Therearemultipletypesofperformancetesting,butsomeofthemostcommononesincludethefollowing:
Loadtesting:Thisisthemostbasicformofperformancetesting.Wecanusealoadtesttounderstandthebehaviorofthesystemunderaspecificexpectedload(numberofconcurrentusers,numberoftransactions,andduration).Therearemultipletypesofloadtesting:
Stresstesting:Thisisnormallyusedtounderstandthemaximumcapacitylimitsofanapplication.Thiskindoftestdeterminesifanapplicationisabletohandleanextremeloadbyusinganextremeloadforanextendedperiodoftime.
Stresstestingisnotreallyusefulwhenworkingonaclient-sideapplication.However,itcanbereallyhelpfulwhenworkingonaNode.jsapplication,sinceNode.jsapplicationscanhavemanysimultaneoususers.
Soaktesting:Thisisalsoknownasendurancetesting.Thiskindoftestissimilartothestresstest,butinsteadofusinganextremeload,itusestheexpectedloadforanextendedperiodoftime.Itisacommonpracticetocollectmemoryusagedataduringthiskindoftesttodetectpotentialmemoryleaks.Thiskindoftesthelpsustodetectiftheperformancesufferssomekindofdegradationafteranextendedperiodoftime.Spiketesting:Thisisalsosimilartothestresstest,butinsteadofusinganextremetimeloadduringanextendedtimeperiod,itusessuddenintervalsofextremeandexpectedload.Thiskindoftesthelpsustodetermineifanapplicationisabletohandledramaticchangesinload.Configurationtesting:Thisisusedtodeterminetheeffectsofconfigurationchangesontheperformanceandbehaviorofanapplication.Acommonexamplewouldbeexperimentingwithdifferentmethodsofloadbalancing.
Note
ThiskindoftestcanalsobeautomatedbyusingtoolssuchasJMeter(http://jmeter.apache.org)orLocust(http://locust.io).
ExceptionhandlingUnderstandinghowtousetheavailableresourcesinanefficientmannerwillhelpustocreatebetterapplications.Inasimilarmanner,understandinghowtohandleruntimeerrorswillhelpustoimprovetheoverallqualityofourapplications.ExceptionhandlinginTypeScriptinvolvesthreemainlanguageelements.
TheErrorclassWhenaruntimeerrortakesplace,aninstanceoftheErrorclassisthrown:
thrownewError();
Wecancreatecustomerrorsinacoupleofdifferentways.TheeasiestwaytoachieveitisbypassingastringasargumenttotheErrorclassconstructor:
ThrownewError("Mybasiccustomerror");
Ifweneedmorecustomizableandadvancedcontrolovercustomexceptions,wecanuseinheritancetoachieveit:
moduleCustomException{
exportdeclareclassError{
publicname:string;
publicmessage:string;
publicstack:string;
constructor(message?:string);
}
exportclassExceptionextendsError{
constructor(publicmessage:string){
super(message);
this.name='Exception';
this.message=message;
this.stack=(<any>newError()).stack;
}
toString(){
returnthis.name+':'+this.message;
}
}
}
Intheprecedingcodesnippet,wehavedeclaredaclassnamedError.ThisclassisavailableatruntimebutisnotdeclaredbyTypeScript,sowewillhavetodoitourselves.Then,wehavecreatedanExceptionclass,whichinheritsfromtheErrorclass.
Finally,wecancreatecustomErrorbyinheritingfromourExceptionclass:
classCustomErrorextendsCustomException.Exception{
//...
}
Thetry…catchstatementsandthrowstatementsAcatchclausecontainsstatementsthatspecifywhattodoifanexceptionisthrowninthetryblock.Weshouldperformsomeoperationsinthetryblock,andiftheyfail,theprogramexecutionflowwillmovefromthetryblocktothecatchblock.
Additionally,thereisanoptionalblockknownasfinally,whichisexecutedafterboththetryandcatch(iftherewasanexceptionincatch)blocks:
try{
//codethatwewanttowork
thrownewError("Oops!");
}
catch(e){
//codeexecutedifexpectedtoworkfails
console.log(e);
}
finally{
//codeexecutedalwaysaftertryortryandcatch(whenerrors)
console.log("finally!");
}
Itisalsoimportanttomentionthatinthemajorityofprogramminglanguages,includingTypeScript,throwingandcatchingexceptionsisanexpensiveoperationintermsofresourceconsumption.Weshouldusethesestatementsifweneedthem,butsometimesitisnecessarytoavoidthembecausetheycanpotentiallynegativelyaffecttheperformanceofourapplications.Therefore,weshouldkeepinmindthatitisagoodideatoavoidtheuseoftry…catchandthrowstatementsinperformance-criticalfunctionsandloops.
SummaryInthischapter,wesawwhatperformanceisandhowtheavailabilityofresourcescaninfluenceit.WealsolookedathowtousesometoolstoanalyzethewayaTypeScriptapplicationusesavailableresources.Thesetoolsallowustospotsomepossibleissues,suchasalowframerate,memoryleaks,andhighloadingtimes.Wehavealsodiscoveredthatwecanautomatemanykindsofperformanceoptimizationtask,aswellastheperformancemonitoringandtestingprocesses.
Inthefollowingchapter,wewillseehowwecanautomatethetestingprocessofourTypeScriptapplicationstoachievegreatapplicationmaintainabilityandreliability.
Chapter7.ApplicationTestingInthischapter,wearegoingtotakealookathowtowriteunittestsforTypeScriptapplications.Wewillseehowtousetoolsandframeworkstofacilitatethetestingprocessofourapplications.
Thecontentsofthischaptercoverthefollowingtopics:
SettingupatestinfrastructureTestingplanningandmethodologiesHowtoworkwithMocha,Chai,andSinon.JSHowtoworkwithtestassertions,specs,andsuitesTestspiesTeststubsTestingonmultipleenvironmentsHowtoworkwithKarmaandPhantomJSEnd-to-endtestingGeneratingtestcoveragereports
Wewillgetstartedbyinstallingsomenecessarythird-partysoftwaredependencies.
SoftwaretestingglossaryAcrossthischapter,wewillusesomeconceptsthatmaynotbefamiliartothosereaderswithoutprevioussoftwaretestingexperience.Let'stakeaquicklookatsomeofthemostpopulartestingconceptsbeforewegetstarted.
AssertionsAnassertionisaconditionthatmustbetestedtoconfirmthatacertainpieceofcodebehavesasexpectedor,inotherwords,toconfirmconformancetoarequirement.
Let'simaginethatweareworkingaspartofoneoftheGoogleChromedevelopmentteamandwehavetoimplementtheJavaScriptMathobject.Ifweareworkingonthepowmethod,therequirementcouldbesomethinglikethefollowing:
"TheMath.pow(base,exponent)functionshouldreturnthebase(thebasenumber)totheexponent(theexponentusedtoraisethebasepower—thatis,base^exponent)."
Withthisinformation,wecouldcreatethefollowingimplementation:
classMath1{
publicstaticpow(base:number,exponent:number){
varresult=base;
for(vari=1;i<exponent;i++){
result=result*base;
}
returnresult;
}
}
Toensurethatthemethodiscorrectlyimplemented,wemusttestitconformswiththerequirement.Ifweanalyzetherequirementsclosely,weshouldidentifyatleasttwonecessaryassertions.
Thefunctionshouldreturnthebasetotheexponent:
varactual=Math1.pow(3,5);
varexpected=243;
varasertion1=(Math1.pow(base1,exponent1)===expected1);
Theexponentisnotusedasthebase(orthebaseisnotusedastheexponent):
varactual=Math1.pow(5,3);
varexpected=125;
varasertion2=(Math1.pow(base2,exponent2)===expected2);
Ifbothassertionsarevalid,thenourcodeadherestotherequirements,andweknowthatitwillworkasexpected:
varisValidCode=(asertion1&&asertion2);
console.log(isValidCode);
SpecsSpecisatermusedbysoftwaredevelopmentengineerstorefertotestspecifications.Atestspecification(nottobeconfusedwithatestplan)isadetailedlistofallthescenariosthatshouldbetested,howtheyshouldbetested,andsoon.Wewillseelaterinthischapterhowwecanuseatestingframeworktodefineatestspec.
TestcasesAtestcaseisasetofconditionsusedtodeterminewhetheroneofthefeaturesofanapplicationisworkingasitwasoriginallyestablishedtowork.Wemightwonderwhatthedifferencebetweenatestassertionandatestcaseis.Whileatestassertionisasinglecondition,atestcaseisasetofconditions.Wewillseelaterinthischapterhowwecanuseatestingframeworktodefinetestcases.
SuitesAsuiteisacollectionoftestcases.Whileatestcaseshouldfocusononlyonetestscenario,atestsuitecancontaintestcasesformanytestscenarios.
SpiesSpiesareafeatureprovidedbysometestingframeworks.Theyallowustowrapamethodandrecorditsusage(input,output,numberoftimesinvoked).Whenwewrapafunctionwithaspy,theunderlyingmethod'sfunctionalitydoesnotchange.
DummiesAdummyobjectisanobjectthatispassedaroundduringtheexecutionofatestbutisneveractuallyused.
StubsAstubisafeatureprovidedbysometestingframeworks.Stubsalsoallowustowrapamethodtoobserveitsusage.Unlikespies,whenwewrapafunctionwithastub,theunderlyingmethod'sfunctionalityisreplacedwithanewbehavior.
MocksMocksareoftenconfusedwithstubs.MartinFowleroncewrotethefollowinginanarticletitledMocksAren'tStubs:
InparticularIseethemoften(mocks)confusedwithstubs-acommonhelpertotestingenvironments.Iunderstandthisconfusion-Isawthemassimilarforawhiletoo,butconversationswiththemockdevelopershavesteadilyallowedalittlemockunderstandingtopenetratemytortoiseshellcranium.Thisdifferenceisactuallytwoseparatedifferences.Ontheonehandthereisadifferenceinhowtestresultsareverified:adistinctionbetweenstateverificationandbehaviorverification.Ontheotherhandisawholedifferentphilosophytothewaytestinganddesignplaytogether,whichItermhereastheclassicalandmockiststylesofTestDrivenDevelopment.
Bothmocksandstubsprovidesomesortofinputtothetestcase;but,despitetheirsimilarities,theflowofinformationfromeachisverydifferent:
StubsprovideinputfortheapplicationundertestsothatthetestcanbeperformedonsomethingelseMocksprovideinputtothetesttodecidewhetherthetestshouldpassorfail
Thedifferencebetweenmocksandstubswillbecomecleareraswemovetowardstheendofthischapter.
TestcoverageThetermtestcoveragereferstoaunitofmeasurement,whichisusedtoillustratethenumberofportionsofcodeinanapplicationthathavebeentestedviaautomatedtests.Testcoveragecanbeobtainedbyautomaticallygeneratingtestcoveragereports.Towardstheendofthechapter,wewillseehowtocreatesuchreportsusingatoolcalledIstanbul(http://gotwarlost.github.io/istanbul/).
PrerequisitesThroughoutthischapter,wewillusesomethird-partytools,includingsomeframeworksandautomationtools.Wewillstartbylookingateachtoolindetail.Beforewegetstarted,weneedtousenpmtocreateapackage.jsonfileinthefolderthatwearegoingtousetoimplementtheexamplesinthischapter.
Let'screateanewfoldernamedappandrunthenpminitcommandinsideittogenerateanewpackage.jsonfile:
npminit
Note
RefertoChapter2,AutomatingYourDevelopmentWorkflowforadditionalhelponnpm.
GulpWewillusetheGulptaskrunnertorunsometasksnecessarytoexecuteourtests.WecaninstallGulpusingnpm:
npminstallgulp-g
Note
Ifyouarenotfamiliarwithtaskrunnersandcontinuousintegrationbuildservers,takealookatChapter2,AutomatingYourDevelopmentWorkflow.
KarmaKarmaisatestrunner.WewilluseKarmatoautomaticallyexecuteourtests.Thisisusefulbecausesometimestheexecutionofthetestwillnotbestartedbyoneofthemembersofoursoftwaredevelopmentteam.Instead,itwillbetriggeredbyacontinuousintegrationbuildserver(usuallyviaataskrunner).
Karmacanbeusedwithmultipletestingframeworks,thankstotheinstallationofplugins.Let'sinstallKarmausingthefollowingcommand:
npminstall--save-devkarma
WewillalsoinstallanotherKarmapluginthatfacilitatesthecreationoftestcoveragereports:
npminstall--save-devkarma-coverage
IstanbulIstanbulisatoolthatidentifieswhichlinesofourapplicationareprocessedduringtheexecutionoftheautomatedtest.Itcangeneratereportsknownastestcoveragereports.Thesereportscanhelpustogetanideaoftheleveloftestingofaprojectbecausetheyshowwhichlinesofcodewerenotexecutedandapercentagevaluethatrepresentsthefractionoftheapplicationthathasbeentested.Itisrecommendedthatatestcoveragevalueofatleast75percentoftheoverallapplicationshouldbeachieved,whilemanyopensourceprojectstargetatestcoverageof100percent.
MochaMochaisapopularJavaScripttestinglibrarythatfacilitatesthecreationoftestsuites,testcases,andtestspecs.MochacanbeusedtotestTypeScriptinthefrontendandbackend,identifyperformanceissues,andgeneratedifferenttypesoftestreports,amongmanyotherfeatures.
Let'sinstallMochaandtheKarma-Mochapluginusingthefollowingcommand:
npminstall--save-devmochakarma-mocha
ChaiChaiisatestassertionlibrarythatsupportstest-drivendevelopment(TDD)andbehavior-drivendevelopment(BDD)teststyles.
Note
WewillseemoreaboutTDDandBDDlaterinthischapter.
ThemaingoalofChaiistoreducetheamountofworknecessarytocreateatestassertionandmakethetestmorereadable.
WecaninstallChaiandtheKarma-Chaipluginusingthefollowingcommand:
npminstall--save-devchaikarma-chai
Sinon.JSSinon.JSisanisolationframeworkthatprovidesuswithasetofAPIs(testspies,stubs,andmocks)thatcanhelpustotestacomponentinisolation.Testingisolatedsoftwarecomponentsisdifficultbecausethereisahighlevelofcouplingbetweenthecomponents.AmockinglibrarysuchasSinon.JScanhelpusisolatethecomponentsinordertotestindividualfeatures.
WecaninstallSinon.JSandtheKarma-Sinonpluginusingthefollowingcommand:
npminstall--save-devsinonkarma-sinon
TypedefinitionsTobeabletoworkwiththird-partylibrariesinJavaScriptwithagoodsupport,weneedtoimportthetypedefinitionsofeachlibrary.Wewillusethetsdpackagemanagertoinstallthenecessarytypedefinitions:
tsdinstallmocha--save
tsdinstallchai--save
tsdinstallsinon--save
tsdinstalljquery--save
Note
RefertoChapter2,AutomatingYourDevelopmentWorkflowforadditionalhelpontsd.
PhantomJSPhantomJSisaheadlessbrowser.WecanusePhantomJStorunourtestsinabrowserwithouthavingtoactuallyopenabrowser.Beingabletodothisisusefulforafewreasons;themainoneisthatPhantomJScanbeexecutedviaacommandinterface,anditisreallyeasytointegratewithtaskrunnersandcontinuousintegrationservers.Thesecondreasonisthatnothavingtoopenabrowserpotentiallyreducesthetimerequiredtocompletetheexecutionofthetestssuites.
WeneedtoinstalltheKarmapluginthatwillrunthetestinPhantomJS:
npminstall--save-devphantomjs
npminstall--save-devkarma-phantomjs-launcher
SeleniumandNightwatch.jsSeleniumisatestrunnerbutitwasespeciallydesignedtorunaparticulartypeoftestknownasanend-to-end(E2E)test.
Note
WewilllearnmoreaboutE2Etestinglateronthischapter,sowedon'tneedtoworrytoomuchaboutthistopicfornow.
Thoughwewillseehowtouseseleniumtowardstheendofthechapter,wecaninstallitnow.WewillnotworkwithSeleniumdirectlybecausewearegoingtouseanothertool(knownasNightwatch.js)forE2Etesting,whichwillautomaticallyrunSeleniumforus.
Nightwatch.jsisanautomatedtestingframework,writteninNode.jsforwebapplicationsandwebsites,whichusestheSeleniumWebDriverAPI.Itisacompletebrowserautomation(end-to-end)solution.
WecaninstallNightwatch.jsandSeleniumbyexecutingthefollowingcommands:
npminstall--save-devgulp-nightwatch
npminstallselenium-standalone-g
selenium-standaloneinstall
Note
TheSeleniumstandalonerequirestheJavabinariestobeinstalledinthedevelopmentenvironmentandaccessiblethroughthe$PATHvariable.RefertotheofficialJavadocumentationathttps://www.java.com/en/download/help/index_installing.xmltolearnmoreabouttheJavainstallation.
TestingplanningandmethodologiesWhenitcomestosoftwaredevelopment,weusuallyhavemanychoices.Everytimewehavetodevelopanewapplication,wecanchoosethetypeofdatabase,thearchitecture,andframeworksthatwewilluse.Notallourchoicesareabouttechnologies.Forexample,wecanalsochooseasoftwaredevelopmentmethodologysuchasextremeprogrammingorscrum.Whenitcomestotesting,therearetwomajorstylesormethodologies:test-drivendevelopment(TDD)andbehavior-drivendevelopment(BDD).
Test-drivendevelopmentTest-drivendevelopmentisatestingmethodologythatfocusesonencouragingdeveloperstowritetestsbeforetheywriteapplicationcode.Usually,theprocessofwritingcodeinTDDconsistsofthefollowingbasicsteps:
1. Writeatestthatfails.2. Runthetestandensurethatitfails(thereisnocodeatthispointsoitshouldfail).3. Writethecodetomakethetestpass.4. Runthetestandensurethatitpasses.5. Runalltheotherteststoensurethatnootherpartsoftheapplicationbreak.6. Repeattheprocess.
ThedifferencebetweenusingTDDornotisreallyamindset.Manydevelopersdon'tlikewritingtests,sochancesarethat'ifweleavetheirimplementationasthelasttaskinthedevelopmentprocess,thetestswillnotimplementedortheapplicationwilljustbepartiallytested.
TDDisrecommendedbecauseiteffectivelyhelpsyouandyourteamtoincreasethetestcoverageofyourapplicationsand,therefore,significantlyreducethenumberofpotentialissues.
Behavior-drivendevelopment(BDD)Behavior-drivendevelopmentappearedafterTDDwiththemissionofbeingarefinedversionofTDD.BDDfocusesonthewaytestsaredescribed(specs)andstatesthatthetestsshouldfocusontheapplicationrequirementsandnotthetestrequirements.Ideally,thiswillencouragedeveloperstothinklessabouttheteststhemselvesandmoreabouttheapplicationasawhole.
Note
TheoriginalarticleinwhichtheBDDprincipleswereintroducedforthefirsttimebyDanNorthisavailableonlineathttp://dannorth.net/introducing-bdd/.
Aswehavealreadyseen,MochaandChaiprovideAPIsfortheTDDandBDDapproaches.Laterinthischapter,wewillfurtherexplorethesetwoapproaches.
RecommendingoneofthesemethodologiesisnottrivialbecauseTDDandBDDarebothreallygoodtestingmethodologies.However,BDDwasdevelopedafterTDDwiththeobjectivetoimproveit,sowecanarguethatBDDhassomeadditionaladvantagesoverTDD.InBDD,thedescriptionofatestfocusesonwhattheapplicationshoulddoandnotwhatthetestcodeistesting.Thiscanhelpthedeveloperstoidentifyteststhatreflectthebehaviordesiredbythecustomer.BDDtestsarethenusedtodocumenttherequirementsofasysteminawaythatcanbeunderstoodandvalidatedbyboththedeveloperandthecustomer.Ontheotherhand,TDDtestscannotbeunderstoodwitheasebythecustomer.
TestsplansandtesttypesThetermtestplanissometimesincorrectlyusedtorefertoatestspecification.Whiletestsspecificationsdefinethescenariosthatwillbetestedandhowtheywillbetested,thetestplanisacollectionofallthetestspecsforagivenarea.
Itisrecommendedtocreateanactualplanningdocumentbecauseatestplancaninvolvemanyprocesses,documents,andpractices.Oneofthemaingoalsofatestplanistoidentifyanddefinewhatkindoftestisadequateforaparticularcomponentorsetofcomponentsinanapplication.
Followingarethemostcommonlyusedtesttypes:
Unittests:Theseareusedtotestanisolatedcomponent.Ifthecomponentisnotisolated—orinotherwords,thecomponenthassomedependencies—wewillhavetousesometoolsandpracticessuchasmocksordependencyinjectiontotrytoisolateitasmuchaswecanduringthetest.
Ifitisnotpossibletomanipulatethecomponentdependencies,wewillusespiestofacilitatethecreationoftheunittests.
Ourmaingoalshouldbetoachievethetotalisolationofacomponentwhenitistested.Aunittestshouldalsobefast,andweshouldtrytoavoidinput/output,networkusage,andanyotheroperationthatcouldpotentiallyaffectthespeedofthetest.Partialintegrationtestsandfullintegrationtests:Theseareusedtotestasetofcomponents(partialintegrationtest)ortheentireapplicationasawhole(fullintegrationtest).Inintegration,wewillnormallyuseknowntestdatatofeedthebackendwithinformationthatwillbedisplayedinthefrontend.Wewillthenassertthatthedisplayedinformationiscorrect.Regressiontests:Thesetestsareusedtoverifythatanissuehasbeenfixed.IfweareusingTDDorBDD,wheneverweencounteranissueweshouldcreateaunittestthatreproducestheissue,andthenchangethecode.Bydoingthis,wewillbeabletorunattemptstoreproducepastissuesandensurethateverythingisstillworking.Performance/Loadtests:Thesetestsverifyiftheapplicationmeetsourperformanceexpectations.Wecanuseperformanceteststoverifythatourapplicationwillbeabletohandlemanyconcurrentusersoractivityspikes.Tolearnmoreaboutthistypeoftest,takealookatthepreviouschapter:Chapter6,ApplicationPerformance.End-to-end(E2E)tests:Thesetestsarenotreallydifferentfromfullintegrationtests.ThemaindifferenceisthatinanE2Etestingsession,wewilltrytoemulateanenvironmentalmostidenticaltotherealuserenvironment.WewilluseNightwatch.jsandSeleniumforthispurpose.Useracceptancetests(UAT):Theseareusedsothatthesystemmeetsalltherequirementsoftheenduser.
SettingupatestinfrastructureAswesawpreviouslyinthischapterwhenwetalkedaboutunittests,usually,testingrequiresbeingabletoisolatetheindividualsoftwarecomponentofourapplications.
Inordertobeabletoisolatethecomponentsofourapplication,wewillneedtoadheretosomeprinciples(suchasthedependencyinversionprinciple)thatwillhelpustoincreasethelevelofdecouplingbetweenthecomponents.
WewillnowconfigureatestingenvironmentusingGulpandKarmaandwritesomeautomatedtestusingMochaandChai.Bytheendofthischapter,wewillknowhowwritingunittestscanhelpustoincreasethelevelofdecouplingandisolationbetweenthecomponentsofanapplication,andhowtheycanleadustothedevelopmentofgreatapplications,especiallywhenitcomestomaintainabilityandreliability.
Let'sgetstartedbycreatingthefolderstructureofanewapplication.Wewillcreatetwofoldersinsidetheappfolderthatwecreatedatthebeginningofthischapter.
Let'snamethefirstfoldersourceandthesecondfoldertest.Here,wecanseehowourdirectorytreeshouldlookbytheendofthechapter:
├──app
├──gulpfile.js
├──index.html
├──karma.conf.js
├──nightwatch.json
├──package.json
├──source
│├──calculator_widget.ts
│├──demos.ts
│├──interfaces.d.ts
│├──math_demo.ts
├──style
│└──demo.css
├──test
│├──bdd.test.ts
│├──e2e.test.ts
│├──tdd.test.ts
├──tsd.json
└──typings
Wearegoingtodevelopareallysmallapplicationtobeabletowriteaunittest.Wearegoingtowriteaunittestandanend-to-endtest.
Note
Thesourcecodeoftheentiredemocanbefoundinthecompanioncodesamples.
Oncewehavecompletedourapplication,wewillbeabletoopenitinabrowser,wherewe
shouldseeaformliketheoneinthefollowingscreenshot.Thisformallowsustofindtheresultofanumber(base)tothepowerofanother(exponent).
BuildingtheapplicationwithGulpWewillgetstartedbycreatinganewgulpfile.jsfileaswedidinChapter2,AutomatingYourDevelopmentWorkflow.Thefirstthingthatwearegoingtodoisimportallthenecessarynodemodules:
vargulp=require("gulp"),
browserify=require("browserify"),
source=require("vinyl-source-stream"),
buffer=require("vinyl-buffer"),
run=require("gulp-run"),
nightwatch=require('gulp-nightwatch'),
tslint=require("gulp-tslint"),
tsc=require("gulp-typescript"),
browserSync=require('browser-sync'),
karma=require("karma").server,
uglify=require("gulp-uglify"),
docco=require("gulp-docco"),
runSequence=require("run-sequence"),
header=require("gulp-header"),
pkg=require(__dirname+"/package.json");
Note
Rememberthatweneedtoinstallallnecessarypackagesbyusingthenpmpackagemanager.Wecantakealookatthepackage.jsonfiletoseeallthedependenciesandtheirrespectiveversions.
ThesecondthingthatwearegoingtodoistocreatesometaskstocompileourTypeScriptcode.Here,weshouldnoticethatwearegoingcompiletheapplicationcodeintothe/build/sourcefolderandtheapplicationtestsintothe/build/testfolder:
vartsProject=tsc.createProject({
removeComments:false,
noImplicitAny:false,
target:"ES5",
module:"commonjs",
declarationFiles:false
});
gulp.task("build-source",function(){
returngulp.src(__dirname+"/source/*.ts")
.pipe(tsc(tsProject))
.pipe(gulp.dest(__dirname+"/build/source/"));
});
ThepreviousGulptaskcompilestheTypeScriptfilesunderthesourcefolderintoJavaScriptfilesthatwillbestoredininsidethebuild/sourcefolder.Weshouldbeabletorunthetaskbyexecutingthefollowingcommand:
gulpbuild-source
Note
Theprecedingcommandwillfailifnosourcefilesareavailable.Youcancopyprojectsourcefilesfromthecompanionsourcecodeorcontinuereadingthischapterandcreatethefilesasweprogress.
Wewillalsodeclareasecondtasktocompileourunittests,buttheoutputwillbestoredunderthebuild/testfolder:
vartsTestProject=tsc.createProject({
removeComments:false,
noImplicitAny:false,
target:"ES5",
module:"commonjs",
declarationFiles:false
});
gulp.task("build-test",function(){
returngulp.src(__dirname+"/test/*.test.ts")
.pipe(tsc(tsTestProject))
.pipe(gulp.dest(__dirname+"/build/test/"));
});
WeshouldbeabletorunthisnewtaskusingGulpbyusingthefollowingcommand:
gulpbuild-test
OncetheJavaScriptisunderthebuildfolder,weneedtobundletheexternalmodules(asweused{module:"commonjs"}intheprecedingcompilersettings)intobundledlibrariesthatcanbeexecutedinawebbrowser.
Browserifyneedsauniqueentrypointforeachlibrary.Forthisreason,wearegoingtocreatethreetasks—oneforeachbundledlibrary.
Wewillcreateatasktobundletheapplicationitself:
gulp.task("bundle-source",function(){
varb=browserify({
standalone:'demos',
entries:__dirname+"/build/source/demos.js",
debug:true
});
returnb.bundle()
.pipe(source("demos.js"))
.pipe(buffer())
.pipe(gulp.dest(__dirname+"/bundled/source/"));
});
JustlikewedidwiththepreviousGulptasks,wecaninvokethenewtaskbyusingthefollowingcommand:
gulpbundle-source
Wewillalsocreateanothertasktobundlealltheunittestsinourapplicationintoasinglebundledsuiteoftests:
gulp.task("bundle-test",function(){
varb=browserify({
standalone:'test',
entries:__dirname+"/build/test/bdd.test.js",
debug:true
});
returnb.bundle()
.pipe(source("bdd.test.js"))
.pipe(buffer())
.pipe(gulp.dest(__dirname+"/bundled/test/"));
});
Note
ThecompanioncodehastestsusingboththeTDDandBDDstylesintwoindependentfilesnamedtdd.test.tsandbdd.test.ts.However,intheexamplesinthischapter,wewillonlyfocusontheBDDstyle.
Wecaninvokethenewtaskbyusingthefollowingcommand:
gulpbundle-test
Finally,wewillcreateanothertasktobundlealltheE2EtestsintheapplicationintoasinglebundledE2Etestsuite:
gulp.task("bundle-e2e-test",function(){
varb=browserify({
standalone:'test',
entries:__dirname+"/build/test/e2e.test.js",
debug:true
});
returnb.bundle()
.pipe(source("e2e.test.js"))
.pipe(buffer())
.pipe(gulp.dest(__dirname+"/bundled/e2e-test/"));
});
Wecaninvokethenewtaskbyusingthefollowingcommand:
gulpbundle-e2e-test
RunningtheunittestwithKarmaWehavealreadycoveredthebasicsofKarmainChapter2,AutomatingYourDevelopmentWorkflow.WearegoingtocreateatasktoexecuteKarma:
gulp.task("run-unit-test",function(cb){
karma.start({
configFile:__dirname+"/karma.conf.js",
singleRun:true
},cb);
});
TheKarmataskconfigurationisreallysimplebecausethemajorityoftheconfigurationislocatedinthekarma.conf.jsfile,whichisincludedinthecompanioncode.Let'stakealookattheconfigurationfile:
module.exports=function(config){
'usestrict';
config.set({
basePath:'',
frameworks:['mocha','chai','sinon'],
browsers:['PhantomJS'],
reporters:['progress','coverage'],
coverageReporter:{
type:'lcov',
dir:__dirname+'/coverage/'
},
plugins:[
'karma-coverage',
'karma-mocha',
'karma-chai',
'karma-sinon',
'karma-phantomjs-launcher'
],
preprocessors:{
'**/bundled/test/bdd.test.js':'coverage'
},
files:[
{
pattern:"/bundled/test/bdd.test.js",
included:true
},
{
pattern:"/node_modules/jquery/dist/jquery.min.js",
included:true
},
{
pattern:"/node_modules/bootstrap/dist/js/bootstrap.min.js",
included:true
}
],
client:{
mocha:{
ui:"bdd"
}
},
port:9876,
colors:true,
autoWatch:false,
logLevel:config.DEBUG
});
};
Ifwetakealookattheconfigurationfile,wewillseethatwehaveconfiguredthepathwherethetestsarelocatedandthebrowserthatwewanttousetorunthetest(PhantomJS).Declaringwhatbrowserwewanttouseisnotenough;wealsoneedtoinstallapluginsoKarmacanlaunchthatbrowser.
SincewearegoingtowritetestusingMocha,Chai,andSinon.JS,wehaveloadedthepluginstointegrateKarmawitheachoftheseframeworks.Therearemanyotherpopulartestingframeworks,andthemajorityofthemarecompatiblewithKarmaviatheuseofplugins.
Anotherinterestingsettingintheprecedingconfigurationfileisthecliententry.WeuseittoconfiguretheoptionsofMochaandindicatethatwearegoingtouseaBDDtestingstyle.
WhenKarmaexecutestheMochaunittests,itgeneratesanHTMLpageinternallyandaddsalltherequiredfilesindicatedinthefilesfieldaswellassomefilesindicatedbythepluginsfield.Fortheprecedingexample,KarmawillgenerateanHTMLpagethatwillcontainreference(usingthe<script>tags)toMocha,Chai,andSinon.JS(indicatedbytheplugins)aswellasjQuery,Bootstrap,andthebdd.test.jsfile(indicatedbythefilesfield).
Note
Thecompanionsourcecodeincludesthepackage.jsonfile.Wecanusethisfiletorunthenpminstallcommandanddownloadallthethird-partydependencies(includingjQueryandBootstrap).
Itisimportanttounderstandthatonlyfilesloadedviathefilesfieldwillbeavailableduringthetestexecution,andthatallthefileswillbeloadedusingascripttag.Sometimes,wemayencounterissuesrelatedtomissingfilesorparsingerrors(whenanon-JavaScriptfileisloadedusingascripttag).Wecanhaveabettercontroloverthefileinclusionprocessusingthesettingspattern,included,served,andwatched:
Settings Description
pattern Thepatterntousetomatchfiles.
IfautoWatchistrue,allfilesthathavesetwatchedtotruewillbewatchedfor
included changes.
served ShouldthefilesbeservedbyKarma'swebserver?
watched Shouldthefilesbeincludedinthebrowserusingthe<script>tag?Wewillusefalseifwewanttoloadthemmanually(forexample,usingRequireJS).
Thekarma.conf.jsfilealsocontainssomesettingstogeneratetestcoveragereports,butwewillskipthosefornowandfocusonthemtowardstheendofthechapter.
Note
Rememberthatyoucanfindallthedetailsabouteachfieldinthekarma.conf.jsfileathttp://karma-runner.github.io/0.8/config/configuration-file.html.
RunningE2EtestswithSeleniumandNightwatch.jsKarma(incombinationwithMocha,Chai,andSinon.JS)isagreattoolwhenitcomestowritingandexecutingunittestsandpartialintegrationtests.However,KarmaisnotthebesttoolwhenitcomestowritingE2Etests.Forthisreason,wewillwriteacollectionofE2Eteststhatwillbewrittenandexecutedusingaseparatesetoftools:SeleniumandNightwatch.js.
ToconfigureNightwatch.js,wewillstartbycreatinganewGulptaskthatwillbeinchargeoftheexecutionoftheE2Etests.WeonlyneedtospecifythelocationofanexternalconfigurationfilenamedNightwatch.js:
gulp.task('run-e2e-test',function(){
returngulp.src('')
.pipe(nightwatch({
configFile:__dirname+'/nightwatch.json'
}));
});
Note
WearegoingtofocusonNightwatch.jsbecauseitisdesignedtoworkwiththemajorityofframeworks;butifyouareworkingwithAngularJS,IwouldrecommendyoutotakealookatProtractor.ProtractorisagreatE2EtestingframeworkthathasahighlevelofintegrationwithAngularJS.
Thenightwatch.jsfilecontainstheentirerequiredconfigurationnecessarytoexecuteourE2Etests.WeneedtospecifythelocationoftheE2EtestsuitesandthebasicSeleniumconfiguration.
WeneedtothinkthatSeleniumismoreorlesslikeKarma;itisatoolthatcanexecuteaunittestinabrowser.ThemaindifferenceisthatSeleniumallowsustowritetestsinawaythatsimulatesmuchbetterhowarealuserwouldbehave.ItisimportanttounderstandthatNightwatch.jsisnotthetooldirectlyinchargeofexecutionofthetest.Nightwatch.jsisaframeworkthathelpstowriteE2EtestsandcancommunicatewithSeleniumtoexecutethetests.
Inthiscase,wewilltellNightwatch.jsnottorunSeleniumforususingthestart_processentryinthenightwatch.jsonconfigurationfile.Thenightwatch.jsonfileshouldlookasfollows:
{
"src_folders":["bundled/e2e-test/"],
"output_folder":"reports",
"selenium":{
"start_process":false
},
"test_settings":{
"default":{
"silent":true,
"screenshots":{
"enabled":true,
"path":"screenshots"
},
"desiredCapabilities":{
"browserName":"chrome",
"javascriptEnabled":true,
"acceptSslCerts":true
}
},
"phantomjs":{
"desiredCapabilities":{
"browserName":"phantomjs",
"javascriptEnabled":true,
"acceptSslCerts":true,
"phantomjs.binary.path":"./node_modules/phantomjs/bin/phantomjs"
}
},
"chrome":{
"desiredCapabilities":{
"browserName":"chrome",
"javascriptEnabled":true,
"acceptSslCerts":true
}
}
}
}
WewillrunSeleniummanuallyusingtheselenium-standalonenpmpackage(wecanchecktheprerequisitessectionforinstallationdetails):
selenium-standalonestart
BesidesconfiguringSelenium,weneedtoconfigurewhichwebbrowserswearegoingtouseduringtheexecutionofourE2Etestsandtorunthewebapplicationonawebserver.
Note
IfyouwishtolearnmoreaboutalltheavailableNightwatch.jsconfigurationparameters,pleasevisittheofficialdocumentationathttp://nightwatchjs.org/guide#settings-file.
Finally,tobeabletoruntheE2Etest,wewillalsoneedtoruntheapplicationitselfonawebserver.AswesawinChapter2,AutomatingYourDevelopmentWorkflow,wecanusebrowserSyncforthatpurpose;sowewilladdatasktodeploybrowserSync:
gulp.task('serve',function(cb){
browserSync({
port:8080,
server:{
baseDir:"./"
}
});
gulp.watch([
"./**/*.js",
"./**/*.css",
"./index.html"
],browserSync.reload,cb);
});
Ifonetestisfailingandwedon'tknowwhatiscausingittofail,wewillbeabletotestitmanuallybyrunningtheapplicationinawebbrowser.
Itisimportanttorunthetasksinthecorrectorder.WeneedtoopenaconsoleorterminalandstartSelenium:
selenium-standalonestart
Openanotherconsoleorterminalandrunthefollowingcommands:
gulpbuild-source
gulpbuild-test
gulpbundle-source
gulpbundle-e2e-test
gulpserve
Finally,openathirdconsoleandrunthefollowingcommand:
gulprun-e2e-test
Creatingtestassertions,specs,andsuiteswithMochaandChaiNowthatthetestinfrastructureisready,wewillstartwritingaunittest.WeneedtorememberthatwearegoingtofollowtheBDDdevelopmenttestingstyle,whichmeansthatwewillwritethetestbeforeweactuallywritethecode.
Wewillwriteawebcalculator;becausewewanttokeepitsimple,wewillonlyimplementoneofitsfeatures.Afterdoingsomeanalysis,wehavecomeupwithadesigninterfacethatwillhelpustounderstandtherequirements.Wewilldeclarethefollowinginterfaceintheinterfaces.d.tsfile:
interfaceMathInterface{
PI:number;
pow(base:number,exponent:number);
}
Aswecansee,thecalculatorwillallowustocalculatetheexponentofanumberandtogetthenumberPI.Nowthatweknowtherequirements,wecanstartwritingsomeunittests.Let'screateafilenamedbdd.test.tsandaddthefollowingcode:
///<referencepath="../typings/tsd.d.ts"/>
///<referencepath="../source/interfaces.d.ts"/>
import{MathDemo}from"../source/math_demo";
varexpect=chai.expect;
describe('BDDtestexampleforMathDemoclass\n',()=>{
before(function(){/*invokedoncebeforeALLtests*/});
after(function(){/*invokedonceafterALLtests*/});
beforeEach(function(){/*invokedoncebeforeEACHtest*/});
afterEach(function(){/*invokedoncebeforeEACHtest*/});
it('shouldreturnthecorrectnumericvalueforPI\n',()=>{
varmath:MathInterface=newMathDemo();
expect(math.PI).to.equals(3.14159265359);
expect(math.PI).to.be.a('number');
});
//...
});
Intheprecedingcodesnippet,wehaveimportedthenecessarytypedefinitionfilesandanexternalmodulenamedMathDemo.ThisexternalmodulewilldeclaretheMathDemoclass,whichwillimplementtheMathInterfacethatweareabouttotest.
Wecanalsoseeashortcutforexpect,sowedon'tneedtowritechai.expecteverytimewe
needtoinvokeexpect:
varexpect=chai.expect;
Justbelowtheshortcutwecanfindthefirsttestsuite:
describe('BDDtestexampleforMathDemoclass\n',()=>{
Testsuitesaredeclaredusingthedescribe()functionandareusedtowrapasetofunittests;andtheunitteststhemselvesaredeclaredusingtheit()function:
it('shouldreturnthecorrectnumericvalueforPI\n',()=>{
Insidetheunittest,wecanperformoneormoreassertions.TheChaiassertionsprovideeasilyreadablecodethankstotheusageofachainablestyle:
expect(math.PI).to.equals(3.14159265359);
expect(math.PI).to.be.a('number');
Therearecasesinwhichwewillnoticethatwearerepeatingacertaintestinitializationlogicacrossmultipleunittestswithinatestsuite.Therearesomehelperfunctionsthatwecanusetoavoidcodeduplication.
Thebefore()functionwillbeinvokedbeforeanytestinthesuitecaseisexecuted.Theafter()functionwillbeexecutedafterallthetestsinthetestsuitehavebeenexecuted:
before(function(){/*invokedoncebeforeALLtests*/});
after(function(){/*invokedonceafterALLtests*/});
ThebeforeEach()functionisexecutedonce(beforethetestisexecuted)foreachtestinthetestsuite,whiletheafterEach()functionisexecutedonce(afterthetestisexecuted)foreachtestinthetestsuite:
beforeEach(function(){/*invokedoncebeforeEACHtest*/});
afterEach(function(){/*invokedoncebeforeEACHtest*/});
Ifwerunthetestatthispoint,itwillfailbecausethefeaturebeingtested(PI)isnotimplemented.Let'screateafilenamedmath_demo.tsandaddthefollowingcode:
///<referencepath="./interfaces.d.ts"/>
classMathDemoimplementsMathInterface{
publicPI:number;
constructor(){
this.PI=3.14159265359;
}
//...
}
export{MathDemo};
IfweexecutethetestwithKarma,itshouldpasswithouterrors.Itisimportanttorunthetasksinthecorrectorder.Todothis,weneedtoopenaconsoleorterminalandrunthefollowingcommands:
gulpbuild-source
gulpbuild-test
gulpbundle-source
gulpbundle-test
Finally,wecanruntheunittestsusingthefollowingcommand:
gulprun-unit-test
TherewasanotherrequirementintheMathInterfaceinterface,sowearegoingtorepeattheentireBDDprocessoncemore;butthistime,wewilltestafunctionnamedpowinsteadofaproperty.Wewillstartbyaddinganewtesttothetestsuitethatwehavepreciouslycreated:
it('shouldreturnthecorrectnumericvalueforpow\n',()=>{
varmath:MathInterface=newMathDemo();
varresult=math.pow(3,5);
varexpected=243;
expect(result).to.be.a('number');
expect(result).to.equal(expected);
});
AswecanseeinthepreviouslydeclaredMathInterfaceinterface,thefunctionthatwearegoingtotestisnamedpowandtakestwonumericarguments.SowehavecreatedatestthatwillcreateanewinstanceofMathDemoandinvokeitspowmethod,passingthenumericvalues3and5asarguments.Theexpectedvalueofcalculating3*3*3*3*3is243;forthisreason,wehaveassertedthatthepow()functionreturnsanumericvalueanditsvalueis243.
Atthispoint,theprecedingtestwillfailbecausethepowmethodhasnotbeenimplemented.Let'sreturntothemath_demo.tsfileandimplementthepowmethod:
///<referencepath="./interfaces.d.ts"/>
classMathDemoimplementsMathInterface{
publicPI:number;
constructor(){
this.PI=3.14159265359;
}
publicpow(base:number,exponent:number){
varresult=base;
for(vari=1;i<exponent;i++){
result=result*base;
}
returnresult;
}
//...
}
export{MathDemo};
Ifwerunthetestsagain,wewillbeabletoseethenumberofteststhathavebeenexecuted,howmanyofthemhavefailed,andhowlongittooktofinishtheexecutionofallthetests:
Executed2of2SUCCESS(0.007secs/0.008secs)
TestingtheasynchronouscodeInChapter3,WorkingwithFunctions,welearnedhowtoworkwithasynchronouscode;andinChapter6,ApplicationPerformance,wesawthatusingasynchronouscodeisoneofthegoldenrulesofwebapplicationperformance.Weshouldaimtowriteasynchronouscodeasmuchaswecan,andforthisreason,itisimportanttolearnhowtotestasynchronouscode.
Let'swriteanasynchronousversionofthepowfunctiontodemonstratehowwecantestanasynchronousfunction.Wewillstartwiththerequirements:
interfaceMathInterface{
//..
powAsync(base:number,exponent:number,cb:(result:number)=>void);
}
WeneedtoimplementafunctionnamedpowAsync,whichtakestwonumericvaluesasparameters(justlikebefore)andacallbackfunction.Thetestfortheasynchronousversionisalmostidenticaltothetestthatwewroteforthesynchronousfunction:
it('shouldreturnthecorrectnumericvalueforpow(async)\n',(done)=>{
varmath:MathInterface=newMathDemo();
math.powAsync(3,5,function(result){
varexpected=243;
expect(result).to.be.a('number');
expect(result).to.equal(expected);
done();//invokedone()insideyourcallbackorfulfilledpromises
});
});
Themainthingthatweneedtonoticeisthat,thistime,thecallbackpassedtotheitmethodreceivesanargumentnameddone.Theargumentisafunctionthatweneedtoexecutetoindicatethatthetestexecutionisfinished.
Bydefault,theitmethodwaitsforthecallbacktoreturn,butwhentestingasynchronouscode,thefunctionmayreturnbeforethetestexecutionisfinished:
publicpowAsyncSlow(base:number,exponent:number,cb:(result:number)=>
void){
vardelay=45;//ms
setTimeout(()=>{
varresult=this.pow(base,exponent);
cb(result);
},delay);
}
Whentestingasynchronouscode,Mochawillconsiderthetestasfailed(timeout)ifittakesmorethan2,000millisecondstoinvokethedonefunction.Thetimelimitbeforeatimeoutcanbeconfigured,ascanbewarningsforslowfunctions.
Note
Mocharecommendsthat,whenafunctiontakesmorethan40milliseconds,weshouldconsiderinvestigatinghowtoimproveitsperformance.Ifthefunctionexecutiontakesover100milliseconds,wemustinvestigate.Executiontimesofover2,000millisecondsarenottoleratedbydefault.
AssertingexceptionsAssertingthetypesorvaluesofvariablesisstraightforward,aswehavebeenabletoexploreinthepreviousexamples;butthereisonescenariothatperhapsisnotasintuitiveasthepreviousone.Thisscenarioistestingforanexception.
Let'saddanewmethodtotheMathInterfaceinterfacewiththeonlypurposeofillustratinghowtotestforanexception:
interfaceMathInterface{
//...
bad(foo?:any):void;
}
Thebadmethodthrowsanexceptionwhenitisinvokedwithanon-numericargument:
publicbad(foo?:any){
if(isNaN(foo)){
thrownewError("Error!");
}
else{
//...
}
}
Inthefollowingtest,wecanseehowwecanuseChai'sexpectAPItoassertthatanexceptionisthrown:
it('shouldthrowanexceptionwhennoparameterspassed\n',()=>{
varmath:MathInterface=newMathDemo();
varthrowsF=function(){math.bad(/*missingargs*/)};
expect(throwsF).to.throw(Error);
});
Note
Ifyouwishtolearnmoreaboutassertions,visittheChaiofficialdocumentationavailableathttp://chaijs.com/api/bdd/.
TDDversusBDDwithMochaandChaiTDDandBDDfollowmanyofthesameprinciplesbuthavesomedifferencesintheirstyle.Whilethesetwostylesprovidethesamefunctionality,BDDisconsideredtobeeasiertoreadbymanyofthemembersofasoftwaredevelopmentteam(notjustdevelopers).
Thefollowingtablecomparesthenamingandstyleofsuites,tests,andassertionsbetweentheTDDandBBDstyles:
TDD BDD
suite describe
setup before
teardown after
suiteSetup beforeEach
suiteTeardown afterEach
test it
assert.equal(math.PI,3.14159265359); expect(math.PI).to.equals(3.14159265359);
Note
Inthecompanioncodesamples,youwillfindalltheexamplesinthischapterfollowingboththeTDDandBDDstyles.
TestspiesandstubswithSinon.JSWehavebeenworkingontheMathDemoclass.Wehaveimplementedandtesteditsfeaturesusingunittestsandassertions.NowwearegoingtocreatealittlewebwidgetthatwillinternallyusetheMathDemoclasstoperformamathematicaloperation.WecanthinkofthisnewclassasagraphicaluserinterfacefortheMathDemoclass.WeneedthefollowingHTML:
<divid="widget">
<inputtype="text"id="base"/>
<inputtype="text"id="exponent"/>
<inputtype="text"id="result"/>
<buttonid="submit"type="submit">Submit</button>
</div>
Note
Inthecompanioncode,theHTMLcodecontainsmoreattributes,suchasCSSclasses;buttheybeenhaveremovedhereforclarity.
Let'screateafilenamedcalculator_widget.tsunderthesourcedirectory.WearegoingtostoretheHTMLcodeinastringvariablelocatedinthescopeofthewebwidget.ThenewclasswillbecalledCalculatorWidget,anditwillimplementtheCalculatorWidgetInterfaceinterface:
interfaceCalculatorWidgetInterface{
render(id:string);
onSubmit():void;
}
WeshouldwritetheunittestbeforeweimplementtheCalculatorWidgetclass,butthistimewewillbreaktheBDDrulesinanattempttofacilitatetheunderstandingofstubsandspies:
///<referencepath="./interfaces.d.ts"/>
///<referencepath="../typings/tsd.d.ts"/>
vartemplate='HTML...';
classCalculatorWidgetimplementsCalculatorWidgetInterface{
private_math:MathInterface;
private$base:JQuery;
private$exponent:JQuery;
private$result:JQuery;
private$btn:JQuery;
constructor(math:MathInterface){
if(math==null)thrownewError("Argumentnullexception!");
this._math=math;
}
publicrender(id:string){
$(id).html(template);
this.$base=$("#base");
this.$exponent=$("#exponent");
this.$result=$("#result");
this.$btn=$("#submit");
this.$btn.on("click",(e)=>{
this.onSubmit();
});
}
publiconSubmit(){
varbase=parseInt(this.$base.val());
varexponent=parseInt(this.$exponent.val());
if(isNaN(base)||isNaN(exponent)){
alert("Baseandexponentmustbeanumber!");
}
else{
this.$result.val(this._math.pow(base,exponent));
}
}
}
export{CalculatorWidget};
Aswecansee,wehavedefinedavariablethatcontainstheHTMLthatwepreviouslyexaminedbutitisnotdisplayedforbrevity.AnewclassnamedCalculatorWidgetisalsodefinedtogetherwiththeclassconstructor.Wecanobservethattheclasshastwoproperties:avariablenamed_domandanimplementationofMathInterfacenamed_math.WearedependingonaninterfacebecauseaswesawinChapter4,Object-OrientedProgrammingwithTypeScript,itisagoodpractice(dependencyinversionprinciple)todoso.
NoticethattheclassconstructortakesanimplementationofMathInterfaceasitsonlyargument.Passingthedependenciesofacomponentviaitsconstructorisalsoagoodpracticeandisusedtoreducethecouplingbetweencomponents.
ThefirstmethodintheclassisnamedrenderandtakestheID(string)ofanHTMLelementasitsonlyargument.TheIDisusedtoselectthenodethatmatchestheIDusingajQueryselector.Onceithasbeenselected,theHTMLthatwepreviouslyexaminedisinsertedintotheselectednode.WecansaythatthecomponentisinchargeofrenderingitsownHTMLandcanbereusedeasilyjustbychangingitscontainer.Thisishowwebwidgetsusuallywork:theyareindependentcomponentsthatcanbeconsideredasreusablestandaloneapplicationswithinaparentapplicationthatisnomorethanjustacollectionofwebwidgets.
AfterrenderingtheHTML,therendermethodcreatesshortcutsforeachcomponentofthewidget'sformandinitializesaclickeventlistener:
publicrender(id:string){
$(id).html(template);
this._dom.$base=$("#base");
this._dom.$exponent=$("#exponent");
this._dom.$result=$("#result");
this._dom.$btn=$("#submit");
this._dom.$btn.on("click",(e)=>{
this.onSubmit();
});
}
Whenauserclicksonthebuttonwithidequalstosubmit,aneventistriggered,andtheeventlistenerinvokestheonSubmitfunctionthatwecanfindinthefollowingcodesnippet.Thisfunctionwillreadthevaluesforbaseandexponentusingtheshortcutspreviouslydeclared:
publiconSubmit(){
varbase=parseInt(this._dom.$base.val());
varexponent=parseInt(this._dom.$exponent.val());
if(isNaN(base)||isNaN(exponent)){
alert("Baseandexponentmustbeanumber!");
}
else{
this._dom.$result.val(this._math.pow(base,exponent));
}
}
}
Ifthevaluesoftheinputs(baseandexponent)arenotnumericvalues,analertmessageisdisplayedtoprovidetheuserswitherrorfeedback.Ifthevaluesarenumeric,thepowmethodoftheMathDemoclassisinvoked,andtheresultisassignedtotheresultfieldvalueviaoneofthepreviouslycreatedshortcuts.
Writingunittestscanbecomeacomplextaskwhenthecomponentsbeingtestedarehighlycoupledwithothercomponents.Intheprevioussection,wetriedtofollowsomegoodpracticessuchasthedependencyinversionprincipleorinjectingdependenciesviatheconstructorofthedependent;butsometimes,evenwhenusinggoodpractices,wewillhavetodealwithhighlycoupledcode.
Spies,mocks,andstubscanhelpustotakeawaysomeofthepaincausedbyhighlycoupledcomponents.Thesefeaturescanalsohelpustoidentifytherootcauseofanissue.Ifwereplaceallthedependenciesofacomponentwithstubsandatestfail,wewillknowthattheissueislocatedinthecomponentbeingtestedandnotinoneofitsdependencies.
Forexample,theCalculatorWidgetclasshasadependencyontheMathDemoclass.Ifthereisanissueinthecalculatorwebsite,wewillnotbeabletoknowiftherootcauseoftheissueislocatedintheCalculatorWidgetclassortheMathDemoclass.However,ifwewritesomeunittestsfortheCalculatorWidgetclassinisolation(replacingitsMathDemodependencywithastub)andsomeofthetestsfail,wewillknowforsurethattherootcauseoftheissueislocatedintheCalculatorWidgetandnotintheMathDemoclass.
Let'stakealookatsometestexamples.
SpiesWearegoingtostartbytakingalookattheuseofspiesbycreatinganewtestsuite.Thistimewewillusethebefore()andbeforeEach()functions.Whenthebefore()functionisinvoked(beforeanyunittestisexecuted),anewHTMLnodeiscreatedtoholdthewidget'sHTML.
ThebeforeEach()functionisusedtoresetthecontainerbeforeeachtest.Thisway,wecanensurethatanewwidgetiscreatedforeachtestinthetestsuite.Thisisagoodideabecauseitwillpreventonetestfrompotentiallyaffectingtheresultsofanother.
describe('BDDtestexampleforCalculatorWidgetclass\n',()=>{
before(function(){
$("body").append('<divid="widget"/>');
});
beforeEach(function(){
$("#widget").empty();
});
Note
Usually,testingframeworks(regardlessofthelanguageweareworkingwith)won'tallowustocontroltheorderinwhichtheunittestsandtestsuitesareexecuted.Thetestscanevenbeexecutedinparallelbyusingmultiplethreads.Forthisreason,itisimportanttoensurethattheunittestsinourtestsuitesareindependentofeachother.
Nowthatthetestsuiteisready,wecancreateunittestsfortherender()andonSubmit()methods.TheteststartsbythecreationofaninstanceofMathDemo,whichisthenpassedtoCalculatorWidgetconstructortocreateanewinstancenamedcalculator.
TherendermethodistheninvokedtorenderthewidgetinsidetheHTMLnodewiththeIDwidget.TheHTMLnodeshouldbeavailableatthisstagebecauseitwascreatedbythebefore()method.Afterthewidgethasbeenrendered,avalueissetfortheinputswithIDsbaseandexponent.
Thetestspecification(onSubmitshouldbeinvokedwhen#submitisclicked)shouldhelpusunderstandthatwearetestingtheclickevent.WearegoingtouseaspytoobservetheonSubmit()function;so,whenthebuttonwithIDsubmitisclicked,thespywilldetectthattheonSubmit()functionwasinvoked.
Tofinishthetest,wearegoingtotriggeraclickeventonthebuttonwithIDsubmitandassertthattheonSubmit()functionwasactuallyonlyinvokedonce:
it('onSubmitshouldbeinvokedwhen#submitisclicked',()=>{
varmath:MathInterface=newMathDemo();
varcalculator=newCalculatorWidget(math);
calculator.render("#widget");
$('#base').val("2");
$('#exponent').val("3");
//spyononSubmit
varonSubmitSpy=sinon.spy(calculator,"onSubmit");
$("#submit").trigger("click");
//assertcalculator.onSubmitwasinvokedwhenclickon#submit
expect(onSubmitSpy.called).to.equal(true);
expect(onSubmitSpy.callCount).to.equal(1);
expect($("#result").val()).to.equal("8");
});
Spieswillallowustoperformmanyoperations:fromcheckinghowmanytimesafunctionhasbeeninvokedtocheckingifitwasinvokedusingthenewoperator,orifitwasinvokedwithasetofspecificparameters.
ThelastassertionhelpsusguaranteethatonSubmit()issettingthecorrectresultintheresultinput.
Note
AllthepossibleoperationsaredetailedintheSinon.JSonlinedocumentationfoundathttp://sinonjs.org/docs/#sinonspy.
StubsItmaylooklikewehavealreadytestedtheentireapplicationbynow,butthatisusuallyneverthecase.Let'sanalyzewhatexactlywehavetestedsofar:
WehavetestedtheentireMathDemoclass,andweknowthatitreturnsthecorrectvaluewhenpowisinvokedWeknowthattheCalculatorWidgetclassisrenderingtheHTMLcorrectlyWeknowthattheCalculatorWidgetclassissettingupsomeeventsandreadingsomevaluesfromtheHTMLinputsasexpected
Sofar,wehavecreatedsometestsfortheMathDemoclassandtheCalculatorWidgetclass,butwehaveforgottentotesttheintegrationbetweenthem.
Wehavebeentestingusing2asbaseand3asexponent,butifwewronglyusedthesamevalueasbaseandexponent,wecouldhavemissedonepotentialissue:maybetheCalculatorWidgetclassispassingtheargumentsinincorrectordertotheMathDemoclasswhenthefunctionpow()isinvokedinthebodyoftheonSubmit()function.
Note
Lateroninthischapter,wewillseehowtogenerateakindofreport(atestcoveragereport)thatcanhelpustoidentifyareasofourapplicationthathavenotbeentested.
WecantestthisscenariobyisolatingtheCalculatorWidgetclassfromitsdependencyontheMathDemoclass.Wecanachievethisbyusingastub.Let'stakealookattheupcomingunitteststoseeastubinaction.
Atthebeginningofthemethod,anewinstanceofMathDemoiscreated,andastubisusedagainstitspowmethod.Thestubwillreplacethepowmethodwithanewmethod.Thenewmethodwillassertthattheparametersreceivedareinthecorrectorder:
it('passtherightargstoMath.pow',(done)=>{
varmath:MathInterface=newMathDemo();
//replacepowmethodwithamethodfortesting
sinon.stub(math,"pow",function(a,b){
//assertthatCalculatorWidget.onSubmitinvokes
//math.powwiththerightarguments
expect(a).to.equal(2);
expect(b).to.equal(3);
done();
});
varcalculator=newCalculatorWidget(math);
calculator.render("#widget");
$('#base').val("2");
$('#exponent').val("3");
$("#submit").trigger("click");
});
Oncethestubisready,anewinstanceoftheCalculatorWidgetclassiscreated,butinsteadofpassinganormalinstanceofMathDemoasitsonlyargument,weareinjectingthestub.Bydoingthis,wearenolongertestingtheMathDemoclass,andwearetestingtheCalculatorWidgetclassinanisolatedenvironment.Thiswouldhavebeenmuchmorecomplicatedwithoutadesignthatfacilitatesreplacingtheclassdependenciesviaaconstructorinjection.
Tofinishthetest,werenderthecalculatorwidget,setthevalueoftheinputswithIDsbaseandexponent,andtriggeraclickonthebuttonwithIDsubmit.TheeventwillinvoketheonSubmitfunction,whichwilltheninvokethepowmethod.Whentheparametersareintheincorrectorder,wewillbeabletobe100percentsureaboutthelocationoftherootcauseofthisissue:theonSubmitfunction.
Creatingend-to-endtestswithNightwatch.jsWritinganE2EtestwithNightwatch.jsisanintuitiveprocess.WeshouldbeabletoreadanE2Etestandbeabletounderstanditevenifitisthefirsttimethatweencounterone.
Ifwetakealookatthefollowingcodesnippet,wewillseethat,oncewehavereachedthepage,thetestwillwait1secondforthebodyofthepagetobevisible.Thetestwillthenwait0.1secondsforsomeelementstobevisible.TheelementscanbeselectedusingCSSselectorsorXPathsyntax.Iftheelementsarevisible,thesetValuemethodwillinsert2inthetextinputwithbaseasIDand3inthetextinputwithexponentasID:
vartest={
'Calculatorpowe2etestexample':function(client){
client
.url('http://localhost:8080/')
.waitForElementVisible('body',1000)
.assert.waitForElementVisible('TypeScriptTesting',100)
.assert.waitForElementVisible('input#base',100)
.assert.waitForElementVisible('input#exponent',100)
.setValue('input#base','2')
.setValue('input#exponent','3')
.click('button#submit')
.pause(100)
.assert.value('input#result','8')
.end();
}
};
export=test;
Thetestwillthenfindthesubmitbuttonandtriggeranon-clickevent.After0.1seconds,thetestassertsthatthecorrectvaluehasbeeninsertedintothetextinputwithresultasID.Wecanseeeachofthesestepsintheconsoleduringthetestexecution.
Wecanrunthetestsusingthefollowingcommand:
gulprun-e2e-test
Note
RememberthatwemustrunthetaskstocompileandbundletheE2EtestsaswellasruntheapplicationinawebserverwithBrowserSyncandexecuteSeleniumbeforebeingabletorunE2Etests.
GeneratingtestcoveragereportsEarlierininthischapter,whenweconfiguredKarma,weaddedsomesettingstogeneratetestcoveragereports.Let'stakealookatthekarma.conf.jsfiletoidentifytestcoverage-relatedconfiguration:
module.exports=function(config){
'usestrict';
config.set({
basePath:'',
frameworks:['mocha','chai','sinon'],
browsers:['PhantomJS'],
reporters:['progress','coverage'],
coverageReporter:{
type:'lcov',
dir:__dirname+'/coverage/'
},
plugins:[
'karma-coverage',
'karma-mocha',
'karma-chai',
'karma-sinon',
'karma-phantomjs-launcher'
],
preprocessors:{
'**/bundled/test/bdd.test.js':'coverage'
},
files:[
{
pattern:"/bundled/test/bdd.test.js",
included:true
},
{
pattern:"/node_modules/jquery/dist/jquery.min.js",
included:true
},
{
pattern:"/node_modules/bootstrap/dist/js/bootstrap.min.js",
included:true
}
],
client:{
mocha:{
ui:"bdd"
}
},
port:9876,
colors:true,
autoWatch:false,
logLevel:config.DEBUG
});
};
Aswecansee,weneedtosetthefolderinwhichthetestcoveragereportwillbestored.Wealsoneedtoaddcoveragetothereporter'ssettingandanewentrynamedcoverageReporttoconfiguretheformatofthereport.
Wecannotforgettoinstallthekarma-coveragepluginusingnpmandaddingareferenceinthekarma.conf.jsunderthepluginsfield.Finally,weneedtoaddcoveragetothepreprocessorfield:
npmkarma-coverage
Togeneratethereport,wejustneedtoexecutetheGulptasksusedtorunalltheunittestsintheapplication.Wecandosobyusingthefollowingcommand:
gulprun-unit-test
Oncetheexecutionofthetesthasbeencompleted,wecanopenthefolderinwhichwedecidedtostorethecoveragereportsandopentheavailableindex.htmlfileinawebbrowser.TheHTMLreportallowsustonavigatetothecoveragestatisticsofaspecificfilebyclickingonthenameofoneofthesourcefiles.
Thereportcanhelpustoidentifywitheasethepartsofourcodethathavenotbeentested(linesarehighlightedinred).Thetestcoveragereportalsocalculatesthenumberoflinestestedagainstthenumberoflinesintheapplication.Aswecanseeintheprecedingscreenshot,only82.24percentofthestatementsaretestedintheexample.
Note
Ifyouwouldliketolearnmoreaboutallthetoolsthatwehavediscussedinthischapter,I
highlyrecommendcheckingoutthebookBackbone.jsTestingwrittenbyRyanRoemer.
SummaryInthischapter,wediscussedsomecoretestingconcepts(includingstubs,spies,suites,andmore).Wealsolookedatthetest-drivendevelopmentandbehavior-drivendevelopmentapproachesandhowtoworkwithsomeoftheleadingJavaScripttestingframeworks,suchasMocha,Chai,Sinon.JS,Karma,Selenium,andNightwatch.js.
Towardstheendofthechapterweexploredhowtotestacrossmultipledevicesandhowtogeneratetestcoveragereports.
Inthenextchapter,wewilllookatdecoratorsandthemetadatareflectionAPI—twoexcitingnewfeaturesintroducedbyTypeScript1.5.
Chapter8.DecoratorsInthischapter,youaregoingtolearnaboutannotationsanddecorators—thetwonewfeaturesbasedonthefutureECMAScript6specification,butwecanusethemtodaywithTypeScript1.5.
Youwilllearnaboutthefollowingtopics:
Annotationsanddecorators:ClassdecoratorsMethoddecoratorsPropertydecoratorsParameterdecoratorsDecoratorfactoryDecoratorswithparameters
ThereflectionmetadataAPI
PrerequisitesTheTypeScriptfeaturesinthischapterrequireTypeScript1.5orhigher.WecanuseGulpaswehavedoneinpreviouschapters,butweneedtoensurethatthelatestversionofTypeScriptisusedbythegulp-typescriptpackage.Let'sstartbycreatingapackage.jsonfileandinstallingtherequiredpackages:
npminit
npminstall--save-devgulpgulp-typescripttypescript
npminstall--savereflect-metadata
Oncewehaveinstalledthepackages,wecancreateagulpfile.jsfileandaddanewtasktocompileourcode.
Thefollowingcodesnippetshowstherequiredcompilerconfiguration.ThecompilationtargetmustbeES5andtheemitDecoratorMetadatasettingmustbesetastrue.WealsoneedtospecifythepackagethatprovidestheTypeScriptcompilertoensurethatthelatestversionisused:
vargulp=require("gulp"),
tsc=require("gulp-typescript"),
typescript=require("typescript");
vartsProject=tsc.createProject({
removeComments:false,
noImplicitAny:false,
target:"es5",
module:"commonjs",
declarationFiles:false,
emitDecoratorMetadata:true,
typescript:typescript
});
Oncethecompilersettingsareready,wecanwriteagulptaskusingthegulp-typescriptplugin:
gulp.task("build-source",function(){
returngulp.src(__dirname+"/file.ts")
.pipe(tsc(tsProject))
.js.pipe(gulp.dest(__dirname+"/"));
});
AnnotationsanddecoratorsAnnotationsareawaytoaddmetadatatoclassdeclarations.Themetadatacanthenbeusedbytoolssuchasdependencyinjectioncontainers.
TheannotationsAPIwasproposedbytheGoogleAtScriptteambutannotationsarenotastandard.However,decoratorsareaproposedstandardforECMAScript7byYehudaKatz,toannotateandmodifyclassesandpropertiesatdesigntime.
Annotationsanddecoratorsareprettymuchthesame:
Annotationsanddecoratorsarenearlythesamething.Fromaconsumerperspectivewehaveexactlythesamesyntax.Theonlythingthatdiffersisthatwedon'thavecontroloverhowannotationsareaddedasmetadatatoourcode.Adecoratorisratheraninterfacetobuildsomethingthatendsupasannotation.
Overalongterm,however,wecanjustfocusondecorators,sincethosearearealproposedstandard.AtScriptisTypeScriptandTypeScriptimplementsdecorators.
--"ThedifferencebetweenAnnotationsandDecorators"byPascalPrecht
Wearegoingtousethefollowingclasstoshowcasehowtoworkwithdecorators:
classPerson{
publicname:string;
publicsurname:string;
constructor(name:string,surname:string){
this.name=name;
this.surname=surname;
}
publicsaySomething(something:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
}
Therearefourtypesofdecoratorsthatcanbeusedtoannotate:classes,properties,methods,andparameters.
TheclassdecoratorsTheofficialTypeScriptdecoratorproposaldefinesaclassdecoratorasfollows:
Aclassdecoratorfunctionisafunctionthatacceptsaconstructorfunctionasitsargument,andreturnseitherundefined,theprovidedconstructorfunction,oranewconstructorfunction.Returningundefinedisequivalenttoreturningtheprovidedconstructorfunction.
--"DecoratorsProposal–TypeScript"byRonBuckton
Aclassdecoratorisusedtomodifytheconstructorofclassinsomeway.Iftheclassdecoratorreturnsundefined,theoriginalconstructorremainsthesame.Ifthedecoratorreturns,thereturnvaluewillbeusedtooverridetheoriginalclassconstructor.
WearegoingtocreateaclassdecoratornamedlogClass.Wecanstartbydefiningthedecoratorasfollows:
functionlogClass(target:any){
//…
}
Theclassdecoratorabovedoesnothaveanylogicyet,butwecanalreadyapplyittoaclass.Toapplyadecorator,weneedtousetheat(@)symbol:
@logClass
classPerson{
publicname:string;
publicsurname:string;
//...
Ifwehavedeclaredandappliedadecorator,afunctionnamed__decoratewillbegeneratedbytheTypeScriptcompiler,whichwillthencompileourcodeinJavaScript.Wearenotgoingtoexaminetheinternalimplementationofthe__decoratefunction,butweneedtounderstandthatitisusedtoapplyadecoratoratruntime.WecanseeitinactionbyexaminingtheJavaScriptcodethatisgeneratedwhenwecompilethedecoratedPersonclassmentionedpreviously:
varPerson=(function(){
functionPerson(name,surname){
this.name=name;
this.surname=surname;
}
Person.prototype.saySomething=function(something){
returnthis.name+""+this.surname+"says:"+something;
};
Person=__decorate([
logClass
],Person);
returnPerson;
})();
Nowthatweknowhowtheclassdecoratorwillbeinvoked,let'simplementit:
functionlogClass(target:any){
//saveareferencetotheoriginalconstructor
varoriginal=target;
//autilityfunctiontogenerateinstancesofaclass
functionconstruct(constructor,args){
varc:any=function(){
returnconstructor.apply(this,args);
}
c.prototype=constructor.prototype;
returnnewc();
}
//thenewconstructorbehaviour
varf:any=function(...args){
console.log("New:"+original.name);
returnconstruct(original,args);
}
//copyprototypesoinstanceofoperatorstillworks
f.prototype=original.prototype;
//returnnewconstructor(willoverrideoriginal)
returnf;
}
Theclassdecoratortakestheconstructoroftheclassbeingdecoratedasitsonlyargument.Thismeansthattheargument(namedtarget)istheconstructorofthePersonclass.
Thedecoratorstartsbycreatingacopyoftheclassconstructor,thenitdefinesautilityfunction(namedconstruct)thatcanbeusedtogenerateinstancesofaclass.
Decoratorsareusedtoaddsomeextralogicormetadatatothedecoratedelement.Whenwetrytoextendthefunctionalityofafunction(methodsorconstructors),weneedtowraptheoriginalfunctionwithanewfunctionthatcontainstheadditionallogicandinvokestheoriginalfunction.
Intheprecedingdecorator,weaddedextralogictologintheconsole,thenameoftheclasswhenanewinstanceiscreated.Toachievethis,anewclassconstructor(namedf)wasdeclared.Thenewconstructorcontainstheadditionallogicandusestheconstructfunctiontoinvoketheoriginalclassconstructor.
Attheendofthedecorator,theprototypeoftheoriginalconstructorfunctioniscopiedtothenewconstructorfunctiontoensurethattheinstanceofoperatorcontinuestoworkwhenitisappliedtoaninstanceofthedecoratedclass.Finally,thenewconstructorisreturnedandsomecodegeneratedbytheTypeScriptcompilerusesittooverridetheoriginalclassconstructor.
Afterdecoratingtheclassconstructor,anewinstanceiscreated:
varme=newPerson("Remo","Jansen");
Ondoingso,thefollowingtextappearsintheconsole:
"New:Person"
ThemethoddecoratorsTheofficialTypeScriptdecoratorproposaldefinesamethoddecoratorasfollows.
Amethoddecoratorfunctionisafunctionthatacceptsthreearguments:Theobjectthatownstheproperty,thekeyfortheproperty(astringorasymbol),andoptionallythepropertydescriptoroftheproperty.Thefunctionmustreturneitherundefined,theprovidedpropertydescriptor,oranewpropertydescriptor.Returningundefinedisequivalenttoreturningtheprovidedpropertydescriptor.
--"DecoratorsProposal–TypeScript"byRonBuckton
Themethoddecoratorisreallysimilartotheclassdecoratorbutitisusedtooverrideamethod,asopposedtousingittooverridetheconstructorofaclass.
Ifthemethoddecoratorreturnsavaluedifferentfromundefined,thereturnedvaluewillbeusedtooverridethepropertydescriptorofthemethod.
Note
NotethatapropertydescriptorisanobjectthatcanbeobtainedbyinvokingtheObject.getOwnPropertyDescriptor()method.
Let'sdeclareamethoddecoratornamedlogMethodwithoutanybehaviorfornow:
functionlogMethod(target:any,key:string,descriptor:any){
//...
}
WecanapplythedecoratortooneofthemethodsinthePersonclass:
//...
@logMethod
publicsaySomething(something:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
//...
Themethoddecoratorisinvokedusingthefollowingarguments:
TheprototypeoftheclassthatcontainsthemethodbeingdecoratedisPerson.prototypeThenameofthemethodbeingdecoratedissaySomethingThepropertydescriptorofthemethodbeingdecoratedisObject.getOwnPropertyDescriptor(Person.prototype,saySomething)
Nowthatweknowthevalueofthedecoratorparameters,wecanproceedtoimplementit:
functionlogMethod(target:any,key:string,descriptor:any){
//saveareferencetotheoriginalmethod
varoriginalMethod=descriptor.value;
//editingthedescriptor/valueparameter
descriptor.value=function(...args:any[]){
//convertmethodargumentstostring
vara=args.map(a=>JSON.stringify(a)).join();
//invokemethodandgetitsreturnvalue
varresult=originalMethod.apply(this,args);
//convertresulttostring
varr=JSON.stringify(result);
//displayinconsolethefunctioncalldetails
console.log(`Call:${key}(${a})=>${r}`);
//returntheresultofinvokingthemethod
returnresult;
}
//returnediteddescriptor
returndescriptor;
}
Justlikewedidwhenweimplementedtheclassdecorator,westartbycreatingacopyoftheelementbeingdecorated.Insteadofaccessingthemethodviatheclassprototype(target["key"]),wewillaccessitviathepropertydescriptor(descriptor.value).
Wethencreateanewfunctionthatwillreplacethemethodbeingdecorated.Thenewfunctioninvokestheoriginalmethodbutalsocontainssomeadditionallogicusedtologintheconsole,themethodname,andthevalueofitsargumentseverytimeitisinvoked.
Afterapplyingthedecoratortothemethod,themethodnameandargumentswillbeloggedintheconsolewhenitisinvoked:
varme=newPerson("Remo","Jansen");
me.saySomething("hello!");
//Call:saySomething("hello!")=>"RemoJansensays:hello!"
ThepropertydecoratorsTheofficialTypeScriptdecoratorproposaldefinesapropertydecoratorasfollows:
Apropertydecoratorfunctionisafunctionthatacceptstwoarguments:Theobjectthatownsthepropertyandthekeyfortheproperty(astringorasymbol).Apropertydecoratordoesnotreturn.
--"DecoratorsProposal–TypeScript"byRonBuckton
Apropertydecoratorisreallysimilartoamethoddecorator.Themaindifferencesarethatapropertydecoratordoesn'treturnavalueandthatthethirdparameter(thepropertydescriptor)isnotpassedtothepropertydecorator.
Let'screateapropertydecoratornamedlogPropertytoseehowitworks:
functionlogProperty(target:any,key:string){
//...
}
WecanuseitinoneofthePersonclass'spropertiesasfollows:
classPerson{
@logProperty
publicname:string;
//...
Aswehavebeendoingsofar,wearegoingtoimplementadecoratorthatwilloverridethedecoratedpropertywithanewpropertythatwillbehaveexactlyastheoriginalone,butwillperformanadditionaltask—loggingthepropertyvalueintheconsolewheneveritchanges:
functionlogProperty(target:any,key:string){
//propertyvalue
var_val=this[key];
//propertygetter
vargetter=function(){
console.log(`Get:${key}=>${_val}`);
return_val;
};
//propertysetter
varsetter=function(newVal){
console.log(`Set:${key}=>${newVal}`);
_val=newVal;
};
//Deleteproperty.Thedeleteoperatorthrows
//instrictmodeifthepropertyisanown
//non-configurablepropertyandreturns
//falseinnon-strictmode.
if(deletethis[key]){
Object.defineProperty(target,key,{
get:getter,
set:setter,
enumerable:true,
configurable:true
});
}
}
Intheprecedingdecorator,wecreatedacopyoftheoriginalpropertyvalueanddeclaredtwofunctions:getter(invokedwhenwechangethevalueoftheproperty)andsetter(invokedwhenwereadthevalueoftheproperty)respectively.
Inthepreviousdecorator,thereturnvaluewasusedtooverridetheelementbeingdecorated.Becausethepropertydecoratordoesn'treturnavalue,wecan'toverridethepropertybeingdecoratedbutwecanreplaceit.WehavemanuallydeletedtheoriginalpropertyandcreatedanewpropertyusingtheObject.definePropertyfunctionandthepreviouslydeclaredgetterandsetterfunctions.
Afterapplyingthedecoratortothenameproperty,wewillbeabletoobserveanychangestoitsvalueintheconsole:
varme=newPerson("Remo","Jansen");
//Set:name=>Remo
me.name="RemoH.";
//Set:name=>RemoH.
varn=me.name;
//Get:nameRemoH.
TheparameterdecoratorsTheofficialdecoratorproposaldefinesaparameterdecoratorasfollows:
Aparameterdecoratorfunctionisafunctionthatacceptsthreearguments:Theobjectthatownsthemethodthatcontainsthedecoratedparameter,thepropertykeyoftheproperty(orundefinedforaparameteroftheconstructor),andtheordinalindexoftheparameter.Thereturnvalueofthisdecoratorisignored.
DecoratorsProposal–TypeScript"byRonBuckton
Let'screateaparameterdecoratornamedaddMetadatatoseehowitworks:
functionaddMetadata(target:any,key:string,index:number){
//...
}
Wecanapplythepropertydecoratortoaparameterasfollows:
publicsaySomething(@addMetadatasomething:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
Theparameterdecoratordoesn'treturn,whichmeansthatwewillnotbeabletooverridethemethodthatcontainstheparameterbeingdecorated.
Wecanuseparameterdecoratorstoaddsomemetadatatotheprototype(target)class.Inthefollowingimplementation,wewilladdanarraynamedlog_${key}_parametersasaclasspropertywherekeyisthenameofthemethodthatcontainstheparameterbeingdecorated:
functionaddMetadata(target:any,key:string,index:number){
varmetadataKey=`_log_${key}_parameters`;
if(Array.isArray(target[metadataKey])){
target[metadataKey].push(index);
}
else{
target[metadataKey]=[index];
}
}
Toallowmorethanoneparametertobedecorated,wecheckwhetherthenewfieldisanarray.Ifthenewfieldisnotanarray,wecreateandinitializethenewfieldtobeanewarraycontainingtheindexoftheparameterbeingdecorated.Ifthenewfieldisanarray,theindexoftheparameterbeingdecoratedisaddedtothearray.
Aparameterdecoratorisnotreallyusefulonitsown;itneedstobecombinedwithamethoddecorator,sotheparameterdecoratoraddsthemetadataandthemethoddecoratorreadsit:
@readMetadata
publicsaySomething(@addMetadatasomething:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
Thefollowingmethoddecoratorworkslikethemethoddecoratorthatweimplementedpreviouslyinthischapter,butitwillreadthemetadataaddedbytheparameterdecoratorandinsteadofdisplayingalltheargumentspassedtothemethodintheconsolewhenitisinvoked,itwillonlylogtheonesthathavebeendecorated:
functionreadMetadata(target:any,key:string,descriptor:any){
varoriginalMethod=descriptor.value;
descriptor.value=function(...args:any[]){
varmetadataKey=`_log_${key}_parameters`;
varindices=target[metadataKey];
if(Array.isArray(indices)){
for(vari=0;i<args.length;i++){
if(indices.indexOf(i)!==-1){
vararg=args[i];
varargStr=JSON.stringify(arg)||arg.toString();
console.log(`${key}arg[${i}]:${argStr}`);
}
}
varresult=originalMethod.apply(this,args);
returnresult;
}
}
returndescriptor;
}
IfweapplythesaySomethingmethod:
varperson=newPerson("Remo","Jansen");
person.saySomething("hello!");
ThereadMetadatadecoratorwilldisplaythevalueoftheparametersthatwereaddedtothemetadata(theclasspropertynamed_log_saySomething_parameters)intheconsolebytheaddMetadatadecorator:
saySomethingarg[0]:"hello!"
Note
Notethat,inthepreviousexample,weusedaclasspropertytostoresomemetadata.Laterinthischapter,youwilllearnhowtousethereflectionmetadataAPI;thisAPIhasbeendesignedspecificallytogenerateandreadmetadataanditis,therefore,recommendedtouseitwhenweneedtoworkwithdecoratorsandmetadata.
ThedecoratorfactoryTheofficialdecoratorproposaldefinesadecoratorfactoryasfollows:
Adecoratorfactoryisafunctionthatcanacceptanynumberofarguments,andmustreturnoneoftheabovetypesofdecoratorfunction.
DecoratorsProposal–TypeScript"byRonBuckton
Youlearnedtoimplementclass,property,method,andparameterdecorators.Inthemajorityofcases,wewillconsumedecorators,notimplementthem.Forexample,inAngular2.0,wewillusean@viewdecoratortodeclarethataclasswillbehaveasaView,butwewillnotimplementthe@viewdecoratorourselves.
Wecanusethedecoratorfactorytomakedecoratorseasiertoconsume.Let'sconsiderthefollowingcodesnippet:
@logClass
classPerson{
@logProperty
publicname:string;
publicsurname:string;
constructor(name:string,surname:string){
this.name=name;
this.surname=surname;
}
@logMethod
publicsaySomething(@logParametersomething:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
}
Theproblemwiththeprecedingcodeisthatwe,asdevelopers,needtoknowthatthelogMethoddecoratorcanonlybeappliedtoamethod.Thismightseemtrivialbecausethedecoratornamingusedabovemakesiteasierforus.
Abettersolutionistoenabledeveloperstousean@logdecoratorwithouthavingtoworryaboutusingtherightkindofdecorator:
@log
classPerson{
@log
publicname:string;
publicsurname:string;
constructor(name:string,surname:string){
this.name=name;
this.surname=surname;
}
@log
publicsaySomething(@logsomething:string):string{
returnthis.name+""+this.surname+"says:"+something;
}
}
Wecanachievethisbycreatingadecoratorfactory.Adecoratorfactoryisafunctionthatisabletoidentifywhatkindofdecoratorisrequiredandreturnit:
functionlog(...args:any[]){
switch(args.length){
case1:
returnlogClass.apply(this,args);
case2:
//breakinsteadofreturnasproperty
//decoratorsdon'thaveareturn
logProperty.apply(this,args);
break;
case3:
if(typeofargs[2]==="number"){
logParameter.apply(this,args);
}
returnlogMethod.apply(this,args);
default:
thrownewError("Decoratorsarenotvalidhere!");
}
}
Aswecanobserveintheprecedingcodesnippet,thedecoratorfactoryusesthenumberandtypeofargumentspassedtothedecoratortoidentifytherequiredkindofdecorator.
DecoratorswithargumentsWecanuseaspecialkindofdecoratorfactorytoallowdeveloperstoconfigurethebehaviorofadecorator.Forexample,wecouldpassastringtoaclassdecoratorasfollows:
@logClass("option")
classPerson{
//...
Inordertobeabletopasssomeparameterstoadecorator,weneedtowrapthedecoratorwithafunction.Thewrapperfunctiontakestheparametersofourchoiceandreturnsadecorator:
functionlogClass(option:string){
returnfunction(target:any){
//classdecoratorlogicgoeshere
//wehaveaccesstothedecoratorparameters
console.log(target,option);
}
}
Thiscanbeappliedtoallthekindsofdecoratorthatyoulearnedaboutinthischapter.
ThereflectionmetadataAPIYoulearnedthatdecoratorscanbeusedtomodifyandextendthebehaviorofaclass'smethodsorproperties.Youalsolearnedthatwecanusedecoratorstoaddmetadatatotheclassbeingdecorated.
Forlessexperienceddevelopers,thepossibilityofaddingmetadatatoaclassmightnotseemreallyusefulorexcitingbutitisoneofthegreatestthingsthathashappenedtoJavaScriptinthepastfewyears.
Aswealreadyknow,TypeScriptonlyusestypesatdesigntime.However,somefeaturessuchasdependencyinjection,runtimetypeassertions,reflection,andtestingarenotpossiblewithoutthetypeinformationbeingavailableatruntime.Thisisnotaproblemanymorebecausewecanusedecoratorstogeneratemetadataandthatmetadatacancontaintypeinformation.Themetadatacanthenbeprocessedatruntime.
WhentheTypeScriptteamstartedtothinkaboutthebestpossiblewaytoallowdeveloperstogeneratetypeinformationmetadata,theyreservedafewspecialdecoratornamesforthispurposes.
Theideawasthat,whenanelementwasdecoratedusingthesereserveddecorators,thecompilerwouldautomaticallyaddthetypeinformationtotheelementbeingdecorated.Thereserveddecoratorswerethefollowing:
TypeScriptcompilerwillhonorspecialdecoratornamesandwillflowadditionalinformationintothedecoratorfactoryparametersannotatedbythesedecorators.
@type–Theserializedformofthetypeofthedecoratortarget
@returnType–Theserializedformofthereturntypeofthedecoratortargetifitisafunctiontype,undefinedotherwise
@parameterTypes–Alistofserializedtypesofthedecoratortarget'sargumentsifitisafunctiontype,undefinedotherwise
@name–Thenameofthedecoratortarget
--"Decoratorsbrainstorming"byJonathanTurner
Shortlyafter,theTypeScriptteamdecidedtousethereflectionmetadataAPI(oneoftheproposedES7features)insteadofthereserveddecorators.
Theideaisalmostidenticalbutinsteadofusingthereserveddecoratornames,wewillusesomereservedmetadatakeystoretrievethemetadatausingthereflectionmetadataAPI.TheTypeScriptdocumentationdefinesthreereservedmetadatakeys:
Typemetadatausesthemetadatakey"design:type".
Parametertypemetadatausesthemetadatakey"design:paramtypes".
Returntypemetadatausesthemetadatakey"design:returntype".
--Issue#2577-TypeScriptOfficialRepositoryatGitHub.com
Let'sseehowwecanusethereflectionmetadataAPI.Weneedtostartbyreferencingandimportingtherequiredreflect-metadatanpmpackage:
///<referencepath="./node_modules/reflect-metadata/reflect-metadata.d.ts"/>
import'reflect-metadata';
Wecanthencreateaclassfortestingpurposes.Wearegoingtogetthetypeofoneoftheclasspropertiesatruntime.WearegoingtodecoratetheclassusingapropertydecoratornamedlogType:
classDemo{
@logType
publicattr1:string;
}
Insteadofusingareserveddecorator,@type,weneedtoinvoketheReflect.getMetadata()methodandpassthedesign:typekey.Thetypesarereturnedasfunctions,forexample,forthetypestring,thefunctionString(){}functionisreturned.Wecanusethefunction.namepropertytogetthetypeasastring:
functionlogType(target:any,key:string){
vart=Reflect.getMetadata('design:type',target,key);
console.log(`${key}type:${t.name}`);
}
IfwecompiletheprecedingcodeandruntheresultingJavaScriptcodeinawebbrowser,wewillbeabletoseethetypeoftheattr1propertyintheconsole:
'attr1type:String'
Note
Rememberthat,inordertorunthisexample,thereflect-medatadalibrarymustbeimported.
Wecanapplytheotherreservedmetadatakeysinasimilarmanner.Let'screateamethodwithmanyparameterstousethedesign:paramtypesreservedmetadatakeytoretrievethetypesoftheparameters
classDemo{
@logParamTypes
publicdoSomething(
param1:string,
param2:number,
param3:Foo,
param4:{test:string},
param5:IFoo,
param6:Function,
param7:(a:number)=>void
):number{
return1;
}
}
Thistime,wewillusethedesign:paramtypesreservedmetadatakey,andbecausewearequeryingthetypesofmultipleparameters,thetypeswillbereturnedasanarraybytheReflect.getMetadata()function:
functionlogParamTypes(target:any,key:string){
vartypes=Reflect.getMetadata('design:paramtypes',target,key);
vars=types.map(a=>a.name).join();
console.log(`${key}paramtypes:${s}`);
}
Ifwecompileandruntheprecedingcodeinawebbrowser,wewillbeabletoseethetypesoftheparametersintheconsole:
'doSomethingparamtypes:String,Number,Foo,Object,Object,Function,
Function'
Thetypesareserializedandfollowsomerules.WecanseethatfunctionsareserializedasFunction,objectsliterals({test:string})andinterfacesareserializedasObject,andsoon:
Type Serialized
void undefined
string String
number Number
boolean Boolean
symbol Symbol
any Object
enum Number
ClassC{} C
Objectliteral{} Object
interface Object
Note
Notethatsomedevelopershaverequiredthepossibilityofaccessingthetypeofinterfacesandtheinheritancetreeofaclassviametadata.Thisfeatureisknownascomplextypeserializationandisnotavailableatthetimeofwritingthisbook,buttheTypeScriptteamhasalreadystartedtoworkonit.
Toconclude,wearegoingtocreateamethodwithareturntypeandusethedesign:returntypereservedmetadatakeytoretrievethetypesofthereturntype:
classDemo{
@logReturntype
publicdoSomething2():string{
return"test";
}
}
Justlikeinthetwopreviousdecorators,weneedtoinvoketheReflect.getMetadata()function,passingthedesign:returntypereservedmetadatakey:
functionlogReturntype(target,key){
varreturnType=Reflect.getMetadata('design:returntype',target,key);
console.log(`${key}returntype:${returnType.name}`);
}
Ifwecompileandruntheprecedingcodeinawebbrowser,wewillbeabletoseethetypesofthereturntypeintheconsole:
'doSomething2returntype:String'
SummaryInthischapter,youlearnedhowtoconsumeandimplementthefouravailabletypesofdecorators(class,method,property,andparameter)andhowtocreateadecoratorfactorytoabstractdevelopersfromthedecoratortypeswhentheyareconsumed.
YoualsolearnedhowtousethereflectionmetadataAPItoaccesstypeinformationatruntime.
Inthenextchapter,youwilllearnaboutthearchitectureofaTypeScriptapplication.Youwillalsolearnabouthowtoworkwithsomedesignpatternsandhowtocreateasingle-pagewebapplication.
Chapter9.ApplicationArchitectureInpreviouschapters,wehavecoveredseveralaspectsofTypeScript,andweshouldnowfeelconfidentenoughtocreateasmallapplication.
Asweknow,TypeScriptwascreatedbyMicrosofttofacilitatethecreationoflarge-scaleJavaScriptapplications.SomeTypeScriptfeaturessuchasmodulesorclassescanfacilitatetheprocessofcreatinglargeapplications,butitisnotenough.Weneedgoodapplicationarchitectureifwewanttosucceedinthelongterm.
Thischapterisdividedintotwomainparts.Inthefirstpart,wearegoingtolookatthesingle-pageapplication(SPA)architectureandsomedesignpatternsthatwillhelpuscreatescalableandmaintainableapplications.Thissectioncoversthefollowingtopics:
Thesingle-pagewebapplicationarchitectureTheMV*architectureModelsandcollectionsItemviewsandcollectionviewsControllersEventsRouterandhashnavigationMediatorClient-siderenderingandvirtualDOMDatabindinganddataflowThewebcomponentandshadowDOMChoosinganMV*framework
Inthesecondpartofthischapter,wearegoingtoputintopracticemanyofthetheoreticalconceptsexploredinthefirstpartofthischapter.Wearegoingtodevelopasingle-pagewebapplicationframework,fromscratch,whichwillbeusedtocreateanapplicationinChapter10,PuttingEverythingTogether.
Thesingle-pageapplicationarchitectureWearegoingtostartbyexploringwhatsingle-pageapplications(SPAs)areandhowtheywork.NumerousSPAframeworksareavailablethatcanhelpusdevelopapplicationswithagoodarchitecture.
Wecouldjumpdirectlyintotheuseofoneoftheseframeworks,butitisalwaysagoodthingtounderstandhowathird-partysoftwarecomponentworksbeforeweuseit.Forthisreason,wearegoingusethefirstpartofthischaptertostudytheinternalarchitectureofanSPA.Let'sstartbyunderstandingwhatanSPAis.
AnSPAisawebapplicationinwhichalltheresources(HTML,CSS,JavaScript,andsoon)areeitherloadedinonesinglerequest,orloadeddynamicallywithoutfullyreloadingthepage.Weusethetermsingle-pagetorefertothiskindofapplicationbecausethewebpageisneverfullyreloadedaftertheinitialpageload.
Inthepast,theWebwasjustacollectionofstaticHTMLfilesandhyperlinks;everytimeweclickedonahyperlink,anewpagewasloaded.Thisaffectedwebapplicationperformancenegativelybecausemanyofthecontentsofthepage(forexample,pageheaders,pagefooters,sidemenus,scripts)wereloadedagainwitheachnewpage.
WhenAJAXsupportarrivedforwebbrowsers,developersstartedtoloadsomeofthepagecontentviaAJAXrequeststoavoidunnecessarypagereloadsandprovidebetteruserexperience.AJAXapplicationsandSPAsworkinaverysimilarway.ThesignificantdifferenceisthatAJAXapplicationsloadsectionsofthewebapplicationasHTML.ThesesectionsarereadytobeappendedtotheDOMassoonastheyfinishloading.Ontheotherhand,SPAsavoidloadingtheHTML;instead,theyloaddataandclient-sidetemplates.ThetemplatesanddataareprocessedandtransformedintoHTMLinthewebbrowserinaprocessknownasclient-siderendering.ThedataisusuallyinXMLorJSONformat,andtherearemanyavailableclient-sidetemplatelanguages.
Let'scomparebothapproachesindetail.Forexample,toshowalistofclientsandordersinanHTMLtableusingtheAJAXapplicationapproach,wecouldloadtheinitialpagecontainingthelistofclientsinHTMLformat,readytobedisplayed.Inthetable,wewouldusearowforeachclient:
<tr>
<td>ClientName1</td>
<td>
<ahref="javascript:void(0);"class="orders_link"data-client-id="1">
ViewOrders
</a>
</td>
<!--morecolumns...-->
</tr>
Note
Youdon'tneedtocreatenewfoldersorfilesfornow.Thisisatheoreticalexampleandisnotmeantobeimplementedorexecuted.
WewouldalsoneedsomeJavaScriptcodetoloadtheclientordersviaAJAXwhenauserclicksontheViewOrderslink:
$(document).ready(){
//loadanddisplayclientorders
functiondisplayOrders(userId){
$.ajax({
method:"GET",
url:`/client/orders.aspx?id=${userId}`,
dataType:"html",
success:function(html){
$("#page_container").html(html);
},
error:function(e){
varmsg="<h1>Sorry,therehasbeenanerror!</h1>";
$("#page_container").html(msg);
}
});
}
//setclickevent
$('.orders_link').on('click',function(e){
varuserId=$(e.currentTarget).data("client-id");
displayOrders(userId);
});
}
Note
RefertotheHandlebars.js(http://handlebarsjs.com/)andJQueryAJAX(http://api.jquery.com/jquery.ajax/)documentationifyouneedadditionalhelptounderstandtheprecedingexample.
Theprecedingcodesnippetwaitsforthepagetofinishloadingbyusingadocument-readyeventhandler.Thenitaddsaneventhandlerforclickeventsonelementswithaclassattributeequaltoorders_link.
TheeventhandlertakestheuserIDfromthedata-client-idattributeandpassesittothedisplayOrdersfunction.ThedisplayOrdersfunctionusesanAJAXrequesttoloadthelistoforders.ThelistofordersisinHTMLformatandcanbeinsertedintotheDOMwithoutchangingitsformat.
InanSPA,theprocessisverysimilar.TheinitialHTMLpage(containingthelistofclients)isloadedjustlikeintheAJAXapplication.InSPAs,thenavigationtoanewpageisalsomanagedbyJavaScriptevents,butitisusuallymanagedbyacomponentknownasRouter.
Let'signorenavigationinSPAsfornowandfocusontheloadingandrendering.InanSPA,
wewillnotloadalistofordersinHTMLformat;wewillloaditusingtheXMLorJSONformats.IfweuseJSON,theresponsemaylooklikethefollowingone:
{
"orders":[
{
"order_id":32423234,
"currency":"EUR",
"date":"13-02-2015,
"items":[
{"product_id":13223523,"price":150.00,"quantity":2}
{"product_id":62352355,"price":50.00,"quantity":1}
]
},
{
"order_id":32423786,
"currency":"EUR",
"date":"13-02-2015,
"items":[
{"product_id":13228898,"price":60.00,"quantity":1}
]
}
]
}
WecanuseanAJAXrequestalmostidenticaltotheonethatweusedtoloadHTMLintheAJAXapplication:
functiongetOrdersData(userId:number,cb){
$.ajax({
method:"GET",
url:`/api/orders/${userId}`,
dataType:"json",
success:function(json){
cb(json);
},
error:function(e){
varmsg="<h1>Sorry,therehasbeenanerror!</h1>";
$("#page_container").html(msg);
}
});
}
Beforewecanshowthelistofordersinthewebbrowser,weneedtotransformitintoHTML.TotransformtheJSONintoHTML,wecanuseatemplatesystem.Therearemanytemplatesystems,butwearegoinguseaHandlebarstemplateforthisexample.Let'stakealookatthesyntaxofoneofthesetemplates:
{{#eachorders}}
<tr>
<td>{{order_id}}</td>
<td>{{date}}</td>
<td>
<ul>
{{#eachitems}}<li>{{product_id}}x{{quantity}}</li>{{/each}}
</ul>
</td>
</tr>
{{/each}}
TheelementsoftheHandlebarstemplatelanguagearewrappedwithdoublebrackets({{and}}).Theprecedingtemplatestartswithaneachflowcontrolstatement.Theeachstatementisusedtorepeatsomeinstructionsforeachoftheelementsinanarray.IfwetakealookattheJSONresponse,wewillbeabletoseethattheorderselementisanarray.Thetemplatewillrepeattheoperationsbetween{{#eachorders}}and{{/each}}onceforeachobjectintheordersarray.
EachrepetitioncreatesanewHTMLtablerow.TodisplaythevalueofoneoftheJSONfieldsintheHTMLoutput,wejustneedtorefertothefieldwrappedarounddoublebrackets.Forexample,whenwerenderthecellcontainingtheorderID,weuse{{order_id}}.
Note
WhenreferringtoaJSONfieldinatemplate,thefieldmustbeinthecurrentscope.Thescopecanbeexplicitlyaccessedusingthethiskeyword,forexample,{{this.order_id}}isequalto{{order_id}}.Thescopeinatemplatechangeswhenweusesomeoftheavailableflowcontrolsentences.Forexample,the{{#eachorders}}statementassignsthecurrentiteminthearraytothethiskeyword.
InordertouseaHandlebarstemplate,weneedtoloadandcompileit.WecanloadthetemplateusingaregularAJAXrequest:
functiongetOrdersTemplate(cb){
$.ajax({
method:"GET",
url:"/client/orders.hbs",
dataType:"text",
success:function(templateSource){
vartemplate=Handlebars.compile(source);
cb(template);
},
error:function(e){
varmsg="<h1>Sorry,therehasbeenanerror!</h1>";
$("#page_container").html(msg);
}
});
}
Intheprecedingexample,wehaveloadedatemplateusinganAJAXrequestandcompileditusingtheHandlebarscompilemethod.
Note
Inarealproductionwebsite,templatesareusuallyprecompiledbythecontinuousintegration
build.Thetemplatesarethenreadytobeusedwhentheyfinishloading.Precompilingthetemplatescanhelptoimprovetheapplication'sperformance.
Wehavecreatedtwofunctions:onetoloadthetemplateandcompileitandtheothertoloadtheJSONdata.ThelaststepistocreateafunctionthatputstogetherthetemplateandtheJSONdatatogeneratetheHTMLtable,whichcontainsthelistofclientorders:
functiondisplayOrders(userId){
getOrdersData(userId,function(data){
getOrdersTemplate(data,function(template){
varhtml=template(json);
$("#page_container").html(html);
});
});
}
ItmayseemlikeSPAsrequiremuchmoreworkandthattheycouldcausepoorperformancecomparedwithAJAXapplicationsbecausetherearebothmoreoperationsandrequeststobeperformedinthewebbrowser.However,thatisfarfromthereality.TounderstandthebenefitsofSPAs,weneedtounderstandwhytheywerecreatedinthefirstplace.
ThecreationofSPAswashighlyinfluencedbytwoevents:thefirstoneistheexponentialincreaseofthepopularityofmobiledevicesandtabletswithInternetaccessandpowerfulhardware.ThesecondeventistheimprovementofJavaScriptperformancethattookplaceduringthesameperiodoftime.
Asmobiledevicesgainedpopularity,companieswereforcedtodevelopamobileversionofthesameclientapplication.CompaniesstarteddevelopingwebservicestogenerateJSONandXML(insteadofHTMLpages)thatcouldbeconsumedbyeachoftheseclientapplications.Thesewebservicescouldbeusedbyallapplications,thusallowingcompaniestoreducecosts.
TheproblemwasthattheexistingAJAXapplicationscouldnottakeadvantageofthewebserviceswithoutaclient-siderenderingsystem.TemplatesystemssuchasMustache(thepredecessorofHandlebars)werereleasedforthefirsttimetosolvethisproblem.
OneofthemainadvantagesofSPAsisthatweneedanHTTPAPI.AnHTTPAPIhasmanyadvantagesoveranapplicationthatrendersHTMLpagesintheserverside.Forexample,wecanwriteunittestsforawebservicewitheasebecauseassertingdataismucheasierthanassertingsomeuserinteractionfunctionality.HTTPAPIscanbeusedbymanyclientapplications,whichcanreducecostsandopennewlinesofbusiness,suchassellingtheHTTPAPIasaproduct.
AnotherimportantadvantageofSPAsisthatbecausealotoftheworkisperformedinthewebbrowser,theserverperformsfewertasksandisabletohandleahighernumberofrequests.Client-sideperformanceisnotnegativelyaffectedbecausepersonalcomputersandmobiledeviceshavebecomereallypowerfulandJavaScriptperformancehasimprovedsignificantly
overthelastfewyears.
NetworkperformanceinSPAscanbebothbetterandworsewhencomparedtonetworkperformanceinAJAXapplications.TheresponseformattedintheHTMLformatcansometimesbeheavierthanthedatainJSONorXMLformats.
ThepricetopaywhenusingJSONorXMListhatbutwewillperformanextrawebrequesttofetchthetemplate.Wecansolvetheseproblemsbypre-compilingthetemplates,implementingcachingmechanismsandjoiningsmalltemplatefilesintolargertemplatefilestoreducethenumberofrequests.
TheMV*architectureAswehaveseen,manytasksthatweretraditionallyperformedontheserversideareperformedontheclientsideinSPAs.ThishascausedanincreaseinthesizeofJavaScriptapplicationsandtheneedforabettercodeorganization.
Asaresult,developershavestartedusinginthefrontendsomeofthedesignpatternsthathavebeenusedwithsuccessinthebackendoverthelastdecade.Amongthose,wecanhighlighttheModel-View-Controller(MVC)designpatternandsomeofitsderivativeversions,suchasModel-View-ViewModel(MVVM)andModel-View-Presenter(MVP).
DevelopersaroundtheworldstartedtosharesomeSPAframeworksthatsomehowtrytoimplementtheMVCdesignpatternbutdonotnecessarilyfollowtheMVCpatternstrictly.ThemajorityoftheseframeworksimplementModelsandViews,butsincenotallofthemimplementControllers,wereferstothisfamilyofframeworksasMV*.
Note
WewillcoverconceptssuchasMVC,Models,andViewslaterinthischapter.
Wewillnowlookatotherarchitectureprinciples,designpatterns,andcomponentscommonlypresentinMV*frameworks.
CommoncomponentsandfeaturesintheMV*frameworksWehaveseenthatsingle-pagewebapplicationsareusuallydevelopedusingafamilyofframeworksknownasMV*,andwehavecoveredthebasicsofsomecommonSPAarchitectureprinciples.
Let'sdelvefurtherintosomecomponentsandfeaturesthatarecommonlyfoundinMV*frameworks.
Note
Inthissection,wewillusesomesmallcodesnippetsfromsomeofthemostpopularMV*frameworks.Wearenotattemptingtolearnhowtouseeachoftheseframeworks,andnopreviousexperiencewithanMV*frameworkisrequired.
OurgoalshouldbetounderstandthecommoncomponentsandfeaturesofanMV*frameworkandnotfocusonaparticularframework.
ModelsAmodelisacomponentusedtostoredata.ThedataisretrievedfromanHTTPAPIanddisplayedintheview.Someframeworksincludeamodelentitythatwe,asdevelopers,mustextend.Forexample,inBackbone.js(apopularMV*framework),amodelmustextendtheBackbone.Modelclass:
classTaskModelextendsBackbone.Model{
publiccreated:number;
publiccompleted:boolean;
publictitle:string;
constructor(){
super();
}
}
Amodelinheritssomemethodsthatcanhelpusinteractwiththewebservices.Forexample,inthecaseofaBackbone.jsmodel,wecanuseamethodnamedfetchtosetthevaluesofamodelusingthedatareturnedbyawebservice.Insomeframeworks,modelsincludelogictoretrievedatafromanHTTPAPI,whileothersincludeanindependentcomponentresponsibleforthecommunicationwithanHTTPAPI.
Inotherframeworks,modelsareplainentities,anditisnotnecessarytoextendorinstantiateoneoftheframework'sclasses:
classTaskModel{
publiccreated:number;
publiccompleted:boolean;
publictitle:string;
}
CollectionsCollectionsareusedtorepresentalistofmodels.Intheprevioussection,wesawanexampleofamodelnamedTaskModel.Whilethismodelcouldbeusedtorepresentasingletaskinalistofthingstodo,acollectioncouldbeusedtorepresentthelistoftasks.
InthemajorityofMV*frameworksthatsupportcollections,weneedtospecifythemodeloftheitemsofacollectionwhenthecollectionisdeclared.Forexample,inthecaseofBackbone.js,theTaskcollectioncouldlooklikethefollowing:
classTaskCollectionextendsBackbone.Collection<TaskModel>{
publicmodel:TaskModel;
constructor(){
this.model=TodoModel;
super();
}
}
Justlikeinthecaseofmodels,someframeworks'collectionsareplainarrays,andwewillnotneedtoextendorinstantiateoneoftheframework'sclasses.Collectionscanalsoinheritsomemethodstofacilitateinteractionwithwebservices.
ItemviewsThemajorityofframeworksfeatureanitemview(orjustview)component.ViewsareresponsibleforrenderingthedatastoredinthemodelsasHTML.Viewsusuallyrequireamodel,atemplate,andacontainertobepassedasaconstructorargument,property,orsetting.
ThemodelandthetemplateareusedtogeneratetheHTML,aswediscoveredearlieroninthischapterThecontainerisusuallytheselectorofoneoftheDOMelementsinthepage;theselectedDOMelementisthenusedasacontainerfortheHTML,whichisinsertedorappendedtoit
Forexample,inMarionette.js(apopularMV*frameworkbasedonBackbone.js),aviewisdeclaredasfollows:
classNavBarItemViewextendsMarionette.ItemView{
constructor(options:any={}){
options.template="#navBarItemViewTemplate";
super(options);
}
}
CollectionviewsAcollectionviewisaspecialtypeofview.Therelationshipbetweencollectionviewsandviewsissomehowcomparablewiththerelationshipbetweencollectionsandmodels.Collectionviewsusuallyrequireacollection,anitemview,andacontainertobepassedasaconstructorargument,property,orsetting.
Acollectionloopsthroughthemodelsinthespecifiedcollection,renderseachofthemusingaspecifieditemview,andthenappendstheresultsofthecontainer.
Note
Inthemajorityofframeworks,whenacollectionviewisrendered,anitemviewisrenderedforeachiteminthecollection;thiscansometimescreateaperformancebottleneck.
Analternativesolutionistouseanitemviewandamodelinwhichoneofitsattributesisanarray.Wecanthenusethe{{#each}}statementintheviewtemplatetorenderacollectioninonesingleoperation,asopposedtooneoperationforeachiteminthecollection.
ThefollowingcodesnippetisanexampleofacollectionviewinMarionette.js:
classSampleCollectionViewextendsMarionette.CollectionView<SampleModel>{
constructor(options:any={}){
super(options);
}
}
varview=newSampleCollectionView({
collection:collection,
el:$("#divOutput"),
childView:SampleView
});
ControllersSomeframeworksfeatureControllers.Controllersareusuallyinchargeofhandlingthelifecycleofspecificmodelsandtheirassociatedviews.Theyareresponsibleforinstantiatingconnectionmodelsandcollectionswiththeirrespectiveviewsandcollectionviewsaswellasdisposingthembeforehandlingthecontrolovertoanothercontroller.
InteractioninMVCapplicationsisorganizedaroundcontrollersandactions.Controllerscanincludeasmanyactionmethodsasneeded,andanactiontypicallyhasone-to-onemappingwithuserinteractions.
WearegoingtotakealookatasmallcodesnippetthatusesanMV*frameworkknownasChaplin.JustlikeMarionette.js,ChaplinisaframeworkbasedonBackbone.js.ThefollowingcodesnippetdefinesaclassthatinheritsfromthebaseControllerclass,whichisdefinedbyChaplin:
classLikesControllerextendsChaplin.Controller{
publicbeforeAction(){
this.redirectUnlessLoggedIn();
}
publicindex(params){
this.collection=newLikes();
this.view=newLikesView({collection:this.collection});
}
publicshow(params){
this.model=newLike({id:params.id});
this.view=newFullLikeView({model:this.model});
}
}
Intheprecedingcodesnippet,wecanseethatthecontrollerisnamedLikesController,andithastwoactionsnamedindexandshowrespectively.WecanalsoobserveamethodnamedbeforeActionthatisexecutedbyChaplinbeforeanactionisinvoked.
EventsAneventisanactionoroccurrencedetectedbytheprogramthatmaybehandledbytheprogram.MV*frameworksusuallydistinguishtwokindsofevents:
Userevents:Applicationsallowuserstointeractwithitbytriggeringandhandlinguserevents,suchasclickingonabutton,scrolling,orsubmittingaform.Usereventsareusuallyhandledinaview.Applicationevents:Theapplicationcanalsotriggerandhandleevents.Forexample,someframeworkstriggeranonRendereventwhenaviewhasbeenrenderedoranonBeforeRoutingeventwhenacontrolleractionisabouttobeinvoked.
ApplicationeventsareagoodwaytoadheretotheOpen/CloseelementoftheSOLIDprinciple.Wecanuseeventstoallowdeveloperstoextendaframework(byaddingeventhandlers)withouthavingtomodifytheframeworkitself.
Applicationeventscanalsobeusedtoavoiddirectcommunicationbetweentwocomponents.WewillcovermoreaboutthemlaterinthischapterwhenwefocusonacomponentknownasMediator.
Routerandhash(#)navigationTherouterisresponsibleforobservingURLchangesandpassingtheexecutionflowtoacontroller'sactionthatmatchestheURL.
ThemajorityofframeworksuseacombinationofatechniqueknownashashnavigationandtheusageoftheHTML5HistoryAPItohandlechangesintheURLwithoutreloadingthepage.
InanSPA,thelinksusuallycontainthehash(#)character.ThischaracterwasoriginallydesignedtosetthefocusononeoftheDOMelementsonapage,butitisusedbyMV*frameworkstonavigatewithoutneedingtofullyreloadthewebpage.
Inordertounderstandthisconcept,wearegoingtoimplementareallybasicRouterfromscratch.Wearegoingtostartbytakingalookathowaroute—aplainobjectusedtorepresentaURL—looksinthemajorityofMV*frameworks:
classRoute{
publiccontrollerName:string;
publicactionName:string;
publicargs:Object[];
constructor(controllerName:string,actionName:string,args:Object[]){
this.controllerName=controllerName;
this.actionName=actionName;
this.args=args;
}
}
Therouterobservesthechangesinthewebbrowser'sURL.WhentheURLchanges,therouterparsesitandgeneratesanewrouteinstance.
Areallybasicroutercouldlookasfollows:
classRouter{
private_defaultController:string;
private_defaultAction:string;
constructor(defaultController:string,defaultAction:string){
this._defaultController=defaultController||"home";
this._defaultAction=defaultAction||"index";
}
publicinitialize(){
//observeURLchangesbyusers
$(window).on('hashchange',()=>{
varr=this.getRoute();
this.onRouteChange(r);
});
}
//EncapsulatesreadingtheURL
privategetRoute(){
varh=window.location.hash;
returnthis.parseRoute(h);
}
//EncapsulatesparsinganURL
privateparseRoute(hash:string){
varcomp,controller,action,args,i;
if(hash[hash.length-1]==="/"){
hash=hash.substring(0,hash.length-1);
}
comp=hash.replace("#",'').split('/');
controller=comp[0]||this._defaultController;
action=comp[1]||this._defaultAction;
args=[];
for(i=2;i<comp.length;i++){
args.push(comp[i]);
}
returnnewRoute(controller,action,args);
}
privateonRouteChange(route:Route){
//invokecontrollerhere!
}
}
Note
Inthesecondpartofthischapter,wearegoingtodevelopanentireSPAframeworkfromscratch,andwewilluseanextendedversionoftheprecedingclass.
Theprecedingclasstakesthenameofthedefaultconstructorandthenameofthedefaultactionasitsconstructorarguments.Thecontrollernamedhomeandtheactionnamedindexareusedasthedefaultvalueswhennoargumentsarepassedtotheconstructor.
Themethodnamedinitializeisusedtocreateaneventlistenerforthehashchangeevent.Webbrowserstriggerthiseventwhenthewindow.location.hashvaluechanges.
Forexample,let'sconsiderthecurrentURLtobehttp://localhost:8080.Auserthenclicksonthefollowinglink:
<ahref="#tasks/index">ViewTasks</a>
Whenthelinkisclicked,thewindow.location.hashvaluewillchangeto"task/index".TheURLinthebrowsernavigationpanelwillchange,butthehashcharacterwillpreventthepagefromfullyreloading.TherouterwilltheninvokeitsgetRoutemethodtotransformtheURLintoanewinstanceoftheRouteclassbyusingtheparseRoutemethod.
TheURLfollowsthefollowingnameconvection:
#conrollerName/actionName/arg1/arg2/arg3/argN
Thismeansthatthetask/indexURListransformedinto:
newRoute("task","index",[]);
Note
ThemajorityofMV*frameworksusetheHTMLHistoryAPItohidethehash(#)characterfromtheURL,butwewillnotimplementthisfeatureinourframework.
TheinstanceoftheRouteclassispassedtotheonRouteChangemethod,whichisresponsibleforinvokingthecontrollerthatmatchestheroute.
Note
WehaveomittedtheimplementationoftheonRouteChangemethodonpurposebutwillrefertothisfunctionintheMediatorandDispatchersectionslaterinthischapter.
Thisisbasicallyhowhashnavigationandrouterswork.Aswecanexpect,inarealframework,arouterhasmanyadditionalfeatures,buttheprecedingexampleshouldhelpusgainagoodunderstandingofhowroutingworksinthemajorityofMV*frameworks.
MediatorSomeMV*frameworksintroduceacomponentknownasMediator.Themediatorisasimpleobjectallothermodulesusetocommunicatewitheachother.
Themediatorusuallyimplementsthepublish/subscribedesignpattern(alsoknownaspub/sub).Thispatternenablesmodulestonotdependoneachother.Insteadofmakingdirectuseofotherpartsoftheapplication,modulescommunicatethroughevents.
Modulescanlistenforandreacttoeventsbutalsopublisheventsoftheirowntogiveothermodulesthechancetoreact.Thisensuresloosecouplingofapplicationmodules,whilestillallowingforeaseofinformationexchange.
Themediatorcanalsohelpustoallowdeveloperstoextendourframework(bysubscribingtoevents)withoutactuallyhavingtomodifytheframeworkitself.AswesawinChapter4,Object-OrientedProgrammingwithTypeScript,thisisagoodthingbecauseitadherestotheOpen/CloseprincipleintheSOLIDprinciples.
Wearegoingtoavoidtheinternaldetailsofhowamediatorworksfornow,butwecantakealookatanexampleofthepublicinterfaceofamediator:
interfaceIMediator{
publish(e:IAppEvent):void;
subscribe(e:IAppEvent):void;
unsubscribe(e:IAppEvent):void;
}
Intheprevioussection,weomittedthedetailsabouthowtherouterinvokesacontrollerbecausetheframeworkthatwearegoingtodevelopwilluseamediator:
classRouter{
//...
privateonRouteChange(route:Route){
this.meditor.publish(newAppEvent("app.dispatch",route,null));
}
}
Theprecedingcodesnippetshowcaseshowtherouteravoidsinvokingthecontroller'sactiondirectly,andinstead,itpublishesaneventusingamediator.
DispatcherTherewassomethinginthepreviouscodesnippetthatmayhavecaughtyourattention:theeventnameisapp.dispatch.
Theapp.dispatcheventreferstoanentityknownasDispatcher.Thismeansthattherouterissendinganeventtothedispatcherandnottoacontroller:
classDispatcher{
//...
publicinitialize(){
this.meditor.subscribe(
newAppEvent("app.dispatch",null,(e:any,data?:any)=>{
this.dispatch(data);
})
);
}
//Createanddisposecontrollerinstances
privatedispatch(route:IRoute){
//1.Disposepreviouscontroller
//2.Createinstanceofnewcontroller
//3.InvokecontrolleractionusingMediator
}
//...
}
Aswecanseeinthiscodesnippet,thedispatcherisresponsibleforthecreationofnewcontrollersandthedisposalofoldcontrollers.WhenarouterfinishesparsingaURL,itwillpassaninstanceoftheRouteclasstothedispatcherusingamediator.Thedispatcherthendisposesthepreviouscontrollercreatesaninstanceofthenewcontroller,andinvokesthecontrolleractionusingamediator.
Client-siderenderingandVirtualDOMWearealreadyfamiliarwiththebasicsofclient-siderendering.Weknowclient-siderenderingrequiresatemplateandsomedatatogenerateHTMLasoutput,butwehaven'tmentionedsomeperformancedetailsthatweneedtoconsiderwhenselectinganMV*framework.
ManipulatingtheDOMisoneofthemainpotentialperformancebottlenecksinSPAs.Forthisreason,itisinterestingtocomparehowframeworksrendertheviewsinternallybeforewedecidetoworkwithoneoranother.
Someframeworksrenderaviewwheneverthemodelchanges,andtherearetwopossiblewaystoknowwhenamodelhaschanged:
Thefirstoneistocheckforchangesusinganinterval(thisoperationissometimesreferredasadirtycheck)Thesecondoptionistouseanobservablemodel
Theobservableapproachismuchmoreefficientthanusingatimeintervalbecausetheobservablemodelwillonlyconsumeprocessingtimewhenithasactuallychanged.Ontheotherhand,theintervalwillconsumeprocessingtimeevenwhenthemodelhasnotchanged.
Whentorenderisimportant,butwealsoneedtoconsiderhowtorender.SomeframeworksmanipulatetheDOMdirectlyandothersuseanin-memoryrepresentationoftheDOMknownasVirtualDOM.VirtualDOMismuchmoreefficientbecauseJavaScriptisabletomanipulatethein-memoryrepresentationoftheDOMmuchfasterthantheDOMitself.
UserinterfacedatabindingUserinterface(UI)databindingisadesignpatternthataimstosimplifydevelopmentofgraphicUIapplications.UIdatabindingbindsUIelementstoanapplicationdomainmodel.
Abindingcreatesalinkbetweentwopropertiessuchthatwhenonechanges,theotheroneisupdatedtothenewvalueautomatically.Bindingscanconnectpropertiesonthesameobject,oracrosstwodifferentobjects.MostMV*frameworksincludesomesortofbindingimplementationbetweenviewsandmodels.
One-waydatabinding
One-waydatabindingisatypeofUIdatabinding.Thistypeofdatabindingonlypropagateschangesinonedirection.
InthemajorityofMV*frameworks,thismeansthatanychangesinthemodelarepropagatedtotheview.Ontheotherhand,anychangesintheviewarenotpropagatedtothemodel.
Two-waydatabinding
Two-waybindingisusedtoensurethatanychangestotheviewarepropagatedtothemodelandanychangesinthemodelarepropagatedtotheview.
DataflowSomeofthelatestMV*frameworkshaveintroducednewapproachesandtechniques.Oneofthesenewconceptsistheunidirectionaldataflowarchitecture(introducedbyFlux).
Thisunidirectionaldataflowarchitectureisbasedontheideathatchangingthevalueofavariableshouldautomaticallyforcerecalculationofthevaluesofvariablesthatdependonitsvalue.
InanMVCapplication,acontrollerhandlesmultipleModelsandViews.Sometimes,aViewusesmorethanonemodel,andwhentwo-waydatabindingisused,wecanendupwithacomplicatedflowofdatatofollow.Thefollowingdiagramillustratessuchascenario:
Note
Inthisdiagram,actiondoesnotrefertotheactionsinacontroller.Actionherereferstouserorapplicationevents.
Dataflowarchitectureattemptstosolvethisproblembyrestrictingtheflowofdatatooneuniquechannelanddirection.Bydoingso,theflowofdatawithintheapplicationcomponentsbecomesmucheasiertofollow.Thefollowingdiagramillustratestheflowofdatainanapplicationthatusesunidirectionaldataflowarchitecture:
Theprecedingdiagramillustrateshowthedataalwaysmovesinthesamedirection.
InFlux'sunidirectionaldataflowarchitecture,alltheactionsaredirectedtothedispatcher.ThedispatcherinFluxislikethedispatcherinourframework,butinsteadofpassingtheexecutionflowtoacontroller,itpassestheexecutionflowtoastore.
StoresareinchargeofretrievingandmanipulatingdataandcanbecomparedwithModelsinMVC.Oncethedatahasbeenmodifiedinsomeway,itispassedtotheviews.
Views,justlikeinMVC,areresponsibleforrenderingthedataasHTMLandhandlinguserevents(actions).Iftheeventrequiressomedatatobemodifiedinsomeway,theViewswillsendanactiontothedispatcherinsteadofmanipulatingitsmodel,aswouldhappeninanapplicationwithtwo-waydatabindingsupport.
Thedataalwaysmovesinthesamedirectionandincircles,whichmakestheexecutionflowofalargedataflowapplicationmucheasiertodebugandpredictthanthatofatwo-waydatabindingMVCapplication.
WebcomponentsandshadowDOMSomeframeworksusethetermwebcomponenttorefertoreusableuserinterfacewidgets.WebcomponentsallowdeveloperstodefinecustomHTMLelements.Forexample,wecoulddefineanewHTML<map>tagtodisplayamap.Webcomponentscanimporttheirowndependenciesanduseclient-sidetemplatestorendertheirownHTMLusingatechnologyknownasshadowDOM.
ShadowDOMallowsthebrowsertouseHTML,CSS,andJavaScriptwithinawebcomponent.ShadowDOMisusefulwhendevelopinglargeapplicationsbecauseithelpstopreventCSS,HTML,andJavaScriptconflictsbetweencomponents.
Note
SomeoftheexistingMV*frameworks(forexample,Polymer)canbeusedtoimplementrealwebcomponents.Whileotherframeworks(forexample,React)usethetermwebcomponentstorefertoreusableuserinterfacewidgets,thosecomponentscannotbeconsideredrealwebcomponentsbecausetheydon'tusethewebcomponentstechnologystack(customelements,HTMLtemplates,shadowDOMandHTMLimports).
ChoosinganapplicationframeworkWecancreateaSPAfromscratch,butusuallywepickupanexistingframeworkbeforecreatingourown.OneofthemainproblemsofchoosingaJavaScriptSPAframeworkisthattherearetoomanychoices.
ThelatestandgreatestJavaScriptframeworkcomesaroundeverysixteenminutes.
--AllenPike
Iwouldpersonallyrecommendconsideringaframeworkoranotherdependingonthefeaturesthatyouthinkthatyouwillneedtoachieveyourgoals.
Forexample,ifwearegoingtoworkonanapplicationwithnotreallycomplexviewsandforms,Backbone.jsoroneofitsderivations(Marionette.js,Chaplin,andsoon)shouldworkforus.However,ifourapplicationisexpectedtohavemanyformsandcomplexviews,Ember.jsorAngularJSmightbeabetteroption.
Note
Ifyouneedsomeextrahelpwhenchoosingoneframeworkoveranother,youshouldvisithttp://todomvc.com.TodoMVCisaprojectthatoffersthesameapplication(ataskmanager)implementedusingMV*conceptsinmostofthepopularJavaScriptMV*frameworkstoday.
WritinganMVCframeworkfromscratchNowthatwehaveagoodideaaboutthecommoncomponentsofanMV*applicationframework,wearegoingtotrytoimplementourownframeworkfromscratch.
Note
Theframeworkthatweareabouttodevelophasnotbeendesignedtobeusedinarealprofessionalenvironment.RealMV*frameworkshavethousandsoffeaturesandhavebeenunderintensedevelopmentformonthsandevenyearsbeforebecomingstable.
ThisframeworkhasbeendevelopednottobethemostefficientorthemostmaintainableMV*frameworkavailable,buttobeagoodlearningresource.
Ourapplicationwillfeaturecontrollers,templates,views,andmodelsaswellasarouter,amediator,andadispatcher.Let'stakealookattheroleofeachofthesecomponentsinourframework:
Application:Thisistherootcomponentofanapplication.Theapplicationcomponentisinchargeoftheinitializationofalltheinternalcomponentsoftheframework(mediator,router,anddispatcher).Mediator:Themediatorisinchargeofthecommunicationbetweenalltheothercomponentsintheapplication.ApplicationEvents:Applicationeventsareusedtosendinformationfromonecomponenttoanother.Anapplicationeventisidentifiedbyanidentifierknownasatopic.Thecomponentscanpublishapplicationeventsaswellassubscribeandunsubscribetoapplicationevents.Router:TherouterobservesthechangesinthebrowserURLandcreatesinstancesoftheRouteclassthatarethensenttotheDispatcherusinganapplicationevent.Routes:TheseareusedtorepresentaURL.TheURLsusenamingconventionsthatcanbeusedtoidentifywhichcontrollerandactionshouldbeinvoked.Dispatcher:ThedispatcherreceivesinstancesoftheRouteclass,whichareusedtoidentifytherequiredcontroller.Thedispatchercanthendisposethepreviouscontrollerandcreateanewcontrollerinstanceifnecessary.Oncethecontrollerhasbeeninitialized,thedispatcherpassestheexecutionflowtothecontrollerusinganapplicationevent.Controllers:Controllersareusedtoinitializeviewsandmodels.Oncetheviewsandmodelsareinitialized,thecontrollerpassestheexecutionflowtooneormoremodelsusinganapplicationevent.Models:ModelsareinchargeoftheinteractionwiththeHTTPAPIaswellasdatamanipulationinmemory.Thisinvolvesdataformattingaswellasoperationssuchastheadditionordeletionofdata.OncetheModelhasfinishedmanipulatingthedata,itispassedtooneormoreviewsusinganapplicationevent.Views:Viewsareinchargeoftheloadandcompilationoftemplates.Oncethetemplatehasbeenloaded,theviewswaitfordatatobesentbythemodels.Whenthedataisreceived,itiscombinedwiththetemplatestogenerateHTMLcode,whichisappendedto
theDOM.ViewsarealsoinchargeofthebindingandunbindingofUIevents(click,focus,andsoon).
Thefollowingdiagramcanhelpustounderstandtheinteractionbetweentheavailablecomponents:
Nowthatwehaveabasicideaabouttheoverallarchitectureofourframework,let'sstartanewproject.
PrerequisitesJustlikewehavebeendoinginthepreviouschaptersofthisbook,itisrecommendedtocreateanewprojectandconfigureanautomateddevelopmentworkflowusingGulp.
Youcantrytocreatetheframeworkandfinalapplicationfollowingthestepsdescribedinthefollowingsections,oryoucandownloadthecompanionsourcecodetogetacopyofthefinishedapplication.
Wearegoingtostartbyinstallingthefollowingruntimedependencieswithnpm:
npminit
npminstallanimate.cssbootstrapdatatableshandlebarsjqueryq--save
Wealsoneedtoinstallthefollowingdevelopmentdependencies:
npmbrowser-syncbrowserifychaigulpgulp-coverallsgulp-tslintgulp-
typescriptgulp-uglifykarmakarma-chaikarma-mochakarma-sinonmocharun-
sequencesinonvinyl-buffervinyl-source-stream--save-dev
Now,let'sinstalltherequiredtypedefinitionfilesusingtsd:
tsdinit
tsdinstalljquerybootstraphandlebarsqchaisinonmochajquery.dataTables
highcharts--save
Theapplicationusesthefollowingdirectorytree:
├──LICENSE
├──README.md
├──css
│└──site.css
├──data
│├──nasdaq.json
│└──nyse.json
├──gulpfile.js
├──index.html
├──karma.conf.js
├──node_modules
├──package.json
├──source
│├──app
││└──//Chapter10
│└──framework
│├──app.ts
│├──app_event.ts
│├──controller.ts
│├──dispatcher.ts
│├──event_emitter.ts
│├──framework.ts
│├──interfaces.ts
│├──mediator.ts
│├──model.ts
│├──route.ts
│├──router.ts
│├──tsconfig.json
│└──view.ts
├──test
├──tsd.json
└──typings
Wewillbeworkingonthefileslocatedunderthesourcefolderduringthischapter.Inthenextchapter,wewillcreateanapplicationusingourframework.Mostofthefilesofthisapplicationwillbelocatedundertheappfolder.
Nowthatwehaveabasicideaabouttheoverallarchitectureofourframework,let'sproceedtoimplementeachofitscomponents.
Note
Thefinalversionoftheentireframeworkandapplicationisincludedinthecompanionsourcecode.
ApplicationeventsWearegoingtouseapplicationeventsthatallowthecommunicationbetweentwocomponents.Forexample,whenamodelfinishesreceivingtheresponseofanHTTPAPI,theresponseoftherequestwillbesentfromthemodeltoaviewusinganapplicationevent.
AswesawinChapter4,Object-OrientedProgrammingwithTypeScript,oneoftheSOLIDprinciplesisthedependencyinversionprinciple,whichstatesthatweshouldnotdependuponconcretions(classes)andshoulddependuponabstractionsinstead(interfaces).WearegoingtotrytofollowtheSOLIDprinciples,solet'sgetstartedbycreatinganewfilenamedinterfaces.tsinsidetheframeworkfolderanddeclaringtheIAppEventinterface:
interfaceIAppEvent{
topic:string;
data:any;
handler:(e:any,data:any)=>void;
}
Anapplicationeventcontainsanidentifierortopicandsomedataoraneventhandler.Wewillunderstandthesepropertiesbetteroncewegettopublishandsubscribetosomeevents.
Let'scontinuebycreatinganewfilenamedapp_event.tsinsidetheframeworkfolderandcopythefollowingcodeintoit:
///<referencepath="./interfaces"/>
classAppEventimplementsIAppEvent{
publicguid:string;
publictopic:string;
publicdata:any;
publichandler:(e:Object,data?:any)=>void;
constructor(topic:string,data:any,handler:(e:any,data?:any)=>
void){
this.topic=topic;
this.data=data;
this.handler=handler;
}
}
export{AppEvent};
TheprecedingcodesnippetdeclaresaclassnamedAppEventwhichimplementstheIappEventinterface.
MediatorAswealreadyknow,themediatorisacomponentthatimplementsthepub/subdesignpatternandisusedtoavoidthedirectcommunicationbetweentwocomponents.
Let'saddanewinterfacetotheinterfaces.tsfile:
interfaceIMediator{
publish(e:IAppEvent):void;
subscribe(e:IAppEvent):void;
unsubscribe(e:IAppEvent):void;
}
Aswecanseeinthiscodesnippet,theIMediatorinterfaceexposesthethreemethodsnecessarytoimplementthepublish/subscribedesignpattern,asfollows:
publish:Thisisusedtotriggerevents.Whenwepublishanevent,alltheeventsubscribersreceiveit.subscribe:Thisisusedtosubscribetoanevent,orinotherwords,setaneventhandlerforanevent.unsubscribe:Thisisusedtounsubscribetoanevent,orinotherwords,removeaneventhandlerforaneventtype.
Now,let'sproceedtocreateanewfilenamedmediator.tsundertheframeworkfolderandaddthefollowingcodetoit:
///<referencepath="./interfaces"/>
classMediatorimplementsIMediator{
private_$:JQuery;
private_isDebug;
constructor(isDebug:boolean=false){
this._$=$({});
this._isDebug=isDebug;
}
publicpublish(e:IAppEvent):void{
if(this._isDebug===true){console.log(newDate().getTime(),"PUBLISH",
e.topic,e.data);}
this._$.trigger(e.topic,e.data);
}
publicsubscribe(e:IAppEvent):void{
if(this._isDebug===true){console.log(newDate().getTime(),"SUBSCRIBE",
e.topic,e.handler);}
this._$.on(e.topic,e.handler);
}
publicunsubscribe(e:IAppEvent):void{
if(this._isDebug===true){console.log(newDate().getTime(),
"UNSUBSCRIBE",e.topic,e.data);}
this._$.off(e.topic);
}
}
export{Mediator};
TheprecedingcodesnippetdeclaresaclassnamedMediator,whichimplementstheIMediatorinterface.TheMediatorconstructorhasadefault(false)parameterthatisusedtoindicateifweareusingthedebugmode.
Thedebugmodeisusefulbecausewhenitisenabled,wewillbeabletoobserveallthecallstothepublish,subscribe,andunsubscribemethodsofthemediatorwithouttheneedtouseadebugger.Inthefollowingscreenshot,wecanobservethekindofinformationthatwecanexpecttoseeinthebrowserconsolewhenthedebugmodeisenabled:
Thepublish,subscribe,andunsubscribemethodsusethejQuerytrigger,on,andoffmethodsrespectivelytoexecuteeventlistenersaswellascreateandremoveeventlistenerswhenrequested.
Thedefaultconstructoralsoinitializesaprivatepropertynamed_$.ThevalueofthispropertyisjustanemptyjQueryobjectinmemory.ThisobjectisusedbyjQuerytoaddandremoveeventhandlerswhenthetrigger,on,andoffmethodareinvoked.
Itimportanttomentionthatifthemediatorisclearedfrommemory,its_$propertywillalsobeclearedfrommemoryandalltheapplicationeventhandlerswillbelost.Inthefollowingsection,wewillseehowtheAppclassensuresthatthemediatorisneverclearedfrommemory.
ApplicationTheapplicationclassistherootcomponentofanapplication.Theapplicationclassisinchargeoftheinitializationofthemaincomponentsofanapplication(router,mediator,anddispatcher).
Wearegoingtostartbydeclaringacoupleofinterfacesrequiredbytheapplicationclass,solet'saddthefollowinginterfacestotheinterfaces.tdfile:
interfaceIAppSettings{
isDebug:boolean,
defaultController:string;
defaultAction:string;
controllers:Array<IControllerDetails>;
onErrorHandler:(o:Object)=>void;
}
interfaceIControllerDetails{
controllerName:string;
controller:{new(...args:any[]):IController;};
}
TheIAppSettingsinterfaceisusedtoindicatetheavailableapplicationsettings.Wecanusetheapplicationsettingstoenablethedebugmode,setthenameofthedefaultcontrollerandaction,settheavailablecontrollers,andsetaglobalerrorhandler.Let'stakealookattheactualimplementationoftheapplicationclass:
///<referencepath="./interfaces"/>
import{Dispatcher}from"./dispatcher";
import{Mediator}from"./mediator";
import{AppEvent}from"./app_event";
import{Router}from"./router";
classApp{
private_dispatcher:IDispatcher;
private_mediator:IMediator;
private_router:IRouter;
private_controllers:IControllerDetails[];
private_onErrorHandler:(o:Object)=>void;
constructor(appSettings:IAppSettings){
this._controllers=appSettings.controllers;
this._mediator=newMediator(appSettings.isDebug||false);
this._router=newRouter(this._mediator,appSettings.defaultController,
appSettings.defaultAction);
this._dispatcher=newDispatcher(this._mediator,this._controllers);
this._onErrorHandler=appSettings.onErrorHandler;
}
publicinitialize(){
this._router.initialize();
this._dispatcher.initialize();
this._mediator.subscribe(newAppEvent("app.error",null,(e:any,data?:
any)=>{
this._onErrorHandler(data);
}));
this._mediator.publish(newAppEvent("app.initialize",null,null));
}
}
export{App};
TheprecedingcodesnippetdeclaresaclassnamedAppthattakestheimplementationofIAppSettingsasitsonlyconstructorargument.Theclassconstructorinitializestheclassproperties(dispatcher,mediator,router,controllerandglobalerrorhandler).
Whenwecreateanewapplication,itautomaticallycreatesanewmediator,anditispassedtoboththerouterandthedispatcher.Thismeansthatoneuniqueinstanceofthemediatorissharedbyallthecomponentsintheapplication,orinotherwords,themediatorisasingleton:itstaysinmemoryfortheentireapplicationlifecycle.
AftercreatinganinstanceoftheAppclass,wemustinvoketheinitializemethodtostarttheexecutionoftheapplication.Wewilllaterseethatwhentherouterisinitialized,itusesthemediatortosubscribetotheapp.initializeevent.
Theinitializemethodcallstheinitializemethodofsomeoftheapplicationcomponents(routeranddispatcher).Itthensetsaneventhandlerforglobalerrorsandpublishestheapp.initializeevent.
Themediatortheninvokestheeventhandlerfortheapp.initializeeventbytherouter.Thisexplainshowtheexecutionflowispassedfromtheapplicationclasstotherouterclass.
RouteInordertobeabletounderstandtheimplementationoftherouterclass,weneedtolearnaboutsomeofitsdependenciesfirst.ThefirstofthesedependenciesistheRouteclass.
TheRouteclassimplementstheRouteinterface.Thisinterfacewaspreviouslyexplainedinthischapter,sowewillnotgointoitsdetailsagain.
interfaceIRoute{
controllerName:string;
actionName:string;
args:Object[];
serialize():string;
}
WehavealsoincludedtheimplementationoftheRouteclasspreviouslyinthischapter,butthemethodnamedserializewasomittedonpurpose.TheserializemethodtransformsaninstanceoftheRouteclassintoaURL.
///<referencepath="./interfaces"/>
classRouteimplementsIRoute{
publiccontrollerName:string;
publicactionName:string;
publicargs:Object[];
constructor(controllerName:string,actionName:string,args:Object[]){
this.controllerName=controllerName;
this.actionName=actionName;
this.args=args;
}
publicserialize():string{
vars,sargs;
sargs=this.args.map(a=>a.toString()).join("/");
s=`${this.controllerName}/${this.actionName}/${sargs}`;
returns;
}
}
export{Route};
EventemitterTherouteralsohasadependencyintheEventEmitterclass.Thisclassisparticularlyimportantbecauseeverysinglecomponent(excepttheapplicationcomponent)intheentireframeworkextendsit.
Aswealreadyknow,allthecomponentsuseamediatortocommunicatewitheachother.Themediatorisasingleton,whichmeansthateverysinglecomponentinourapplicationneedstobeprovidedwithaccesstothemediatorinstance.
TheEventEmitterclassisusedtoreducetheamountofboilerplatecodethatisnecessarytoachievethisandtoprovidedeveloperswithsomehelpersthatfacilitatethepublicationandsubscriptionofmultipleapplicationevents:
interfaceIEventEmitter{
triggerEvent(event:IAppEvent);
subscribeToEvents(events:Array<IAppEvent>);
unsubscribeToEvents(events:Array<IAppEvent>);
}
Now,let'screateafilenamedevent_emitter.tsundertheframeworkdirectoryandcopythefollowingcodeintoit:
///<referencepath="./interfaces"/>
import{AppEvent}from"./app_event";
classEventEmitterimplementsIEventEmitter{
protected_metiator:IMediator;
protected_events:Array<IAppEvent>;
constructor(metiator:IMediator){
this._metiator=metiator;
}
publictriggerEvent(event:IAppEvent){
this._metiator.publish(event);
}
publicsubscribeToEvents(events:Array<IAppEvent>){
this._events=events;
for(vari=0;i<this._events.length;i++){
this._metiator.subscribe(this._events[i]);
}
}
publicunsubscribeToEvents(){
for(vari=0;i<this._events.length;i++){
this._metiator.unsubscribe(this._events[i]);
}
}
}
export{EventEmitter};
WhenthesubscribeToEventsmethodisinvoked,the_eventspropertyisusedtostoretheeventstowhichacomponentissubscribed.
WhenacomponentdecidestoremoveitseventhandlersbyusingtheunsubscribeToEventsmethod,wedon'tneedtopassthefulllistofeventsagainbecausetheeventemitterusestheeventspropertytorememberthem.
RouterTherouterobservestheURLforchangesandgeneratesinstancesoftheRouteclassthatarethenpassedtothedispatcherusinganapplicationevent.TheRouterclassimplementstheIRouterinterface:
interfaceIRouterextendsIEventEmitter{
initialize():void;
}
Let'stakealookattheinternalimplementationoftheRouterclass:
///<referencepath="./interfaces"/>
import{EventEmitter}from"./event_emitter";
import{AppEvent}from"./app_event";
import{Route}from"./route";
classRouterextendsEventEmitterimplementsIRouter{
private_defaultController:string;
private_defaultAction:string;
constructor(metiator:IMediator,defaultController:string,defaultAction:
string){
super(metiator);
this._defaultController=defaultController||"home";
this._defaultAction=defaultAction||"index";
}
publicinitialize(){
//observeURLchangesbyusers
$(window).on('hashchange',()=>{
varr=this.getRoute();
this.onRouteChange(r);
});
//beabletotriggerURLchanges
this.subscribeToEvents([
//usedtotriggerroutingonappstart
newAppEvent("app.initialize",null,(e:any,data?:any)=>{
this.onRouteChange(this.getRoute());
}),
//usedtotriggerURLchangesfromothercomponents
newAppEvent("app.route",null,(e:any,data?:any)=>{
this.setRoute(data);}),
]);
}
//EncapsulatesreadingtheURL
privategetRoute(){
varh=window.location.hash;
returnthis.parseRoute(h);
}
//EncapsulateswrittingtheURL
privatesetRoute(route:Route){
vars=route.serialize();
window.location.hash=s;
}
//EncapsulatesparsinganURL
privateparseRoute(hash:string){
varcomp,controller,action,args,i;
if(hash[hash.length-1]==="/"){
hash=hash.substring(0,hash.length-1);
}
comp=hash.replace("#",'').split('/');
controller=comp[0]||this._defaultController;
action=comp[1]||this._defaultAction;
args=[];
for(i=2;i<comp.length;i++){
args.push(comp[i]);
}
returnnewRoute(controller,action,args);
}
//PasscontroltotheDispatcherviatheMediator
privateonRouteChange(route:Route){
this.triggerEvent(newAppEvent("app.dispatch",route,null));
}
}
export{Router};
Wehaveseenthisclasspreviouslyinthischapter,buttherearesomesignificantdifferenceshere.ThistimetheRouteclassextendstheEventEmitterclasstakesamediatorandthenamesofthedefaultcontrolleranddefaultactionasitsconstructorarguments.
TheinitializemethodnowincludesacalltothesubscribeToEventsmethod,whichisusedtoaddanapplicationeventhandlerfortheapp.initializeevent.ThiseventisusedtoensurethattherouterparsestheURLwhentheapplicationlaunchesforthefirsttime.TherouterobservestheURLforchanges,butwhentheapplicationislaunchedforthefirsttime,therearenochangesintheURL,andtheapplicationdoesnotinvokeanycontroller.Therouterusestheapp.initializeeventhandlertosolvethisproblem.
Therouterisalsosubscribedtotheapp.routeevent.TheeventhandlerofthiseventusesamethodnamedsetRoutetosetthebrowser'sURL.Theapp.routeapplicationeventisusedtoallowothercomponentstonavigatetoaroute.
Finally,wecanfindthemethodnamedparseRoute,whichisusedtotransformaURLintoaninstanceoftheRouteclass,andtheonRouteChangemethod,whichisusedtopublishanapp.dispatchapplicationevent.
DispatcherThedispatcherisacomponentusedtocreateanddisposecontrollerswhenneeded.Disposingcontrollersisimportantbecauseacontrollercanusealargenumberofmodelsandviews,whichcanconsumeaconsiderableamountofmemory.
Ifwehavemanycontrollers,theamountofmemoryconsumedcouldbecomeaperformanceissue.Oneofthemaingoalsofthedispatcheristopreventthispotentialissue.
ThedispatcherimplementstheIDispatcherandIEventEmitterinterfaces:
interfaceIDispatcherextendsIEventEmitter{
initialize():void;
}
Let'stakealookattheimplementationofthedispatcherclass:
///<referencepath="./interfaces"/>
import{EventEmitter}from"./event_emitter";
import{AppEvent}from"./app_event";
classDispatcherextendsEventEmitterimplementsIDispatcher{
private_controllersHashMap:Object;
private_currentController:IController;
private_currentControllerName:string;
constructor(metiator:IMediator,controllers:IControllerDetails[]){
super(metiator);
this._controllersHashMap=this.getController(controllers);
this._currentController=null;
this._currentControllerName=null;
}
Weshouldbestartingtobecomefamiliarwithhowthemediatorworksatthispoint.EverycomponentinheritsfromtheEventEmitterclassandusesitsmethodstosubscribetosomeeventsinthemethodnamedinitialize.
Laterinthischapter,wewillbeabletoobservethatsomeclasses(Controllers,Views,andModels)alsohaveamethodnameddispose,whichisusedtounsubscribetothemethodstowhichthecomponentsubscribedintheinitializemethod.
//listentoapp.dispatchevents
publicinitialize(){
this.subscribeToEvents([
newAppEvent("app.dispatch",null,(e:any,data?:any)=>{
this.dispatch(data);
})
]);
}
ThishashmapisusedtobeabletofindacontrollerasfastaspossiblewhenanewrouteneedstobedispatchedThefollowingmethodisusedtogenerateahashmapthatusesthecontrollernameasthekeyandthecontrollerconstructorasvalues:
privategetController(controllers:IControllerDetails[]):Object{
varhashMap,hashMapEntry,name,controller,l;
hashMap={};
l=controllers.length;
if(l<=0){
this.triggerEvent(newAppEvent(
"app.error",
"Cannotcreateanapplicationwithoutatleastonecontoller.",
null));
}
for(vari=0;i<l;i++){
controller=controllers[i];
name=controller.controllerName;
hashMapEntry=hashMap[name];
if(hashMapEntry!==null&&hashMapEntry!==undefined){
this.triggerEvent(newAppEvent(
"app.error",
"Twocontrollercannotusethesamename.",
null));
}
hashMap[name]=controller.controller;
}
returnhashMap;
}
Thefollowingmethodisresponsibleforthecreation,initialization,anddisposalofcontrollerinstances;thecodeiscommentedtofacilitateitsunderstanding:
privatedispatch(route:IRoute){
varController=this._controllersHashMap[route.controllerName];
//trytofindcontroller
if(Controller===null||Controller===undefined){
this.triggerEvent(newAppEvent(
"app.error",
`Controllernotfound:${route.controllerName}`,
null));
}
else{
//createacontrollerinstance
varcontroller:IController=newController(this._metiator);
//actionisnotavailable
vara=controller[route.actionName];
if(a===null||a===undefined){
this.triggerEvent(newAppEvent(
"app.error",
`Actionnotfoundincontroller:${route.controllerName}-+
${route.actionName}`,
null));
}
//actionisavailable
else{
if(this._currentController==null){
//initializecontroller
this._currentControllerName=route.controllerName;
this._currentController=controller;
this._currentController.initialize();
}
else{
//disposepreviouscontrollerifnotneeded
if(this._currentControllerName!==route.controllerName){
this._currentController.dispose();
this._currentControllerName=route.controllerName;
this._currentController=controller;
this._currentController.initialize();
}
}
//passflowfromdispatchertothecontroller
this.triggerEvent(newAppEvent(
`app.controller.${this._currentControllerName}.${route.actionName}`,
route.args,
null
));
}
}
}
}
export{Dispatcher};
Afterdisposingthepreviouscontroller(ifnecessary)andcreatinganewcontroller,thiscontrollerisinitialized.Whenacontrollerisinitialized,itsinitializemethodisinvoked,andasweknow,itisthenthatacomponentsubscribestosomeevents.
Whenthedispatcherpublishesthefollowingapplicationevent,thecontrollerisalreadysubscribedtoitandtheexecutionflowispassedtothecontroller'seventhandler:
`app.controller.${this._currentControllerName}.${route.actionName}`
ControllerControllersareinchargeoftheinitializationanddisposalofviewsandmodels.Sincecontrollersmustbedisposablebythedispatcher,acontrollermustimplementthedisposemethodfromtheIControllerinterface:
interfaceIControllerextendsIEventEmitter{
initialize():void;
dispose():void;
}
ThemodelsandviewsaresetaspropertiesoftheclassesthatextendtheControllerclass.TheControllerclassitselfdoesnotprovideuswithanyfunctionality,asitismeanttobeimplementedbydeveloperswhenworkingonanapplication.
///<referencepath="./interfaces"/>
import{EventEmitter}from"./event_emitter";
import{AppEvent}from"./app_event";
classControllerextendsEventEmitterimplementsIController{
constructor(metiator:IMediator){
super(metiator);
}
publicinitialize():void{
thrownewError('Controller.prototype.initialize()isabstractyoumust
implementit!');
}
publicdispose():void{
thrownewError('Controller.prototype.dispose()isabstractyoumust
implementit!');
}
}
export{Controller};
Eventhoughitisnotforcedbytheframework,itisrecommendedyouusethemediatortopassthecontroltooneofthemodels(notviews)fromthecontroller.
ModelandmodelsettingsModelsareusedtointeractwithawebserviceandtransformthedatareturnedbyit.Modelsallowustoread,format,update,ordeletethedatareturnedbyawebservice.ModelsimplementtheIModelandIEventEmitterinterfaces:
interfaceIModelextendsIEventEmitter{
initialize():void;
dispose():void;
}
AmodelneedstobeprovidedwiththeURLofthewebservicethatitconsumes.WearegoingtouseaclassdecoratornamedModelSettingstosettheURLoftheservicetobeconsumed.
WecouldinjecttheserviceURLviaitsconstructor,butitisconsideredabadpracticetoinjectdata(asopposedtoabehavior)viaaclassconstructor.Thedecoratorincludessomecommentstofacilitateitsunderstanding:
///<referencepath="./interfaces"/>
import{EventEmitter}from"./event_emitter";
functionModelSettings(serviceUrl:string){
returnfunction(target:any){
//saveareferencetotheoriginalconstructor
varoriginal=target;
//autilityfunctiontogenerateinstancesofaclass
functionconstruct(constructor,args){
varc:any=function(){
returnconstructor.apply(this,args);
}
c.prototype=constructor.prototype;
varinstance=newc();
instance._serviceUrl=serviceUrl;
returninstance;
}
//thenewconstructorbehaviour
varf:any=function(...args){
returnconstruct(original,args);
}
//copyprototypesointanceofoperatorstillworks
f.prototype=original.prototype;
//returnnewconstructor(willoverrideoriginal)
returnf;
}
}
Inthenextchapter,wewillbeabletoapplythedecoratorasfollows:
@ModelSettings("./data/nasdaq.json")
classNasdaqModelextendsModelimplementsIModel{
//...
Let'stakealookattheinternalimplementationoftheModelclass:
classModelextendsEventEmitterimplementsIModel{
//thevaluesof_serviceUrlmustbesetusingtheModelSettingsdecorator
private_serviceUrl:string;
constructor(metiator:IMediator){
super(metiator);
}
//mustbeimplementedbyderivedclasses
publicinitialize(){
thrownewError('Model.prototype.initialize()isabstractandmust
implemented.');
}
//mustbeimplementedbyderivedclasses
publicdispose(){
thrownewError('Model.prototype.dispose()isabstractandmust
implemented.');
}
protectedrequestAsync(method:string,dataType:string,data){
returnQ.Promise((resolve:(r)=>{},reject:(e)=>{})=>{
$.ajax({
method:method,
url:this._serviceUrl,
data:data||{},
dataType:dataType,
success:(response)=>{
resolve(response);
},
error:(...args:any[])=>{
reject(args);
}
});
});
}
protectedgetAsync(dataType:string,data:any){
returnthis.requestAsync("GET",dataType,data);
}
protectedpostAsync(dataType:string,data:any){
returnthis.requestAsync("POST",dataType,data);
}
protectedputAsync(dataType:string,data:any){
returnthis.requestAsync("PUT",dataType,data);
}
protecteddeleteAsync(dataType:string,data:any){
returnthis.requestAsync("DELETE",dataType,data);
}
}
export{Model,ModelSettings};
Justlikeinthecaseofthecontrollers,theinitializeanddisposemethodsaremeanttobeimplementedbythederivedmodels,sotheydon'tcontainanylogichere.
TherequestAsyncmethodisusedtoretrievedatafromawebserviceorstaticfile.Aswecansee,themethodusesthejQueryAJAXAPIandQ'sPromises.
TheclassalsoincludesthegetAsync,postAsync,putAsync,anddeleteAsyncmethods,whicharehelperstoperformGET,POST,PUT,andDELETErequestsrespectively.
Eventhoughitisnotforcedbytheframework,itisrecommendedyouusethemediatortopassthecontroltooneoftheviewsfromthemodel.
ViewandviewsettingsViewsareusedtorendertemplatesandhandleUIevents.Justliketherestofthecomponentsinourapplication,theViewclassextendstheEventEmitterclass:
interfaceIViewextendsIEventEmitter{
initialize():void;
dispose():void;
}
AviewneedstobeprovidedwiththeURLofthetemplatethatitconsumes.WearegoingtouseaclassdecoratornamedViewSettingstosettheURLofthetemplatetobeconsumed.
WecouldinjectthetemplateURLviaitsconstructor,butitisconsideredabadpracticetoinjectdata(asopposedtoabehavior)viaaclassconstructor.Thedecoratorincludessomecommentstofacilitateitsunderstanding:
///<referencepath="./interfaces"/>
import{EventEmitter}from"./event_emitter";
import{AppEvent}from"./app_event";
functionViewSettings(templateUrl:string,container:string){
returnfunction(target:any){
//saveareferencetotheoriginalconstructor
varoriginal=target;
//autilityfunctiontogenerateinstancesofaclass
functionconstruct(constructor,args){
varc:any=function(){
returnconstructor.apply(this,args);
}
c.prototype=constructor.prototype;
varinstance=newc();
instance._container=container;
instance._templateUrl=templateUrl;
returninstance;
}
//thenewconstructorbehaviour
varf:any=function(...args){
returnconstruct(original,args);
}
//copyprototypesoinstanceofoperatorstillworks
f.prototype=original.prototype;
//returnnewconstructor(willoverrideoriginal)
returnf;
}
}
Inthenextchapter,wewillbeabletoapplythedecoratorasfollows:
@ViewSettings("./source/app/templates/market.hbs","#outlet")
classMarketViewextendsViewimplementsIView{
//...
Let'stakealookattheViewclass.Justlikeinthecaseofthecontrollersandmodels,theinitializeanddisposemethodsaremeanttobeimplementedbythederivedviews,sotheydon'tcontainanylogichere.
classViewextendsEventEmitterimplementsIView{
//thevaluesof_containerand_templateUrlmustbesetusingthe
ViewSettingsdecorator
protected_container:string;
private_templateUrl:string;
private_templateDelegate:HandlebarsTemplateDelegate;
constructor(metiator:IMediator){
super(metiator);
}
//mustbeimplementedbyderivedclasses
publicinitialize(){
thrownewError('View.prototype.initialize()isabstractandmust
implemented.');
}
//mustbeimplementedbyderivedclasses
publicdispose(){
thrownewError('View.prototype.dispose()isabstractandmust
implemented.');
}
Theviewclassincludestwonewmethods(namedbindDomEventsandunbindDomEvents)thatmustbeimplementedbytheirderivedclasses.Aswecanguessfromtheirnames,thesemethodsshouldbeusedtoset(bindDomEvents)andunset(unbindDomEvents)UIeventhandlers:
//mustbeimplementedbyderivedclasses
protectedbindDomEvents(model:any){
thrownewError('View.prototype.bindDomEvents()isabstractandmust
implemented.');
}
//mustbeimplementedbyderivedclasses
protectedunbindDomEvents(){
thrownewError('View.prototype.unbindDomEvents()isabstractandmust
implemented.');
}
Thefollowingasynchronousmethodsusepromisesandareusedtoloadatemplate(loadTemplateAsync),compileit(compileTemplateAsync),cacheit(getTemplateAsync),andrenderit(renderAsync)—allthemethodsareprivateexceptrenderAsync,whichismeantobe
usedbythederivedviews:
//asynchroniuslyloadsatemplate
privateloadTemplateAsync(){
returnQ.Promise((resolve:(r)=>{},reject:(e)=>{})=>{
$.ajax({
method:"GET",
url:this._templateUrl,
dataType:"text",
success:(response)=>{
resolve(response);
},
error:(...args:any[])=>{
reject(args);
}
});
});
}
//asynchroniuslycompileatemplate
privatecompileTemplateAsync(source:string){
returnQ.Promise((resolve:(r)=>{},reject:(e)=>{})=>{
try{
vartemplate=Handlebars.compile(source);
resolve(template);
}
catch(e){
reject(e);
}
});
}
//asynchroniuslyloadsandcompileatemplateifnotdonealready
privategetTemplateAsync(){
returnQ.Promise((resolve:(r)=>{},reject:(e)=>{})=>{
if(this._templateDelegate===undefined||this._templateDelegate===
null){
this.loadTemplateAsync()
.then((source)=>{
returnthis.compileTemplateAsync(source);
})
.then((templateDelegate)=>{
this._templateDelegate=templateDelegate;
resolve(this._templateDelegate);
})
.catch((e)=>{reject(e);});
}
else{
resolve(this._templateDelegate);
}
});
}
//asynchroniuslyrenderstheview
protectedrenderAsync(model){
returnQ.Promise((resolve:(r)=>{},reject:(e)=>{})=>{
this.getTemplateAsync()
.then((templateDelegate)=>{
//generatehtmlandappendtotheDOM
varhtml=this._templateDelegate(model);
$(this._container).html(html);
//passmodeltoresolvesoitcanbeusedby
//subviewsandDOMeventinitializer
resolve(model);
})
.catch((e)=>{reject(e);});
});
}
}
export{View,ViewSettings};
FrameworkTheframeworkfileisusedtoprovideaccesstoallthecomponentsintheframeworkfromonesinglefile.Thismeansthatwhenweimplementanapplicationusingourframework,wewillnotneedtoimportadifferentfileforeachcomponent:
///<referencepath="./interfaces"/>
import{App}from"./app";
import{Route}from"./route";
import{AppEvent}from"./app_event";
import{Controller}from"./controller";
import{View,ViewSettings}from"./view";
import{Model,ModelSettings}from"./model";
export{App,AppEvent,Controller,View,ViewSettings,Model,ModelSettings,
Route};
SummaryInthischapter,weunderstoodwhatasingle-pagewebapplicationis,whatitscommoncomponentsare,andwhatthemaincharacteristicsofthisarchitectureare.
WealsocreatedourownMV*framework.ThispracticalexperienceandknowledgewillhelpustounderstandmanyoftheavailableMV*frameworks.
Inthenextchapter,wewilltrytoputinpracticemanyoftheconceptsthatwehavelearnedinthisbookbycreatingafullSPAusingtheframeworkthatwecreatedinthischapter.
Chapter10.PuttingEverythingTogetherInthischapter,wearegoingtoputintopracticethemajorityoftheconceptsthatwehavecoveredinthepreviouschapters.
Wewilldevelopasmallsingle-pagewebapplicationusingtheSPAframeworkthatwedevelopedinChapter9,ApplicationArchitecture.
ThisapplicationwillallowustofindouthowtheNASDAQandNYSEstocksaredoingonaparticularday.Itwillnotbeaverylargeapplication,butitwillbebigenoughtodemonstratetheadvantagesofworkingwithTypeScriptandusingagoodapplicationarchitecture.
Wewillwritesomeclassesandseveralfunctions.Someofthesefunctionswillbeasynchronous(Chapter1,IntroducingTypeScript;Chapter3,WorkingwithFunctions;Chapter4,Object-OrientedProgrammingwithTypeScript;andChapter5,Runtime).WewillalsoconsumesomedecoratorsprovidedbyourSPAframework(Chapter8,Decorators).
Tocompletethechapter,wewillcreateanautomatedbuildtofacilitatethedevelopmentprocess(Chapter2,AutomatingYourDevelopmentWorkflow),improvetheapplicationperformance(Chapter6,ApplicationPerformance),andensurethatitworkscorrectlybywritingsomeunitandintegrationtests(Chapter7,ApplicationTesting).
Inthischapter,wewillaimtohelpyougainconfidencewithTypeScriptandtheSPAarchitecture.WeneedtofocusontheSOLIDprinciplesandtheseparationofconcerns.Ourgoalistocreateanapplicationthatismaintainableandtestable,andanapplicationthatcangrowovertimeandwhichcomponentscanbereusedinfutureapplications.
PrerequisitesInthisapplication,wewillusethetoolsandthedirectorytreethatwecreatedinthepreviouschapter.Youcanusethetsd.jsonandpackage.jsonfilesincludedinthecompanionsourcecodetoinstalltherequirednpmpackagesandtypedefinitionfiles.RefertotheprerequisitessectionundertheWritinganMVCframeworkfromscratchsectioninChapter9,ApplicationArchitecture,foradditionalinformationabouttheprerequisitesofthisapplication.
Theapplication'srequirementsWewilldevelopasmallapplicationthatwillallowuserstoseealistofstocksymbols.Astocksymbolrepresentsacompanythattradesitssharesonastockexchange.
Theapplicationhomepagewilldisplaystocksymbolsfromtwopopularstockexchanges:NASDAQ(NationalAssociationofSecuritiesDealersAutomatedQuotations)andNYSE(NewYorkstockexchange).
Asyoucanseeinthefollowingscreenshot,thewebapplicationrequiresatopmenucontaininglinksthatallowtheusertoseethestocksymbolsinoneoftheaforementionedstockexchanges.Thelistofstocksymbolswillbedisplayedinatable,whichwillincludesomebasicdetailsaboutthestocks,suchasthepriceofashareinthelastsaleorthenameorthecompany:
Thelastcolumninthetablecontainssomebuttonsthatwillallowuserstonavigatetoasecondscreenthatdisplaysastockquote.Astockquoteisjustasummaryofthepricingperformancedetailsofthestockforagivenperiodoftime.
Thestockquotescreenwilldisplayalinegraphthatisusedbythebrokerstoseehowthepriceoftheshares(theyaxis)hasevolvedovertime(thexaxis).Wecandisplaymultiplelinestovisualizetheevolutionoftheopeningprice(thepriceofthesharesatthebeginningoftheday),theclosingprice(thepriceofashareattheendoftheday),thehighprice(the
highestsellingpriceoftheshareinagivenday),andthelowprice(thelowestsellingpriceoftheshareinagivenday).
Theapplication'sdataAsweexplainedinthepreviouschapter,weneedanapplicationbackendthatallowsustoquerythedatafromawebbrowserusingAJAXrequestsinordertodevelopanSPA.ThismeansthatwearegoingtoneedanHTTPAPI.
WewilluseafreelyavailablepublicHTTPAPIthatwillallowustoobtainrealstockquotedata.Forthelistofavailablestocksymbols,wewillusestaticJSONfiles.TheseJSONfileshavebeengeneratedbytransformingaCSVfileavailableontheNASDAQwebsite.TheexternalHTTPAPIwillalsoprovidethelinegraphdata.
Intotal,wewillbeusingthreesetsofdata:
Marketdata:ThisdataisstoredinstaticJSONfiles.ThesefileshavebeengeneratedfromaCSVfileprovidedbytheNASDAQofficialwebsiteandcanbefoundinthecompanionexample.Stockquotedata:Thishasbeenprovidedbyanexternalwebservice.TheexternaldataproviderthatwewilluseinthisexampleisacompanycalledMarkit,specializinginfinancialinformationservices.WewillusetheirmarketdataAPI(v2),whichisavailableforfreeandhasbeenwelldocumentedathttp://dev.markitondemand.com/.Chartdata:ThisisalsoprovidedinawebservicebyMarkit.
Theapplication'sarchitectureWewilldevelopanSPAusingourownframework.Aswesawinthepreviouschapter,ourframeworkcanmapaURLwithanactioninacontroller.
Ourapplicationwillhavethreemainscreens.EachscreenusesadifferentURL,asfollows:
#market/nasdaqdisplaysstocksintheNASDAQstockmarket#market/nysedisplaysstocksintheNYSEstockmarket#symbol/quote/{symbol}displaysastockquotefortheselectedstocksymbol
EachofthemainURLsmentionedearlierwillbeimplementedasacontroller'sactioninourapplication.Inthepreviouschapter,yousawthatURLsadheretothefollowingnamingconvention:#controllerName/actionName/arg1/arg2/argN.
IfweextrapolatethisnamingconventiontotheURLsmentionedintheprecedinglist,wecandeducethatourapplicationwillhavetwocontrollers:MarketControllerandSymbolController.
TheMarketControllercontrollerwillbeimplementedusingtwomodelsandoneview:
NasdaqModel:ThisloadsalistofNASDAQstocksfromastaticJSONfileNyseModel:ThisloadsalistofNYSEstocksfromastaticJSONfileMarketView:ThisrendersthelistofeithertheNASDAQorNYSEstocks
Eachcomponentcommunicateswiththeotherusingapplicationeventsandthemediator.Theexecutionorderofthemarketscreenlooksasfollows:
TheSymbolControllercontrollerwillbeimplementedusingtwomodelsandtwoviews:
QuoteModel:ThisloadsastockquotefortheselectedsymbolChartModel:ThisloadssymbolperformancedatapointsforthelastyearChartView:ThisdisplaysstockperformanceinaninteractivechartSymbolView:Thisdisplaysthelastpricechangefortheselectedsymbol
Eachcomponentcommunicateswiththeotherusingapplicationeventsandthemediator.Theexecutionorderofthestockquotescreenlooksasfollows:
Theapplication'sfilestructurePresentedinthissectionisthefolderstructureoftheapplicationwearegoingtobuild.Intherootdirectory,youcanfindtheapplicationaccesspoint(index.html),aswellassomeoftheautomationtools'configurationfiles(gulpfile.js,karma.conf.js,package.json,andsoon).Youcanalsoobservethetypingsfolder,whichcontainssometypedefinitionfiles.
Justasinthepreviouschapters,theapplicationsourcecodeislocatedunderthesourcedirectory.Theunitandintegrationtestsarelocatedinthetestfolder.Thefollowingisthefolderstructureoftheapplication:
├──LICENSE
├──README.md
├──css
│└──site.css
├──data
│├──nasdaq.json
│└──nyse.json
├──gulpfile.js
├──index.html
├──karma.conf.js
├──node_modules
├──package.json
├──source
│├──app
││├──controllers
│││├──market_controller.ts
│││└──symbol_controller.ts
││├──main.ts
││├──models
│││├──chart_model.ts
│││├──nasdaq_model.ts
│││├──nyse_model.ts
│││└──quote_model.ts
││├──templates
│││├──market.hbs
│││└──symbol.hbs
││└──views
││├──chart_view.ts
││├──market_view.ts
││└──symbol_view.ts
│└──framework
│└──framework.ts(Chapter9)
├──test
│├──app
│└──framework
├──tsd.json
└──typings
Underthesourcedirectory,youcanobservetwofolders,namedappandframework.Wecreatedallthefilesundertheframeworkdirectoryinthepreviouschapter.Thistime,wewillfocusontheapplication,whichmeanswewillbeworkingundertheappdirectorymostofthe
time.
Insidetheappdirectory,youcanfindsomedirectoriesnamedcontrollers,models,templates,andviews.Asyoucanguess,thesedirectoriesareusedtostorecontrollers,models,templates,andviewsrespectively.
Youcanalsofindthemain.tsfileinsidetheappdirectory.Thisfileistheapplication'sentrypoint,butbecausewearegoingtouseES6modules,wearenotgoingtobeabletoloadthisfileinawebbrowserusinga<script/>tag.
ConfiguringtheautomatedbuildJustaswedidinChapter2,AutomatingYourDevelopmentWorkflow,weneedtocreateaconfigurationfiletoconfigurethedesiredGulptasks.Solet'screateafilenamedgulpfile.jsandimporttherequiredGulpplugins:
vargulp=require("gulp"),
browserify=require("browserify"),
source=require("vinyl-source-stream"),
buffer=require("vinyl-buffer"),
tslint=require("gulp-tslint"),
tsc=require("gulp-typescript"),
karma=require("karma").server,
coveralls=require('gulp-coveralls'),
uglify=require("gulp-uglify"),
runSequence=require("run-sequence"),
header=require("gulp-header"),
browserSync=require("browser-sync"),
reload=browserSync.reload,
pkg=require(__dirname+"/package.json");
Weneedtorememberthatbeforewecanimportoneofthesepackages,wemustfirstinstallthemusingnpm.
Oncethepluginshavebeenimported,wecanproceedtowriteourfirsttask,whichisusedtocheckforsomebasicnameconventionrulesandtoavoidsomebadpractices(theTypeScriptfilesareunderthesourceandtestsdirectories):
gulp.task("lint",function(){
returngulp.src([
"source/**/**.ts",
"test/**/**.test.ts"
])
.pipe(tslint())
.pipe(tslint.report("verbose"));
});
WealsoneedanothertasktocompileourTypeScriptcodeintoJavaScriptcode.Asweareworkingwithdecorators,weneedtoensurethatweareusingTypeScript1.5orhigherandthattheexperimentalDecoratorscompilersettingsandtargetareconfiguredasinthefollowingcodesnippet:
vartsProject=tsc.createProject({
target:"es5",
module:"commonjs",
experimentalDecorators:true,
typescript:typescript
});
Oncewehavesetupthecompileroptions,wecanproceedtowritesometasks.Thefirstonewillcompiletheapplicationcode:
gulp.task("build",function(){
returngulp.src("src/**/**.ts")
.pipe(tsc(tsProject))
.js.pipe(gulp.dest("build/source/"));
});
Thesecondonewillcompiletheunittestandintegrationtestcode.Weneedtouseanewprojectobjecttoavoidpotentialruntimeissues:
vartsTestProject=tsc.createProject({
target:"es5",
module:"commonjs",
experimentalDecorators:true,
typescript:typescript
});
gulp.task("build-test",function(){
returngulp.src("test/**/*.test.ts")
.pipe(tsc(tsTestProject))
.js.pipe(gulp.dest("/build/test/"));
});
ThetwoprevioustasksshouldbeenoughtogenerateJavaScript,butbecauseweareusingCommonJSmodules,weneedtowriteatasktobundletheCommonJSmodulesintoapackagethatcanbeloadedandexecutedinawebbrowser.AswesawinChapter2,AutomatingYourDevelopmentWorkflow,wewillcreateafewGulptasksthatuseBrowserifyforthispurpose.
Weneedatasktobundletheapplicationcode:
gulp.task("bundle-source",function(){
varb=browserify({
standalone:'TsStock',
entries:"build/source/app/main.js",
debug:true
});
returnb.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(gulp.dest("bundled/source/"));
});
Wefurtherneedatasktobundletheapplication'sunittests:
gulp.task("bundle-unit-test",function(){
varb=browserify({
standalone:'test',
entries:"build/test/bdd.test.js",
debug:true
});
returnb.bundle()
.pipe(source("bdd.test.js"))
.pipe(buffer())
.pipe(gulp.dest("bundled/test/"));
});
Weneedafinaltasktobundletheapplication'sintegrationtests:
gulp.task("bundle-e2e-test",function(){
varb=browserify({
standalone:'test',
entries:"build/test/e2e.test.js",
debug:true
});
returnb.bundle()
.pipe(source("e2e.test.js"))
.pipe(buffer())
.pipe(gulp.dest("bundled/e2e-test/"));
});
Wewillreturntothegulpfile.jsconfigurationfilelaterinthischaptertoaddsomeadditionaltasksthatwillbeinchargeofrunningtheapplicationanditsautomatedtests,aswellassomeoptimizations.
Note
Untilnow,wehavebeenworkingontheconfigurationofanautomateddevelopmentworkflow.Fromnowon,wewillfocusontheapplicationcomponents.Acomponentiscomposedoffourcoreelements:template,stylerules,services,andthecomponent'slogic.Youwillbeabletofindthestylerulesandtemplatesinthecompanioncodesamples,butwewillmainlyfocusontheTypeScriptfiles(servicesandthecomponent'slogic)here.
Theapplication'slayoutLet'screateanewfile,namedindex.html,undertheapplication'srootdirectory.Thefollowingcodesnippetisanalteredversionoftherealindex.htmlpage,whichisincludedwiththecompanionsourcecode:
<ulclass="navnavbar-nav">
<li>
<ahref="#market/nasdaq">NASDAQ</a>
</li>
<li>
<ahref="#market/nyse">NYSE</a>
</li>
</ul>
<divid="outlet">
<!--HTMLGENERATEDBYVIEWSGOESHERE-->
</div>
AsyoucanseeintheprecedingHTMLsnippet,thecodehastwoimportantelements.ThefirstsignificantelementistheURLofthetwolinks.Theselinksincludethehashcharacter(#),andtheywillbeprocessedbytheapplication'srouter.
ThesecondsignificantelementistheelementthatusesoutletasID.ThisnodeisusedbyourframeworkasacontainerwheretheDOMofeachnewpageisdynamicallygeneratedandaddedtothepage.
ImplementingtherootcomponentAsyousawinthepreviouschapter,therootcomponentofourcustomMVCframeworkistheAppcomponent.So,let'screateanewfile,namedmain.ts,underthesource/appdirectory.
Wecanaccessalltheinterfacesintheframeworkbyaddingareferencetothesource/interfaces.tsasfollows:
///<referencepath="../framework/interfaces"/>
Wecanthenaccessallthecomponentsintheframeworkbyimportingtheframework/framework.tsfile:
import{App,View}from"../framework/framework";
Ourapplicationwillhavetwocontrollers.Thefilesdon'texistyetbutwecanaddthetwoimportstatementsanyway:
import{MarketController}from"./controllers/market_controller";
import{SymbolController}from"./controllers/symbol_controller";
Atthispoint,weneedtocreateanobjectliteralthatimplementstheIAppSettingsinterface.Thisobjectallowsustosetsomebasicconfiguration,suchasthenameofthedefaultcontrolleroraction,oraglobalerrorhandler.However,themostimportantfieldintheobjectliteralisthecontrollerfield,whichmustbeanarrayofIControllerDetails.IfyouneedadditionaldetailsabouttheIControllerDetails,refertothepreviouschapter.
varappSettings:IAppSettings={
isDebug:true,
defaultController:"market",
defaultAction:"nasdaq",
controllers:[
{controllerName:"market",controller:MarketController},
{controllerName:"symbol",controller:SymbolController}
],
onErrorHandler:function(e:Object){
alert("Sorry!therehasbeenanerrorpleasecheckouttheconsoleformore
info!");
console.log(e.toString());
}
};
WecanthencreatetheAppinstanceandinvoketheinitializemethodtostartexecutingit:
varmyApp=newApp(appSettings);
myApp.initialize();
Atthispoint,ourcodedoesnotcompilebecausewehavenotdefinedtheMarketControllerandSymbolControllercontrollersyet.Let'sdefineourfirstcontroller.
ImplementingthemarketcontrollerLet'screateanewfilenamedmarket_controller.tsundertheapp/controllersdirectory.WeneedtoimporttheControllerandAppEvententitiesfromtheframeworkalongwithsomeentitiesthatarenotavailableyet(NyseModel,NasdaqModelandMarketView).
///<referencepath="../../framework/interfaces"/>
import{Controller,AppEvent}from"../../framework/framework";
import{MarketView}from"../views/market_view";
import{NasdaqModel}from"../models/nasdaq_model";
import{NyseModel}from"../models/nyse_model";
Inanapplicationthatusesourframework,acontrollermustextendthebaseControllerclassandimplementtheIControllerclass:
classMarketControllerextendsControllerimplementsIController{
Wearenotforcedtodeclaretheviewsandmodelsusedbythecontrollerasitsproperties,butitisrecommended:
private_marketView:IView;
private_nasdaqModel:IModel;
private_nyseModel:IModel;
Itisalsorecommendedthatyousetthevalueofallthecontroller'sdependenciesinsidethecontrollerconstructor:
constructor(metiator:IMediator){
super(metiator);
this._marketView=newMarketView(metiator);
this._nasdaqModel=newNasdaqModel(metiator);
this._nyseModel=newNyseModel(metiator);
}
Note
Insteadofsettingthevalueofallthecontroller'sdependenciesinsidethecontrollerconstructor,itwouldbeevenbettertouseanIoCcontainertoautomaticallyinjectthecontroller'sdependenciesviaitsconstructor.Though,implementinganIoCcontainerisnotasimpletask,itisbeyondthescopeofthisbook.
Wemustimplementtheinitializemethod.Theinitializemethodistheplacewhereacontrollershoulddothefollowing:
Subscribetooneapplicationeventforeachactionavailableinthecontroller.Inthiscase,thecontrollerhastwoactions(thenasdaqandnysemethods).InitializeviewsbyinvokingtheView.initialize()method.Inthiscase,thereisonlyoneview(marketView).InitializemodelsbyinvokingtheModel.initialize()method.Inthiscase,therearetwo
models(nasdaqModelandnyseModel).
publicinitialize():void{
//subscribetocontrolleractionevents
this.subscribeToEvents([
newAppEvent("app.controller.market.nasdaq",null,(e,args:
string[])=>{this.nasdaq(args);}),
newAppEvent("app.controller.market.nyse",null,(e,args:
string[])=>{this.nyse(args);})
]);
//initializeviewandmodelsevents
this._marketView.initialize();
this._nasdaqModel.initialize();
this._nyseModel.initialize();
}
Thedisposemethodistheoppositeoftheinitializemethod.Ifaneventhandlerwascreatedintheinitializemethod,itshouldbedestroyedinthedisposemethod.TheunsubscribeToEventshelperwillunsubscribealltheeventsthatweresubscribedusingthesubscribeToEventshelper:
//disposeviews/modelsandstoplisteningtocontrolleractions
publicdispose():void{
//disposethecontrollerevents
this.unsubscribeToEvents();
//disposeviewsandmodelevents
this._marketView.dispose();
this._nasdaqModel.dispose();
this._nyseModel.dispose();
}
Asyousawinthepreviouschapter,thedispatcherusesthecontroller'sinitializeanddisposemethodstofreesomememorywhenitisnotneededanymore.Ifweforgettodisposeoneoftheviewsusedbythecontrollerinitsdisposemethod,theviewcouldendupstayinginmemoryforever.
Theactionsofacontrollershouldnotperformanykindofdatamanipulation(modelsshouldbeinchargeofthat)oruserinterfaceeventsmanagement(viewsshouldbeinchargeofthat).Ideally,acontroller'sactionsshouldonlypublishoneormoreapplicationeventssotheexecutionflowgoesfromthecontrollertooneormoremodels.
Inthecaseofthenasdaqaction,thecontrollerpublishesoneoftheeventstowhichthenasdaqmodelsubscribedwhentheinitializemethodofNasdaqModelwasinvoked:
//displayNASDAQstocks
publicnasdaq(args:string[]){
this._metiator.publish(newAppEvent("app.model.nasdaq.change",null,
null));
}
Inthecaseofthenyseaction,thecontrollerpublishesoneoftheeventstowhichthenysemodelwassubscribedwhentheinitializemethodofNyseModelwasinvoked:
//displayNYSEstocks
publicnyse(args:string[]){
this._metiator.publish(newAppEvent("app.model.nyse.change",null,null));
}
}
export{MarketController};
ImplementingtheNASDAQmodelLet'screateanewfilenamednasdaq_model.tsundertheapp/modelsdirectory.WecanthenimporttheModel,AppEvent,andModelSettingsfromourframeworkanddeclareanewclassnamedNasdaqModel.ThenewclassmustextendthebaseModelclassandimplementtheIModelinterface.
WewillalsousetheModelSettingsdecoratortoindicatethepathofawebserviceorstaticdatafile.Inthiscase,wewilluseastaticdatafile,whichcanbefoundinthecompanionsourcecode:
///<referencepath="../../framework/interfaces"/>
import{Model,AppEvent,ModelSettings}from"../../framework/framework";
@ModelSettings("./data/nasdaq.json")
classNasdaqModelextendsModelimplementsIModel{
constructor(metiator:IMediator){
super(metiator);
}
Themodelwillsubscribetotheapp.model.nasdaq.changeeventwhentheinitializemethodisinvoked.Thisisactuallytheeventthatthecontroller'sactionpublishedtopasstheexecutionflowfromthecontrollertothemodel:
//listentomodelevents
publicinitialize(){
this.subscribeToEvents([
newAppEvent("app.model.nasdaq.change",null,(e,args)=>{
this.onChange(args);})
]);
}
Justlikeinthepreviouscontroller,theunsubscribeToEventshelperwillunsubscribealltheeventsthatweresubscribedusingthesubscribeToEventshelper:
//disposemodelevents
publicdispose(){
this.unsubscribeToEvents();
}
Thisistheeventhandleroftheapp.model.nasdaq.changeevent.TheeventhandlerusesthegetAsyncmethodtoloadthedatafromtheserviceURLthatwepreviouslyspecifiedusingtheModelSettingsdecorator.ThegetAsyncmethodisinheritedfromthebaseModelclass,whichweimplementedinthepreviouschapter.
ThegetAsyncmethodreturnsapromise;ifthepromiseisfulfilled,thedataisformattedandthenpassedtoaview:
privateonChange(args):void{
this.getAsync("json",args)
.then((data)=>{
//formatdata
varstocks={items:data,market:"NASDAQ"};
//passcontrolltothemarketview
this.triggerEvent(newAppEvent("app.view.market.render",stocks,
null));
})
.catch((e)=>{
//passcontroltotheglobalerrorhandler
this.triggerEvent(newAppEvent("app.error",e,null));
});
}
}
export{NasdaqModel};
ImplementingtheNYSEmodelLet'screateanewfilenamednyse_model.tsundertheapp/modelsdirectory.TheNyseModelclassisalmostidenticaltotheNasdaqModelclass,sowewillnotgointotoomuchdetail:
@ModelSettings("./data/nyse.json")
classNyseModelextendsModelimplementsIModel{
//...
}
export{NyseModel};
Allweneedtodoiscopythecontentsofthenasdaq_model.tsfileintothenyse_model.tsfileandreplace(casesensitive)nasdaqwithnyse.
Note
Thiskindofcodeduplicationisknownasacodesmell.Acodesmellindicatesthatsomethingiswrongandweneedtorefactor(improve)it.WecouldavoidalotofcodeduplicationbyusingGenerictypes.Howevergenerictypeswerenotusedherebecausewethoughthatshowcasingtheusageofdecoratorswouldbemorevaluableforthereadersofthisbook.
ImplementingthemarketviewLet'screateanewfilenamedmarket_view.tsundertheapp/viewsdirectory.WecanthenimporttheAppEvent,ViewSettings,andRoutecomponentsfromourframeworkanddeclareanewclassnamedMarketView.ThenewclassmustextendthebaseViewclassandimplementtheIViewinterface.
WewillalsousetheViewSettingsdecoratortoindicatethepath,aHandlebarstemplate,andaselector,whichisusedtofindtheDOMelementthatwillbeusedastheparentnodeoftheview'sHTML:
///<referencepath="../../framework/interfaces"/>
import{View,AppEvent,ViewSettings,Route}from"../../framework/framework";
@ViewSettings("./source/app/templates/market.hbs","#outlet")
classMarketViewextendsViewimplementsIView{
constructor(metiator:IMediator){
super(metiator);
}
Thisviewissubscribedtotheapp.view.market.rendereventanditshandlerinvokestherenderAsyncmethod,whichhasbeeninheritedfromthebaseviewclass.Thismethodreturnsapromise,whichisfulfilledifthetemplatepassedtotheViewSettingsdecoratorhasbeenloadedandcompiledsuccessfully.
Forthepromisetobefulfilled,theviewmustbesuccessfullyrenderedandappendedtotheDOMelementthatmatchestheselectorpassedtotheViewSettingsdecorator:
initialize():void{
this.subscribeToEvents([
newAppEvent("app.view.market.render",null,(e,args:any)=>{
this.renderAsync(args)
.then((model)=>{
//setDOMevents
this.bindDomEvents(model);
})
.catch((e)=>{
//passcontroltotheglobalerrorhandler
this.triggerEvent(newAppEvent("app.error",e,null));
});
}),
]);
}
Justlikeinthepreviouscontrollerandmodel,theunsubscribeToEventshelperwillunsubscribealltheeventsthatweresubscribedtousingthesubscribeToEventshelper:
publicdispose():void{
this.unbindDomEvents();
this.unsubscribeToEvents();
}
Viewsareresponsibleforthemanagementofuserevents.Thecomponentsinourframeworkusetheinitializemethodtosubscribetoapplicationevents,andthedisposemethodtounsubscribetoapplicationevents.Inthecaseofuserevents,wewillusethebindDomEventsmethodtosettheuserevents,andtheunbindDomEventsmethodtodisposeofthem:
//initializesDOMevents
protectedbindDomEvents(model:any){
varscope=$(this._container);
//handleclickon"quote"button
$(".getQuote").on('click',scope,(e)=>{
varsymbol=$(e.currentTarget).data('symbol');
this.getStockQuote(symbol);
});
//maketablesortableandsearchable
$(scope).find('table').DataTable();
}
//disposesDOMevents
protectedunbindDomEvents(){
varscope=this._container;
$(".getQuote").off('click',scope);
vartable=$(scope).find('table').DataTable();
table.destroy();
}
Oneoftheusereventsobservesclicksonthequotebuttons.Whentheeventistriggered,thefollowingeventhandlerisinvoked:
privategetStockQuote(symbol:string){
//navigatetorouteusingrouteevent
this.triggerEvent(newAppEvent(
"app.route",
newRoute("symbol","quote",[symbol]),
null));
}
}
Asyoucansee,thiseventhandlercreatesanewrouteandpublishesanapp.routeevent.ThiswillcausetheroutertonavigatetothequoteactionintheSymbolController:export{MarketView};
ImplementingthemarkettemplateThetemplateloadedandcompiledbyMarketViewlooksasfollows:
<divclass="panelpanel-defaultfadeInUpanimated">
<divclass="panel-body">
<h2>{{market}}</h2>
<tableclass="tabletable-responsibletable-condensed">
<thead>
<tr>
<th>Symbol</th>
<th>Name</th>
<th>LastSale</th>
<th>MarketCapital</th>
<th>IPOyear</th>
<th>Sector</th>
<th>industry</th>
<th>Quote</th>
</tr>
</thead>
<tbody>
{{#eachitems}}
<tr>
<td><spanclass="labellabel-default">{{Symbol}}</span></td>
<td>{{{Name}}}</td>
<td>{{LastSale}}</td>
<td>{{MarketCap}}</td>
<td>{{IPOyear}}</td>
<td>{{Sector}}</td>
<td>{{industry}}</td>
<td>
<buttonclass="btnbtn-primarybtn-smgetQuote"data-symbol="
{{Symbol}}">
<spanclass="glyphiconglyphicon-stats"aria-hidden="true">
</span>
Quote
</button>
</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
ImplementingthesymbolcontrollerLet'screateanewfilenamedsymbol_controller.tsundertheapp/controllersdirectory.ThisfilewillcontainanewcontrollernamedSymbolController.TheimplementationofthiscontrollerislargelysimilartotheimplementationoftheMarketControllercontroller,sowearegoingtoavoidgoingintotoomuchdetail.
Themaindifferencebetweenthiscontrollerandthepreviouscontrolleristhatthenewcontrollerusestwonewmodels(QuoteModelandChartModel)andtwonewviews(SymbolViewandChartView):
///<referencepath="../../framework/interfaces"/>
import{Controller,AppEvent}from"../../framework/framework";
import{QuoteModel}from"../models/quote_model";
import{ChartModel}from"../models/chart_model";
import{SymbolView}from"../views/symbol_view";
import{ChartView}from"../views/chart_view";
classSymbolControllerextendsControllerimplementsIController{
private_quoteModel:IModel;
private_chartModel:IModel;
private_symbolView:IView;
private_chartView:IView;
constructor(metiator:IMediator){
super(metiator);
this._quoteModel=newQuoteModel(metiator);
this._chartModel=newChartModel(metiator);
this._symbolView=newSymbolView(metiator);
this._chartView=newChartView(metiator);
}
//initializeviews/modelsandstratlisteningtocontrolleractions
publicinitialize():void{
//subscribetocontrolleractionevents
this.subscribeToEvents([
newAppEvent("app.controller.symbol.quote",null,(e,symbol:string)
=>{this.quote(symbol);})
]);
//initializeviewandmodelsevents
this._quoteModel.initialize();
this._chartModel.initialize();
this._symbolView.initialize();
this._chartView.initialize();
}
//disposeviews/modelsandstoplisteningtocontrolleractions
publicdispose():void{
//disposethecontrollerevents
this.unsubscribeToEvents();
//disposeviewsandmodelevents
this._symbolView.dispose();
this._quoteModel.dispose();
this._chartView.dispose();
this._chartModel.dispose();
}
ItisalsoimportanttonoticethatthequoteactionpassesthecontroltotheQuoteModelmodel:
publicquote(symbol:string){
this.triggerEvent(newAppEvent("app.model.quote.change",symbol,null));
}
}
export{SymbolController};
ImplementingthequotemodelLet'screateanewfilenamedquote_model.tsundertheapp/modelsdirectory.Thisisthethirdmodelthatwehaveimplementedsofar.Thismeansthatyoushouldbefamiliarwiththebasicsalready,buttherearesomeminoradditionsinthisparticularmodel.Thefirstthingthatyouwillnoticeisthatthewebserviceisnolongerastaticfile:
///<referencepath="../../framework/interfaces"/>
import{Model,AppEvent,ModelSettings}from"../../framework/framework";
@ModelSettings("http://dev.markitondemand.com/Api/v2/Quote/jsonp")
classQuoteModelextendsModelimplementsIModel{
constructor(metiator:IMediator){
super(metiator);
}
//listentomodelevents
publicinitialize(){
this.subscribeToEvents([
newAppEvent("app.model.quote.change",null,(e,args)=>{
this.onChange(args);})
]);
}
//disposemodelevents
publicdispose(){
this.unsubscribeToEvents();
}
ThesecondthingthatyoushouldnoticeisthattheonChangefunctioninvokesanewfunction(formatModel)whenthepromisereturnedbygetAsyncisfulfilled:
privateonChange(args):void{
//formatargs
vars={symbol:args};
this.getAsync("jsonp",s)
.then((data)=>{
//formatdata
varquote=this.formatModel(data);
//passcontrolltothemarketview
this.triggerEvent(newAppEvent("app.view.symbol.render",quote,
null));
})
.catch((e)=>{
//passcontroltotheglobalerrorhandler
this.triggerEvent(newAppEvent("app.error",e,null));
});
}
Thenewfunctionjustformatstheresponseofthewebservicestobedisplayedinauser-
friendlymanner.Wecouldhavedonethisformattinginsidethepromisefulfillmentcallback.Usingaseparatefunctionmakesthecodesignificantlycleaner.
privateformatModel(data){
data.Change=data.Change.toFixed(2);
data.ChangePercent=data.ChangePercent.toFixed(2);
data.Timestamp=newDate(data.Timestamp).toLocaleDateString();
data.MarketCap=(data.MarketCap/1000000).toFixed(2)+"M.";
data.ChangePercentYTD=data.ChangePercentYTD.toFixed(2);
return{quote:data};
}
}
export{QuoteModel};
ImplementingthesymbolviewLet'screateanewfilenamedsymbol_view.tsundertheapp/viewsdirectory.TheSymbolViewviewreceivesthestockdataformattedbytheQuoteModelmodelthroughthemediatorusingtheapp.view.symbol.renderevent:
///<referencepath="../../framework/interfaces"/>
import{View,AppEvent,ViewSettings}from"../../framework/framework";
@ViewSettings("./source/app/templates/symbol.hbs","#outlet")
classSymbolViewextendsViewimplementsIView{
constructor(metiator:IMediator){
super(metiator);
}
ThisviewisjustlikeMarketView;itsubscribestosomeeventsusingtheinitializemethod,andlaterdisposesofthoseeventsusingthedisposemethod.TheSymbolViewviewcanalsoinitializeanddisposeofusereventsusingthebindDomEventsandunbindDomEventsmethods.
However,thereisonesignificantdifferencebetweenSymbolViewandMarketView.AfterthepromisereturnedbyrenderAsynchasbeenfulfilledandtheusereventshavebeeninitialized,theexecutionflowispassedtoanothermodelviatheapp.model.chart.changeevent.Atthispoint,thestockquotescreenisvisiblebutitismissingthechart.
initialize():void{
this.subscribeToEvents([
newAppEvent("app.view.symbol.render",null,(e,model:any)=>{
this.renderAsync(model)
.then((model)=>{
//setDOMevents
this.bindDomEvents(model);
//passcontroltochartView
this.triggerEvent(newAppEvent("app.model.chart.change",
model.quote.Symbol,null));
})
.catch((e)=>{
this.triggerEvent(newAppEvent("app.error",e,null));
});
}),
]);
}
publicdispose():void{
this.unbindDomEvents();
this.unsubscribeToEvents();
}
//initializesDOMevents
protectedbindDomEvents(model:any){
varscope=$(this._container);
//setDOMeventshere
}
//disposesDOMevents
protectedunbindDomEvents(){
varscope=this._container;
//killDOMeventshere
}
}
export{SymbolView};
ImplementingthechartmodelLet'screateanewfilenamedchart_model.tsundertheapp/modelsdirectory.Thisisthelastmodelthatwewillimplement:
///<referencepath="../../framework/interfaces"/>
import{Model,AppEvent,ModelSettings}from"../../framework/framework";
@ModelSettings("http://dev.markitondemand.com/Api/v2/InteractiveChart/jsonp")
classChartModelextendsModelimplementsIModel{
constructor(metiator:IMediator){
super(metiator);
}
//listentomodelevents
publicinitialize(){
this.subscribeToEvents([
newAppEvent("app.model.chart.change",null,(e,args)=>{
this.onChange(args);})
]);
}
//disposemodelevents
publicdispose(){
this.unsubscribeToEvents();
}
Thistime,wewillneedtoformatboththerequestandtheresponse.WeneedtoencodetherequestparameterbecausethewebservicerequiresagroupofsettingsthatcannotbesentasparametersintheURLwithoutencodingitfirst.
TheonChangemethodusesthebrowser'sJSON.stringifyfunctiontotransformtherequiredwebservicearguments(aJSONobject)intoastring.Thestringisthenencodedusingthebrowser'sencodeURIComponentfunctionsoitcanbeusedasaparameterintheURL.
TheresponseisformattedusingamethodnamedformatModel:
privateonChange(args):void{
//formatargs(moreinfoathttp://dev.markitondemand.com/)
varp={
Normalized:false,
NumberOfDays:365,
DataPeriod:"Day",
Elements:[
{Symbol:args,Type:"price",Params:["ohlc"]}
]
};
varqueryString="parameters="+encodeURIComponent(JSON.stringify(p));
this.getAsync("jsonp",queryString)
.then((data)=>{
//formatdata
varchartData=this.formatModel(args,data);
//passcontrolltothemarketview
this.triggerEvent(newAppEvent("app.view.chart.render",chartData,
null));
})
.catch((e)=>{
//passcontroltotheglobalerrorhandler
this.triggerEvent(newAppEvent("app.error",e,null));
});
}
Thisfunctionisusedtoformattheresponsefromdev.markitondemand.com,soitcanbeusedbyHighchartswithease.Highchartsisalibrarythatallowustorendergraphsontheclientside:
privateformatModel(symbol,data){
//moreinfoathttp://dev.markitondemand.com/
//andhttp://www.highcharts.com/demo/line-time-series
varchartData={
title:symbol,
series:[]
};
varseries=[
{name:"open",data:data.Elements[0].DataSeries.open.values},
{name:"close",data:data.Elements[0].DataSeries.close.values},
{name:"high",data:data.Elements[0].DataSeries.high.values},
{name:"low",data:data.Elements[0].DataSeries.low.values}
];
for(vari=0;i<series.length;i++){
varserie={
name:series[i].name,
data:[]
}
for(varj=0;j<series[i].data.length;j++){
varval=series[i].data[j];
vard=newDate(data.Dates[j]).getTime();
serie.data.push([d,val]);
}
chartData.series.push(serie);
}
returnchartData;
}
}
export{ChartModel};
ImplementingthechartviewLet'screateanewfilenamedchart_view.tsundertheapp/viewsdirectory.Thisisthelastviewthatwewillimplement.Thisviewisalmostidenticaltothepreviousones,butthereisonesignificantdifference.AsthechartisrenderedbyHighchartsandnotHandlebars,wewillavoidpassingatemplateURLtotheViewSettingsdecorator:
///<referencepath="../../framework/interfaces"/>
import{View,AppEvent,ViewSettings}from"../../framework/framework";
@ViewSettings(null,"#chart_container")
classChartViewextendsViewimplementsIView{
constructor(metiator:IMediator){
super(metiator);
}
TheChartViewviewissubscribedtotheapp.view.chart.renderevent.TheeventhandlerisinvokedwhentheChartModelmodelhasbeenloadedandformatted,butsincewedon'tneedtorenderaHandlebarstemplate,wewillnotinvoketherenderAsyncmethodhere(aswedidinallthepreviousviews),andwewillinvokeamethodnamedrenderChartinstead:
initialize():void{
this.subscribeToEvents([
newAppEvent("app.view.chart.render",null,(e,model:any)=>{
this.renderChart(model);
this.bindDomEvents(model);
}),
]);
}
publicdispose():void{
this.unbindDomEvents();
this.unsubscribeToEvents();
}
//initializesDOMevents
protectedbindDomEvents(model:any){
varscope=$(this._container);
//setDOMeventshere
}
//disposesDOMevents
protectedunbindDomEvents(){
varscope=this._container;
//killDOMeventshere
}
TherenderChartmethodusestheHighchartsAPI(http://api.highcharts.com/highcharts)totransformthedatareturnedbyChartModelintoanicelookinginteractivechart:
privaterenderChart(model){
$(this._container).highcharts({
chart:{
zoomType:'x'
},
title:{
text:model.title
},
subtitle:{
text:'Clickanddragintheplotareatozoomin'
},
xAxis:{
type:'datetime'
},
yAxis:{
title:{
text:'Price'
}
},
legend:{
enabled:true
},
tooltip:{
shared:true,
crosshairs:true
},
plotOptions:{
area:{
marker:{
radius:0
},
lineWidth:0.1,
threshold:null
}
},
series:model.series
});
}
}
export{ChartView};
TestingtheapplicationWecantestthisapplicationusingthesamesetoftoolsthatweusedinthepreviouschaptersofthisbook.Asyoualreadyknow,inordertorunourunittest,weneedtocreateaGulptasklikethefollowingone:
gulp.task("run-unit-test",function(cb){
karma.start({
configFile:"karma.conf.js",
singleRun:true
},cb);
});
WehaveusedtheKarmatestrunner,andweneedtosetitsconfigurationusingthekarma.conf.jsfile.Thekarma.conf.jsfileisalmostidenticaltotheonethatweusedinChapter7,ApplicationTesting,andwillnotbeincludedhereforthesakeofbrevity.
Wealsoneedatasktorunsomeend-to-endtests:
gulp.task('run-e2e-test',function(){
returngulp.src('')
.pipe(nightwatch({
configFile:'nightwatch.json'
}));
});
Thenightwatch.jsonfileisalmostidenticaltheonethatweusedinChapter7,ApplicationTesting,andthuswillnotbeincludedhere.
Refertothecompanionsourcecodetoseethecontentofnightwatch.jsonandthekarma.conf.jsfile,aswellassomeexamplesofunittestsandE2Etests.
PreparingtheapplicationforaproductionreleaseNowthattheapplicationhasbeenimplementedandtested,wecanprepareitforreleaseinaproductionenvironment.
Inthissection,wewillimplementtwoGulptasks.ThefirsttaskisusedtocompresstheoutputJavaScriptcode.CompressingtheJavaScriptcodewillimproveboththeloadingandexecutionperformanceofourapplication:
gulp.task("compress",function(){
returngulp.src("bundled/source/bundle.js")
.pipe(uglify({preserveComments:false}))
.pipe(gulp.dest("dist/"))
});
ThesecondGulptaskthatwewillimplementisusedtoaddacopyrightheader.Thetaskusessomeofthefieldsfromthenpmconfigurationfile(package.json)togenerateastring,whichcontainsthecopyrightdetails.ThestringisthenaddedtothetopofthecompressedJavaScriptfilethatwasgeneratedbytheprevioustask:
gulp.task("header",function(){
varpkg=require("package.json");
varbanner=["/**",
"*<%=pkg.name%>v.<%=pkg.version%>-<%=pkg.description%>",
"*Copyright(c)2015<%=pkg.author%>",
"*<%=pkg.license%>",
"*<%=pkg.homepage%>",
"*/",
""].join("\n");
returngulp.src("dist/bundle.js")
.pipe(header(banner,{pkg:pkg}))
.pipe(gulp.dest("dist/"));
});
WecouldalsocreatesomeextraGulptaskstoimprovetheperformanceofourapplicationfurther.Forexample,wecouldcreateatasktogenerateacachemanifest(asimpletextfilethatliststheresourcesthebrowsershouldcacheforofflineaccess)toimplementclient-sidecaching.
SummaryInthischapter,wecreatedanMVCapplicationthatallowedustofindouthowtheNASDAQandNYSEstocksweredoingonaparticularday.Thisapplicationisasingle-pagewebapplication,anditsarchitecturemakesitscomponentseasytoextend,reuse,maintain,andtest.
Theapplicationshowcasesmanyoftheconceptsthatwecoveredinthepreviouschapters.Wecreatedanautomatedbuild,andweusedmanyfunctions,classes,modules,andothercorelanguagefeatures.Wealsousedmodulesandworkedwithsomeasynchronousfunctions,andweusedsomedecorators.Theautomatedbuildperformssometasksthatwillhelpustoimprovetheapplicationperformanceandensuresthatitworkscorrectly.
ThisapplicationisnotaverylargeJavaScriptapplication.However,theapplicationislargeenoughtoshowcasethewaysinwhichTypeScriptcanhelpusdevelopcomplexapplicationsthatarereadytogrowandadapttochangeswithease.
IhopeyouenjoyedthisbookandfeeleagertolearnmoreaboutTypeScript.
IfyouareupforachallengeandyouwouldliketoreinforceyourTypeScriptskills,trythefollowing:
Youcantrytoachieve100percenttestcoverageintheapplicationthatwehavedevelopedoverthelasttwochapters.YoucanimproveourcustomSPAtheframeworkandintroducefeaturessuchasusinganIoCcontainerorusingaunidirectionaldataflow.
YoucanalsovisittheTodoMVCwebsite(http://todomvc.com/)tofindexamplesofintegrationbetweenTypeScriptandpopularMV*frameworks,suchasEmber.jsorBackbone.js,tolearnhowtouseaproduction-readySPAframework.
Part2.Module2TypeScriptDesignPatterns
BoostyourdevelopmentefficiencybylearningaboutdesignpatternsinTypeScript
Chapter1.ToolsandFrameworksWecouldalwaysusethehelpofrealcodetoexplainthedesignpatternswe'llbediscussing.Inthischapter,we'llhaveabriefintroductiontothetoolsandframeworksthatyoumightneedifyouwanttohavesomepracticewithcompleteworkingimplementationsofthecontentsofthisbook.
Inthischapter,we'llcoverthefollowingtopics:
InstallingNode.jsandTypeScriptcompilerPopulareditorsorIDEsforTypeScriptConfiguringaTypeScriptprojectAbasicworkflowthatyoumightneedtoplaywithyourownimplementationsofthedesignpatternsinthisbook
InstallingtheprerequisitesThecontentsofthischapterareexpectedtoworkonallmajorandup-to-datedesktopoperatingsystems,includingWindows,OSX,andLinux.
AsNode.jsiswidelyusedasaruntimeforserverapplicationsaswellasfrontendbuildtools,wearegoingtomakeitthemainplaygroundofcodeinthisbook.
TypeScriptcompiler,ontheotherhand,isthetoolthatcompilesTypeScriptsourcefilesintoplainJavaScript.It'savailableonmultipleplatformsandruntimes,andinthisbookwe'llbeusingtheNode.jsversion.
InstallingNode.jsInstallingNode.jsshouldbeeasyenough.Butthere'ssomethingwecoulddotominimizeincompatibilityovertimeandacrossdifferentenvironments:
Version:We'llbeusingNode.js6withnpm3built-ininthisbook.(ThemajorversionofNode.jsmayincreaserapidlyovertime,butwecanexpectminimumbreakingchangesdirectlyrelatedtoourcontents.Feelfreetotryanewerversionifit'savailable.)Path:IfyouareinstallingNode.jswithoutapackagemanager,makesuretheenvironmentvariablePATHisproperlyconfigured.
Openaconsole(acommandpromptorterminal,dependingonyouroperatingsystem)andmakesureNode.jsaswellasthebuilt-inpackagemanagernpmisworking:
$node-v
6.x.x
$npm-v
3.x.x
InstallingTypeScriptcompilerTypeScriptcompilerforNode.jsispublishedasannpmpackagewithcommandlineinterface.Toinstallthecompiler,wecansimplyusethenpminstallcommand:
$npminstalltypescript-g
Option-gmeansaglobalinstallation,sothattscwillbeavailableasacommand.Nowlet'smakesurethecompilerworks:
$tsc-v
Version2.x.x
Note
YoumaygetaroughlistoftheoptionsyourTypeScriptcompilerprovideswithswitch-h.Takingalookintotheseoptionsmayhelpyoudiscoversomeusefulfeatures.
Beforechoosinganeditor,let'sprintoutthelegendaryphrase:
1. Savethefollowingcodetofiletest.ts:
functionhello(name:string):void{
console.log(`hello,${name}!`);
}
hello('world');
2. Changetheworkingdirectoryofyourconsoletothefoldercontainingthecreatedfile,andcompileitwithtsc:
$tsctest.ts
3. Withluck,youshouldhavethecompiledJavaScriptfileastest.js.ExecuteitwithNode.jstogettheceremonydone:
$nodetest.js
hello,world!
Herewego,ontheroadtoretireyourCTO.
ChoosingahandyeditorAcompilerwithoutagoodeditorwon'tbeenough(ifyouarenotabelieverofNotepad).ThankstotheeffortsmadebytheTypeScriptcommunity,thereareplentyofgreateditorsandIDEsreadyforTypeScriptdevelopment.
However,thechoiceofaneditorcouldbemuchaboutpersonalpreferences.Inthissection,we'lltalkabouttheinstallationandconfigurationofVisualStudioCodeandSublimeText.ButotherpopulareditorsorIDEsforTypeScriptwillalsobelistedwithbriefintroductions.
VisualStudioCodeVisualStudioCodeisafreelightweighteditorwritteninTypeScript.Andit'sanopensourceandcross-platformeditorthatalreadyhasTypeScriptsupportbuilt-in.
YoucandownloadVisualStudioCodefromhttps://code.visualstudio.com/andtheinstallationwillprobablytakenomorethan1minute.
ThefollowingscreenshotshowsthedebugginginterfaceofVisualStudioCodewithaTypeScriptsourcefile:
ConfiguringVisualStudioCode
AsCodealreadyhasTypeScriptsupportbuilt-in,extraconfigurationsareactuallynotrequired.ButiftheversionofTypeScriptcompileryouusetocompilethesourcecodediffersfromwhatCodehasbuilt-in,itcouldresultinunconformitybetweeneditingandcompiling.
Tostayawayfromtheundesiredissuesthiswouldbring,weneedtoconfigureTypeScriptSDKusedbyVisualStudioCodemanually:
1. PressF1,typeOpenUserSettings,andenter.VisualStudioCodewillopenthesettings
JSONfilebythesideofaread-onlyJSONfilecontainingallthedefaultsettings.2. Addthefieldtypescript.tsdkwiththepathofthelibfolderundertheTypeScript
packagewepreviouslyinstalled:
1.Executethecommandnpmroot-ginyourconsoletogettherootofglobalNode.jsmodules.
2.Appendtherootpathwith/typescript/libastheSDKpath.
Note
YoucanalsohaveaTypeScriptpackageinstalledlocallywiththeproject,andusethelocalTypeScriptlibpathforVisualStudioCode.(Youwillneedtousethelocallyinstalledversionforcompilingaswell.)
Openingafolderasaworkspace
VisualStudioCodeisafile-andfolder-basededitor,whichmeansyoucanopenafileorafolderandstartwork.
ButyoustillneedtoproperlyconfiguretheprojecttotakethebestadvantageofCode.ForTypeScript,theprojectfileistsconfig.json,whichcontainsthedescriptionofsourcefilesandcompileroptions.Knowlittleabouttsconfig.json?Don'tworry,we'llcometothatlater.
HerearesomefeaturesofVisualStudioCodeyoumightbeinterestedin:
Tasks:Basictaskintegration.Youcanbuildyourprojectwithoutleavingtheeditor.Debugging:Node.jsdebuggingwithsourcemapsupport,whichmeansyoucandebugNode.jsapplicationswritteninTypeScript.Git:BasicGitintegration.Thismakescomparingandcommittingchangeseasier.
Configuringaminimumbuildtask
ThedefaultkeybindingforabuildtaskisCtrl+Shift+Borcmd+Shift+BonOSX.Bypressingthesekeys,youwillgetapromptnotifyingyouthatnotaskrunnerhasbeenconfigured.ClickConfigureTaskRunnerandthenselectaTypeScriptbuildtasktemplate(eitherwithorwithoutthewatchmodeenabled).Atasks.jsonfileunderthe.vscodefolderwillbecreatedautomaticallywithcontentsimilartothefollowing:
{
"version":"0.1.0",
"command":"tsc",
"isShellCommand":true,
"args":["-w","-p","."],
"showOutput":"silent",
"isWatching":true,
"problemMatcher":"$tsc-watch"
}
Nowcreateatest.tsfilewithsomehello-worldcodeandrunthebuildtaskagain.YoucaneitherpresstheshortcutwementionedbeforeorpressCtrl/Cmd+P,typetasktsc,andenter.
Ifyouweredoingthingscorrectly,youshouldbeseeingtheoutputtest.jsbythesideoftest.ts.
Therearesomeusefulconfigurationsfortaskingthatcan'tbecovered.YoumayfindmoreinformationonthewebsiteofVisualStudioCode:https://code.visualstudio.com/.
Frommyperspective,VisualStudioCodedeliversthebestTypeScriptdevelopmentexperienceintheclassofcodeeditors.Butifyouarenotafanofit,TypeScriptisalsoavailablewithofficialsupportforSublimeText.
SublimeTextwithTypeScriptpluginSublimeTextisanotherpopularlightweighteditoraroundthefieldwithamazingperformance.
ThefollowingimageshowshowTypeScriptIntelliSenseworksinSublimeText:
TheTypeScriptteamhasofficiallybuiltapluginforSublimeText(version3preferred),andyoucanfindadetailedintroduction,includingusefulshortcuts,intheirGitHubrepositoryhere:https://github.com/Microsoft/TypeScript-Sublime-Plugin.
Note
TherearestillsomeissueswiththeTypeScriptpluginforSublimeText.ItwouldbenicetoknowaboutthembeforeyoustartwritingTypeScriptwithSublimeText.
InstallingPackageControl
PackageControlisdefactopackagemanagerforSublimeText,withwhichwe'llinstalltheTypeScriptplugin.
Ifyoudon'thavePackageControlinstalled,performthefollowingsteps:
1. ClickPreferences>BrowsePackages...,itopenstheSublimeTextpackagesfolder.2. BrowseuptotheparentfolderandthenintotheInstallPackagesfolder,anddownload
thefilebelowintothisfolder:https://packagecontrol.io/Package%20Control.sublime-package
3. RestartSublimeTextandyoushouldnowhaveaworkingpackagemanager.
NowweareonlyonestepawayfromIntelliSenseandrefactoringwithSublimeText.
InstallingtheTypeScriptplugin
WiththehelpofPackageControl,it'seasytoinstallaplugin:
1. OpentheSublimeTexteditor;pressCtrl+Shift+PforWindowsandLinuxorCmd+Shift+PforOSX.
2. TypeInstallPackageinthecommandpalette,selectPackageControl:InstallPackageandwaitforittoloadthepluginrepositories.
3. TypeTypeScriptandselecttoinstalltheofficialplugin.
NowwehaveTypeScriptreadyforSublimeText,cheers!
LikeVisualStudioCode,unmatchedTypeScriptversionsbetweenthepluginandcompilercouldleadtoproblems.Tofixthis,youcanaddthefield"typescript_tsdk"withapathtotheTypeScriptlibintheSettings-Userfile.
OthereditororIDEoptionsVisualStudioCodeandSublimeTextarerecommendedduetotheireaseofuseandpopularityrespectively.Buttherearemanygreattoolsfromtheeditorclasstofull-featuredIDE.
Thoughwe'renotgoingthroughthesetupandconfigurationofthosetools,youmightwanttotrythemoutyourself,especiallyifyouarealreadyworkingwithsomeofthem.
However,theconfigurationfordifferenteditorsandIDEs(especiallyIDEs)coulddiffer.ItisrecommendedtouseVisualStudioCodeorSublimeTextforgoingthroughtheworkflowandexamplesinthisbook.
AtomwiththeTypeScriptplugin
Atomisacross-platformeditorcreatedbyGitHub.Ithasanotablecommunitywithplentyofusefulplugins,includingatom-typescript.atom-typescriptistheresultofthehardworkofBasaratAliSyed,andit'susedbymyteambeforeVisualStudioCode.IthasmanyhandyfeaturesthatVisualStudioCodedoesnothaveyet,suchasmodulepathsuggestion,compileonsave,andsoon.
LikeVisualStudioCode,Atomisalsoaneditorbasedonwebtechnologies.Actually,theshellusedbyVisualStudioCodeisexactlywhat'susedbyAtom:Electron,anotherpopularprojectbyGitHub,forbuildingcross-platformdesktopapplications.
Atomisproudofbeinghackable,whichmeansyoucancustomizeyourownAtomeditorprettymuchasyouwant.
ThenyoumaybewonderingwhyweturnedtoVisualStudioCode.ThemainreasonisthatVisualStudioCodeisbeingbackedbythesamecompanythatdevelopsTypeScript,andanotherreasonmightbetheperformanceissuewithAtom.
Butanyway,Atomcouldbeagreatchoiceforastart.
VisualStudio
VisualStudioisoneofthebestIDEsinthemarket.Andyetithas,ofcourse,officialTypeScriptsupport.
SinceVisualStudio2013,acommunityversionisprovidedforfreetoindividualdevelopers,smallcompanies,andopensourceprojects.
IfyouarelookingforapowerfulIDEofTypeScriptonWindows,VisualStudiocouldbeawonderfulchoice.ThoughVisualStudiohasbuilt-inTypeScriptsupport,domakesureit'sup-to-date.And,usually,youmightwanttoinstallthenewestTypeScripttoolsforVisualStudio.
WebStorm
WebStormisoneofthemostpopularIDEsforJavaScriptdevelopers,andithashadanearlyadoptiontoTypeScriptaswell.
AdownsideofusingWebStormforTypeScriptisthatitisalwaysonestepslowercatchinguptothelatestversioncomparedtoothermajoreditors.UnlikeeditorsthatdirectlyusethelanguageserviceprovidedbytheTypeScriptproject,WebStormseemstohaveitsowninfrastructureforIntelliSenseandrefactoring.But,inreturn,itmakesTypeScriptsupportinWebStormmorecustomizableandconsistentwithotherfeaturesitprovides.
IfyoudecidetouseWebStormasyourTypeScriptIDE,pleasemakesuretheversionofsupportedTypeScriptmatcheswhatyouexpect(usuallythelatestversion).
GettingyourhandsontheworkflowAftersettingupyoureditor,wearereadytomovetoaworkflowthatyoumightusetopracticethroughoutthisbook.ItcanalsobeusedastheworkflowforsmallTypeScriptprojectsinyourdailywork.
Inthisworkflow,we'llwalkthroughthesetopics:
Whatisatsconfig.jsonfile,andhowcanyouconfigureaTypeScriptprojectwithit?TypeScriptdeclarationfilesandthetypingscommand-linetoolHowtowritetestsrunningunderMocha,andhowtogetcoverageinformationusingIstanbulHowtotestinbrowsersusingKarma
ConfiguringaTypeScriptprojectTheconfigurationsofaTypeScriptprojectcandifferforavarietyofreasons.Butthegoalsremainclear:weneedtheeditoraswellasthecompilertorecognizeaprojectanditssourcefilescorrectly.Andtsconfig.jsonwilldothejob.
Introductiontotsconfig.json
ATypeScriptprojectdoesnothavetocontainatsconfig.jsonfile.However,mosteditorsrelyonthisfiletorecognizeaTypeScriptprojectwithspecifiedconfigurationsandtoproviderelatedfeatures.
Atsconfig.jsonfileacceptsthreefields:compilerOptions,files,andexclude.Forexample,asimpletsconfig.jsonfilecouldbelikethefollowing:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"rootDir":"src",
"outDir":"out"
},
"exclude":[
"out",
"node_modules"
]
}
Or,ifyouprefertomanagethesourcefilesmanually,itcouldbelikethis:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"rootDir":"src",
"outDir":"out"
},
"files":[
"src/foo.ts",
"src/bar.ts"
]
}
Previously,whenweusedtsc,weneededtospecifythesourcefilesexplicitly.Now,withtsconfig.json,wecandirectlyruntscwithoutarguments(orwith-w/--watchifyouwantincrementalcompilation)inafolderthatcontainstsconfig.json.
Compileroptions
AsTypeScriptisstillevolving,itscompileroptionskeepchanging,withnewfeaturesandupdates.AninvalidoptionmaybreakthecompilationoreditorfeaturesforTypeScript.When
readingtheseoptions,keepinmindthatsomeofthemmighthavebeenchanged.
Thefollowingoptionsareusefulonesoutofthelist.
target
targetspecifiestheexpectedversionofJavaScriptoutputs.Itcouldbees5(ECMAScript5),es6(ECMAScript6/2015),andsoon.
Features(especiallyECMAScriptpolyfills)thatareavailableindifferentcompilationtargetsvary.Forexample,beforeTypeScript2.1,featuressuchasasync/awaitwereavailableonlywhentargetingES6.
ThegoodnewsisthatNode.js6withthelatestV8enginehassupportedmostES6features.AndthelatestbrowsershavealsogreatES6support.SoifyouaredevelopingaNode.jsapplicationorabrowserapplicationthat'snotrequiredforbackwardcompatibilities,youcanhaveyourconfigurationtargetES6.
module
BeforeES6,JavaScripthadnostandardmodulesystem.Varietiesofmoduleloadersaredevelopedfordifferentscenarios,suchascommonjs,amd,umd,system,andsoon.
IfyouaredevelopingaNode.jsapplicationorannpmpackage,commonjscouldbethevalueofthisoption.Actually,withthehelpofmodernpackagingtoolssuchaswebpackandbrowserify,commonjscouldalsobeanicechoiceforbrowserprojectsaswell.
declaration
Enablethisoptiontogenerate.d.tsdeclarationfilesalongwithJavaScriptoutputs.Declarationfilescouldbeusefulasthetypeinformationsourceofadistributedlibrary/framework;itcouldalsobehelpfulforsplittingalargerprojecttoimprovecompilingperformanceanddivisioncooperation.
sourceMap
Byenablingthisoption,TypeScriptcompilerwillemitsourcemapsalongwithcompiledJavaScript.
jsx
TypeScriptprovidesbuilt-insupportforReactJSX(.tsx)files.Byspecifyingthisoptionwithvaluereact,TypeScriptcompilerwillcompile.tsxfilestoplainJavaScriptfiles.Orwithvaluepreserve,itwilloutput.jsxfilessoyoucanpost-processthesefileswithotherJSXcompilers.
noEmitOnError
Bydefault,TypeScriptwillemitoutputsnomatterwhethertypeerrorsarefoundornot.Ifthis
isnotwhatyouwant,youmaysetthisoptiontotrue.
noEmitHelpers
WhencompilinganewerECMAScriptfeaturetoalowertargetversionofJavaScript,TypeScriptcompilerwillsometimesgeneratehelperfunctionssuchas__extends(ES6tolowerversions),and__awaiter(ES7tolowerversions).
Duetocertainreasons,youmaywanttowriteyourownhelperfunctions,andpreventTypeScriptcompilerfromemittingthesehelpers.
noImplicitAny
AsTypeScriptisasupersetofJavaScript,itallowsvariablesandparameterstohavenotypenotation.However,itcouldhelptomakesureeverythingistyped.
Byenablingthisoption,TypeScriptcompilerwillgiveerrorsifthetypeofavariable/parameterisnotspecifiedandcannotbeinferredbyitscontext.
experimentalDecorators*
Asdecorators,atthetimeofwritingthisbook,hasnotyetreachedastablestageofthenewECMAScriptstandard,youneedtoenablethisoptiontousedecorators.
emitDecoratorMetadata*
Runtimetypeinformationcouldsometimesbeuseful,butTypeScriptdoesnotyetsupportreflection(maybeitneverwill).Luckily,wegetdecoratormetadatathatwillhelpundercertainscenarios.
Byenablingthisoption,TypeScriptwillemitdecoratorsalongwithaReflect.metadata()decoratorwhichcontainsthetypeinformationofthedecoratedtarget.
outDir
Usually,wedonotwantcompiledfilestobeinthesamefolderofsourcecode.ByspecifyingoutDir,youcantellthecompilerwhereyouwouldwantthecompiledJavaScriptfilestobe.
outFile
Forsmallbrowserprojects,wemightwanttohavealltheoutputsconcatenatedasasinglefile.Byenablingthisoption,wecanachievethatwithoutextrabuildtools.
rootDir
TherootDiroptionistospecifytherootofthesourcecode.Ifomitted,thecompilerwouldusethelongestcommonpathofsourcefiles.Thismighttakesecondstounderstand.
Forexample,ifwehavetwosourcefiles,src/foo.tsandsrc/bar.ts,andatsconfig.json
fileinthesamedirectoryofthesrcfolder,theTypeScriptcompilerwillusesrcastherootDir,sowhenemittingfilestotheoutDir(let'ssayout),theywillbeout/foo.jsandout/bar.js.
However,ifweaddanothersourcefiletest/test.tsandcompileagain,we'llgetthoseoutputslocatedinout/src/foo.js,out/src/bar.js,andout/test/test.jsrespectively.Whencalculatingthelongestcommonpath,declarationfilesarenotinvolvedastheyhavenooutput.
Usually,wedon'tneedtospecifyrootDir,butitwouldbesafertohaveitconfigured.
preserveConstEnums
EnumisausefultoolprovidedbyTypeScript.Whencompiled,it'sintheformofanEnum.memberexpression.Constantenum,ontheotherhand,emitsnumberliteralsdirectly,whichmeanstheEnumobjectisnolongernecessary.
AndthusTypeScript,bydefault,willremovethedefinitionsofconstantenumsinthecompiledJavaScriptfiles.
Byenablingthisoption,youcanforcethecompilertokeepthesedefinitionsanyway.
strictNullChecks
TypeScript2.1makesitpossibletoexplicitlydeclareatypewithundefinedornullasitssubtype.Andthecompilercannowperformmorethoroughtypecheckingforemptyvaluesifthisoptionisenabled.
stripInternal*
Whenemittingdeclarationfiles,therecouldbesomethingyou'llneedtouseinternallybutwithoutabetterwaytospecifytheaccessibility.Bycommentingthiscodewith/**@internal*/(JSDocannotation),TypeScriptcompilerthenwon'temitthemtodeclarationfiles.
isolatedModules
Byenablingthisoption,thecompilerwillunconditionallyemitimportsforunresolvedfiles.
Note
Optionssuffixedwith*areexperimentalandmighthavealreadybeenremovedwhenyouarereadingthisbook.Foramorecompleteandup-to-datecompileroptionslist,pleasecheckouthttp://www.typescriptlang.org/docs/handbook/compiler-options.html.
Addingsourcemapsupport
Sourcemapscanhelpalotwhiledebugging,nomatterforadebuggerorforerrorstack
tracesfromalog.
Tohavesourcemapsupport,weneedtoenablethesourceMapcompileroptionintsconfig.json.Extraconfigurationsmightbenecessarytomakeyourdebuggerworkwithsourcemaps.
Forerrorstacktraces,wecanusethehelpofthesource-map-supportpackage:
$npminstallsource-map-support--save
Toputitintoeffect,youcanimportthispackagewithitsregistersubmoduleinyourentryfile:
import'source-map-support/register';
DownloadingdeclarationsusingtypingsJavaScripthasalargeandboomingecosystem.AsthebridgeconnectingTypeScriptandotherJavaScriptlibrariesandframeworks,declarationfilesareplayingaveryimportantrole.
Withthehelpofdeclarationfiles,TypeScriptdevelopercanuseexistingJavaScriptlibrarieswithnearlythesameexperienceaslibrarieswritteninTypeScript.
ThankstotheeffortsoftheTypeScriptcommunity,almosteverypopularJavaScriptlibraryorframeworkgotitsdeclarationfilesonaprojectcalledDefinitelyTyped.Andtherehasalreadybeenatoolcalledtsdfordeclarationfilemanagement.Butsoon,peoplerealizedthelimitationofasinglehugerepositoryforeverything,aswellastheissuestsdcannotsolvenicely.ThentypingsisgentlybecomingthenewtoolforTypeScriptdeclarationfilemanagement.
Installingtypings
typingsisjustanotherNode.jspackagewithacommand-lineinterfacelikeTypeScriptcompiler.Toinstalltypings,simplyexecutethefollowing:
$npminstalltypings-g
Tomakesureithasbeeninstalledcorrectly,youcannowtrythetypingscommandwithargument--version:
$typings--version
1.x.x
Downloadingdeclarationfiles
CreateabasicNode.jsprojectwithapropertsconfig.json(moduleoptionsetascommonjs),andatest.tsfile:
import*asexpressfrom'express';
Withoutthenecessarydeclarationfiles,thecompilerwouldcomplainwithCannotfindmoduleexpress.And,actually,youcan'tevenuseNode.jsAPIssuchasprocess.exit()orrequireNode.jsmodules,becauseTypeScriptitselfjustdoesnotknowwhatNode.jsis.
Tobeginwith,we'llneedtoinstalldeclarationfilesofNode.jsandExpress:
$typingsinstallenv~node--global
$typingsinstallexpress
Ifeverythinggoesfine,typingsshould'vedownloadedseveraldeclarationfilesandsavedthemtofoldertypings,includingnode.d.ts,express.d.ts,andsoon.AndIguessyou'vealreadynoticedthedependencyrelationshipexistingondeclarationfiles.
Note
IfthisisnotworkingforyouandtypingscomplainswithUnabletofind"express"("npm")intheregistrythenyoumightneedtodoitthehardway-tomanuallyinstallExpressdeclarationfilesandtheirdependenciesusingthefollowingcommand:$typingsinstalldt~<package-name>--global
ThereasonforthatisthecommunitymightstillbemovingfromDefinitelyTypedtothetypingsregistry.Theprefixdt~tellstypingstodownloaddeclarationfilesfromDefintelyTyped,and--globaloptiontellstypingstosavethesedeclarationfilesasambientmodules(namelydeclarationswithmodulenamespecified).
typingshasseveralregistries,andthedefaultoneiscallednpm(pleaseunderstandthisnpmregistryisnotthenpmpackageregistry).So,ifnoregistryisspecifiedwith<source>~prefixor--sourceoption,itwilltrytofinddeclarationfilesfromitsnpmregistry.Thismeansthattypingsinstallexpressisequivalenttotypingsinstallnpm~expressortypingsinstallexpress--sourcenpm.
Whiledeclarationfilesfornpmpackagesareusuallyavailableonthenpmregistry,declarationfilesfortheenvironmentareusuallyavailableontheenv.registry.Asthosedeclarationsareusuallyglobal,a--globaloptionisrequiredforthemtoinstallcorrectly.
Option"save"
typingsactuallyprovidesa--saveoptionforsavingthetypingnamesandfilesourcestotypings.json.However,inmyopinion,thisoptionisnotpracticallyuseful.
It'sgreattohavethemostpopularJavaScriptlibrariesandframeworkstyped,butthesedeclarationfiles,especiallydeclarationsnotfrequentlyused,canbeinaccurate,whichmeansthere'safairchancethatyouwillneedtoeditthesefilesyourself.
Itwouldbenicetocontributedeclarations,butitwouldalsobemoreflexibletohavetypingsmmanagedbysourcecontrolaspartoftheprojectcode.
TestingwithMochaandIstanbulTestingcouldbeanimportantpartofaproject,whichensuresfeatureconsistencyanddiscoversbugsearlier.Itiscommonthatachangemadeforonefeaturecouldbreakanotherworkingpartoftheproject.Arobustdesigncouldminimizethechancebutwestillneedteststomakesure.
Itcouldleadtoanendlessdiscussionabouthowtestsshouldbewrittenandthereareinterestingcodedesigntechniquessuchastest-drivendevelopment(TDD);thoughtherehasbeenalotofdebatesaroundit,itstillworthknowingandmayinspireyouincertainways.
MochaandChai
MochahasbeenoneofthemostpopulartestframeworksforJavaScript,whileChaiisagoodchoiceasanassertionlibrary.Tomakelifeeasier,youmaywritetestsforyourownimplementationsofcontentsthroughthisbookusingMochaandChai.
ToinstallMocha,simplyrunthefollowingcommand,anditwilladdmochaasaglobalcommand-linetooljustliketscandtypings:
$npminstallmocha-g
Chai,ontheotherhand,isusedasamoduleofaproject,andshouldbeinstalledundertheprojectfolderasadevelopmentdependency:
$npminstallchai--save-dev
Chaisupportsshouldstyleassertion.Byinvokingchai.should(),itaddstheshouldpropertytotheprototypeofObject,whichmeansyoucanthenwriteassertionssuchasthefollowing:
'foo'.should.not.equal('bar');
'typescript'.should.have.length(10);
WritingtestsinJavaScript
Byexecutingthecommandmocha,itwillautomaticallyruntestsinsidethetestfolder.BeforewestarttowritetestsinTypeScript,let'stryitoutinplainJavaScriptandmakesureit'sworking.
Createafiletest/starter.jsandsaveitwiththefollowingcode:
require('chai').should();
describe('somefeature',()=>{
it('shouldpass',()=>{
'foo'.should.not.equal('bar');
});
it('shoulderror',()=>{
(()=>{
thrownewError();
}).should.throw();
});
});
Runmochaundertheprojectfolderandyoushouldseealltestspassing.
WritingtestsinTypeScript
TestswritteninTypeScripthavetobecompiledbeforebeingrun;wheretoputthosefilescouldbeatrickyquestiontoanswer.
Somepeoplemightwanttoseparatetestswiththeirowntsconfig.json:
src/tsconfig.json
test/tsconfig.json
Theymayalsowanttoputoutputfilessomewherereasonable:
out/app/
out/test/
However,thiswillincreasethecostofbuildprocessmanagementforsmallprojects.So,ifyoudonotmindhavingsrcinthepathsofyourcompiledfiles,youcanhaveonlyonetsconfig.jsontogetthejobdone:
src/
test/
tsconfig.json
Thedestinationswillbeasfollows:
out/src/
out/test/
AnotheroptionIpersonallypreferistohavetestsinsideofsrc/test,andusethetestfolderundertheprojectrootforMochaconfigurations:
src/
src/test/
tsconfig.json
Thedestinationswillbeasfollows:
out/
out/test/
But,eitherway,we'llneedtoconfigureMochaproperlytodothefollowing:
Runtestsundertheout/testdirectoryConfiguretheassertionlibraryandothertoolsbeforestartingtoruntests
Toachievethese,wecantakeadvantageofthemocha.optsfileinsteadofspecifyingcommand-lineargumentseverytime.Mochawillcombinelinesinthemocha.optsfilewithothercommand-lineargumentsgivenwhilebeingloaded.
Createtest/mocha.optswiththefollowinglines:
--require./test/mocha.js
out/test/
Asyoumighthaveguessed,thefirstlineistotellMochatorequire./test/mocha.jsbeforestartingtorunactualtests.AndthesecondlinetellsMochawherethesetestsarelocated.
And,ofcourse,we'llneedtocreatetest/mocha.jscorrespondingly:
require('chai').should();
AlmostreadytowritetestsinTypeScript!ButTypeScriptcompilerdoesnotknowhowwouldfunctiondescribeoritbelike,soweneedtodownloaddeclarationfilesforMocha:
$typingsinstallenv~mocha--global
Nowwecanmigratethetest/starter.jsfiletosrc/test/starter.tswithnearlynochange,butremovingthefirstlinethatenablestheshouldstyleassertionofChai,aswehavealreadyputitintotest/mocha.js.
Compileandrun;buymeacupofcoffeeifitworks.Butitprobablywon't.We'vetalkedabouthowTypeScriptcompilerdeterminestherootofsourcefileswhenexplainingtherootDircompileroption.Aswedon'thaveanyTypeScriptfilesunderthesrcfolder(notincludingitssubfolders),TypeScriptcompilerusessrc/testastherootDir.Thusthecompiledtestfilesarenowundertheoutfolderinsteadoftheexpectedout/test.
Tofixthis,eitherexplicitlyspecifyrootDir,orjustaddthefirstnon-testTypeScriptfiletothesrcfolder.
GettingcoverageinformationwithIstanbul
Coveragecouldbeimportantformeasuringthequalityoftests.However,itmighttakemuchefforttoreachanumbercloseto100%,whichcouldbeaburdenfordevelopers.Tobalanceeffortsontestsandcodethatbringdirectvaluetotheproduct,therewouldgoanotherdebate.
InstallIstanbulvianpmjustaswiththeothertools:
$npminstallistanbul-g
ThesubcommandforIstanbultogeneratecodecoverageinformationisistanbulcover.ItshouldbefollowedbyaJavaScriptfile,butweneedtomakeitworkwithMocha,whichisacommand-linetool.Luckily,theentryoftheMochacommandis,ofcourse,aJavaScriptfile.
Tomakethemworktogether,we'llneedtoinstallalocal(insteadofglobal)versionofMochafortheproject:
$npminstallmocha--save-dev
Afterinstallation,we'llgetthefile_mochaundernode_modules/mocha/bin,whichistheJavaScriptentrywewerelookingfor.SonowwecanmakeIstanbulwork:
$istanbulcovernode_modules/mocha/bin/_mocha
Thenyoushould'vegotafoldernamedcoverage,andwithinitthecoveragereport.
Reviewingthecoveragereportisimportant;itcanhelpyoudecidewhetheryouneedtoaddtestsforspecificfeaturesandcodebranches.
TestinginrealbrowserswithKarmaWe'vetalkedabouttestingwithMochaandIstanbulforNode.jsapplications.Itisanimportanttopicfortestingcodethatrunsinabrowseraswell.
KarmaisatestrunnerforJavaScriptthatmakestestinginrealbrowsersonrealdevicesmucheasier.ItofficiallysupportstheMocha,Jasmine,andJUnittestingframeworks,butit'salsopossibleforKarmatoworkwithanyframeworkviaasimpleadapter.
Creatingabrowserproject
ATypeScriptapplicationthatrunsinbrowserscanbequitedifferentfromaNode.jsone.Butifyouknowwhattheprojectshouldlooklikeafterthebuild,youshouldalreadyhavecluesonhowtoconfigurethatproject.
Toavoidintroducingtoomanyconceptsandtechnologiesnotdirectlyrelated,Iwillkeepthingsassimpleaspossible:
We'renotgoingtousemoduleloaderssuchasRequire.jsWe'renotgoingtotouchthecodepackagingprocess
Thismeanswe'llgowithseparatedoutputfilesthatneedtobeputintoanHTMLfilewithascripttagmanually.Here'sthetsconfig.jsonwe'llbeplayingwith;noticethatwenolongerhavethemoduleoption,set:
{
"compilerOptions":{
"target":"es5",
"rootDir":"src",
"outDir":"out"
},
"exclude":[
"out",
"node_modules"
]
}
Thenlet'screatepackage.jsonandinstallpackagesmochaandchaiwiththeirdeclarations:
$npminit
$npminstallmochachai--save-dev
$typingsinstallenv~mocha--global
$typingsinstallchai
Andtobeginwith,let'sfillthisprojectwithsomesourcecodeandtests.
Createsrc/index.tswiththefollowingcode:
functiongetLength(str:string):number{
returnstr.length;
}
Andcreatesrc/test/test.tswithsometests:
describe('getlength',()=>{
it('"abc"shouldhavelength3',()=>{
getLength('abc').should.equal(3);
});
it('""shouldhavelength0',()=>{
getLength('').should.equal(0);
});
});
Again,inordertomaketheshouldstyleassertionwork,we'llneedtocallchai.should()beforetestsstart.Todoso,createfiletest/mocha.jsjustlikewedidintheNode.jsproject,thoughthecodelineslightlydiffers,aswenolongerusemodules:
chai.should();
Nowcompilethesefileswithtsc,andwe'vegotourprojectready.
InstallingKarma
KarmaitselfrunsonNode.js,andisavailableasannpmpackagejustlikeotherNode.jstoolswe'vebeenusing.ToinstallKarma,simplyexecutethenpminstallcommandintheprojectdirectory:
$npminstallkarma--save-dev
And,inourcase,wearegoingtohaveKarmaworkingwithMocha,Chai,andthebrowserChrome,sowe'llneedtoinstallrelatedplugins:
$npminstallkarma-mochakarma-chaikarma-chrome-launcher--save-dev
BeforeweconfigureKarma,itisrecommendedtohavekarma-cliinstalledgloballysothatwecanexecutethekarmacommandfromtheconsoledirectly:
$npminstallkarma-cli-g
ConfiguringandstartingKarma
TheconfigurationsaretotellKarmaaboutthetestingframeworksandbrowsersyouaregoingtouse,aswellasotherrelatedinformationsuchassourcefilesandteststorun.
TocreateaKarmaconfigurationfile,executekarmainitandansweritsquestions:
Testingframework:MochaRequire.js:noBrowsers:Chrome(addmoreifyoulike;besuretoinstallthecorrespondinglaunchers)Sourceandtestfiles:
test/mocha.js(thefileenablesshouldstyleassertion)out/*.js(sourcefiles)out/test/*.js(testfiles)
Filestoexclude:emptyWatchforchanges:yes
Nowyoushouldseeakarma.conf.jsfileundertheprojectdirectory;openitwithyoureditorandadd'chai'tothelistofoptionframeworks.
Almostthere!Executethecommandkarmastartand,ifeverythinggoesfine,youshouldhavespecifiedbrowsersopenedwiththetestingresultsbeingloggedintheconsoleinseconds.
IntegratingcommandswithnpmThenpmprovidesasimplebutusefulwaytodefinecustomscriptsthatcanberunwiththenpmruncommand.Andithasanotheradvantage-whennpmrunacustomscript,itaddsnode_modules/.bintothePATH.Thismakesiteasiertomanageproject-relatedcommand-linetools.
Forexample,we'vetalkedaboutMochaandIstanbul.Theprerequisiteforhavingthemascommandsistohavetheminstalledglobally,whichrequiresextrastepsotherthannpminstall.Nowwecansimplysavethemasdevelopmentdependencies,andaddcustomscriptsinpackage.json:
"scripts":{
"test":"mocha",
"cover":"istanbulcovernode_modules/mocha/bin/_mocha"
},
"devDependencies":{
"mocha":"latest",
"istanbul":"latest"
}
Nowyoucanruntestwithnpmruntest(orsimplynpmtest),andruncoverwithnpmruncoverwithoutinstallingthesepackagesglobally.
Whynototherfancybuildtools?Youmightbewondering:whydon'tweuseabuildsystemsuchasGulptosetupourworkflow?Actually,whenIstartedtowritethischapter,IdidlistGulpasthetoolweweregoingtouse.Later,IrealizeditdoesnotmakemuchsensetouseGulptobuildtheimplementationsinmostofthechaptersinthisbook.
ThereisamessageIwanttodeliver:balance.
Once,Ihadadiscussiononbalanceversusprincipleswithmyboss.Thedisagreementwasclear:heinsistedoncontrollableprinciplesoversubjectivebalance,whileIprefercontextualbalanceoverfixedprinciples.
Actually,Iagreewithhim,fromthepointofviewofateamleader.Ateamisusuallybuiltupwithdevelopersatdifferentlevels;principlesmakeiteasierforateamtobuildhigh-qualityproducts,whilenoteveryoneisabletofindtherightbalancepoint.
However,whentheroleturnsfromaproductiveteammembertoalearner,itisimportanttolearnandtofeeltherightbalancepoint.Andthat'scalledexperience.
SummaryThegoalofthischapterwastointroduceabasicworkflowthatcouldbeusedbythereadertoimplementthedesignpatternswe'llbediscussing.
WetalkedabouttheinstallationofTypeScriptcompilerthatrunsonNode.js,andhadbriefintroductionstopopularTypeScripteditorsandIDEs.Later,wespentquitealotofpageswalkingthroughthetoolsandframeworksthatcouldbeusedifthereaderwantstohavesomepracticewithimplementationsofthepatternsinthisbook.
Withthehelpofthesetoolsandframeworks,we'vebuiltaminimumworkflowthatincludescreating,building,andtestingaproject.Andtalkingaboutworkflows,youmusthavenoticedthattheyslightlydifferamongapplicationsfordifferentruntimes.
Inthenextchapter,we'lltalkaboutwhatmaygowrongandmessuptheentireprojectwhenitscomplexitykeepsgrowing.Andwe'lltrytocomeupwithspecificpatternsthatcansolvetheproblemsthisveryprojectfaces.
Chapter2.TheChallengeofIncreasingComplexityTheessenceofaprogramisthecombinationofpossiblebranchesandautomatedselectionsbasedoncertainconditions.Whenwewriteaprogram,wedefinewhat'sgoingoninabranch,andunderwhatconditionthisbranchwillbeexecuted.
Thenumberofbranchesusuallygrowsquicklyduringtheevolutionofaproject,aswellasthenumberofconditionsthatdeterminewhetherabranchwillbeexecutedornot.
Thisisdangerousforhumanbeings,whohavelimitedbraincapacities.
Inthischapter,wearegoingtoimplementadatasynchronizingservice.Startingbyimplementingsomeverybasicfeatures,we'llkeepaddingstuffandseehowthingsgo.
Thefollowingtopicswillbecovered:
Designingamulti-devicesynchronizingstrategyUsefulJavaScriptandTypeScripttechniquesandhintsthatarerelated,includingobjectsasmapsandthestringliteraltypeHowtheStrategyPatternhelpsinaproject
ImplementingthebasicsBeforewestarttowriteactualcode,weneedtodefinewhatthissynchronizingstrategywillbelike.Tokeeptheimplementationfromunnecessarydistractions,theclientwillcommunicatewiththeserverdirectlythroughfunctioncallsinsteadofusingHTTPrequestsorSockets.Also,we'llusein-memorystorage,namelyvariables,tostoredataonbothclientandserversides.
Becausewearenotseparatingtheclientandserverintotwoactualapplications,andwearenotactuallyusingbackendtechnologies,itdoesnotrequiremuchNode.jsexperiencetofollowthischapter.
However,pleasekeepinmindthateventhoughweareomittingnetworkanddatabaserequests,wehopethecorelogicofthefinalimplementationcouldbeappliedtoarealenvironmentwithoutbeingmodifiedtoomuch.So,whenitcomestoperformanceconcerns,westillneedtoassumelimitednetworkresources,especiallyfordatapassingthroughtheserverandclient,althoughtheimplementationisgoingtobesynchronousinsteadofasynchronous.Thisisnotsupposedtohappeninpractice,butinvolvingasynchronousoperationswillintroducemuchmorecode,aswellasmanymoresituationsthatneedtobetakenintoconsideration.Butwewillhavesomeusefulpatternsonasynchronousprogramminginthecomingchapters,anditwoulddefinitelyhelpifyoutrytoimplementanasynchronousversionofthesynchronizinglogicinthischapter.
Aclient,ifwithoutmodifyingwhat'sbeensynchronized,storesacopyofallthedataavailableontheserver,andwhatweneedtodoistoprovideasetofAPIsthatenabletheclienttokeepitscopyofdatasynchronized.
So,itisreallysimpleatthebeginning:comparingthelast-modifiedtimestamp.Ifthetimestampontheclientisolderthanwhat'sontheserver,thenupdatethecopyofdataalongwithnewtimestamp.
CreatingthecodebaseFirstly,let'screateserver.tsandclient.tsfilescontainingtheServerclassandClientclassrespectively:
exportclassServer{
//...
}
exportclassClient{
//...
}
Iprefertocreateanindex.tsfileasthepackageentry,whichhandleswhattoexportinternally.Inthiscase,let'sexporteverything:
export*from'./server';
export*from'./client';
ToimporttheServerandClientclassesfromatestfile(assumingsrc/test/test.ts),wecanusethefollowingcodetos:
import{Server,Client}from'../';
DefiningtheinitialstructureofthedatatobesynchronizedSinceweneedtocomparethetimestampsfromtheclientandserver,weneedtohaveatimestamppropertyonthedatastructure.Iwouldliketohavethedatatosynchronizeasastring,solet'saddaDataStoreinterfacewithatimestamppropertytotheserver.tsfile:
exportinterfaceDataStore{
timestamp:number;
data:string;
}
GettingdatabycomparingtimestampsCurrently,thesynchronizingstrategyisone-way,fromtheservertotheclient.Sowhatweneedtodoissimple:wecomparethetimestamps;iftheserverhasthenewerone,itrespondswithdataandtheserver-sidetimestamp;otherwise,itrespondswithundefined:
classServer{
store:DataStore={
timestamp:0,
data:''
};
getData(clientTimestamp:number):DataStore{
if(clientTimestamp<this.store.timestamp){
returnthis.store;
}else{
returnundefined;
}
}
}
NowwehaveprovidedasimpleAPIfortheclient,andit'stimetoimplementtheclient:
import{Server,DataStore}from'./';
exportclassClient{
store:DataStore={
timestamp:0,
data:undefined
};
constructor(
publicserver:Server
){}
}
Tip
Prefixingaconstructorparameterwithaccessmodifiers(includingpublic,private,andprotected)willcreateapropertywiththesamenameandcorrespondingaccessibility.Itwillalsoassignthevalueautomaticallywhentheconstructoriscalled.
NowweneedtoaddasynchronizemethodtotheClientclassthatdoesthejob:
synchronize():void{
letupdatedStore=this.server.getData(this.store.timestamp);
if(updatedStore){
this.store=updatedStore;
}
}
That'seasilydone.However,areyoualreadyfeelingsomewhatawkwardwithwhatwe've
written?
Two-waysynchronizingUsually,whenwetalkaboutsynchronization,wegetupdatesfromtheserverandpushchangestotheserveraswell.Nowwearegoingtodothesecondpart,pushingthechangesiftheclienthasnewerdata.
Butfirst,weneedtogivetheclienttheabilitytoupdateitsdatabyaddinganupdatemethodtotheClientclass:
update(data:string):void{
this.store.data=data;
this.store.timestamp=Date.now();
}
Andweneedtheservertohavetheabilitytoreceivedatafromtheclientaswell.SowerenamethegetDatamethodoftheServerclassassynchronizeandmakeitsatisfythenewjob:
synchronize(clientDataStore:DataStore):DataStore{
if(clientDataStore.timestamp>this.store.timestamp){
this.store=clientDataStore;
returnundefined;
}elseif(clientDataStore.timestamp<this.store.timestamp){
returnthis.store;
}else{
returnundefined;
}
}
Nowwehavethebasicimplementationofoursynchronizingservice.Later,we'llkeepaddingnewthingsandmakeitcapableofdealingwithavarietyofscenarios.
ThingsthatwentwrongwhileimplementingthebasicsCurrently,whatwe'vewrittenisjusttoosimpletobewrong.Buttherearestillsomesemanticissues.
Passingadatastorefromtheservertotheclientdoesnotmakesense
WeusedDataStoreasthereturntypeofthesynchronizemethodonServer.Butwhatwewereactuallypassingthroughisnotadatastore,butinformationthatinvolvesdataanditstimestamp.Theinformationobjectjusthappenedtohavethesamepropertiesasadatastoreatthispointintime.
Also,itwillbemisleadingtopeoplewhowilllaterreadyourcode(includingyourselfinthefuture).Mostofthetime,wearetryingtoeliminateredundancies.Butthatdoesnothavetomeaneverythingthatlooksthesame.Solet'smakeittwointerfaces:
interfaceDataStore{
timestamp:number;
data:string;
}
interfaceDataSyncingInfo{
timestamp:number;
data:string;
}
Iwouldevenprefertocreateanotherinstance,insteadofdirectlyreturningthis.store:
return{
timestamp:this.store.timestamp,
data:this.store.data
};
However,iftwopiecesofcodewithdifferentsemanticmeaningsaredoingthesamethingfromtheperspectiveofcodeitself,youmayconsiderextractingthatpartasautility.
Makingtherelationshipsclear
Nowwehavetwoseparatedinterfaces,DataStoreandDataSyncingInfo,inserver.ts.Obviously,DataSyncingInfoshouldbeasharedinterfacebetweentheserverandtheclient,whileDataStorehappenstobethesameonbothsides,butit'snotactuallyshared.
Sowhatwearegoingtodoistocreateaseparateshared.d.ts(itcouldalsobeshared.tsifitcontainsmorethantypings)thatexportsDataSyncingInfoandaddanotherDataStoretoclient.ts.
Note
Donotfollowthisblindly.Sometimesitisdesignedfortheserverandtheclienttohave
exactlythesamestores.Ifthat'sthesituation,theinterfaceshouldbeshared.
GrowingfeaturesWhatwe'vedonesofarisbasicallyuseless.But,fromnowon,wewillstarttoaddfeaturesandmakeitcapableoffittinginpracticalneeds,includingthecapabilityofsynchronizingmultipledataitemswithmultipleclients,andmergingconflicts.
SynchronizingmultipleitemsIdeally,thedataweneedtosynchronizewillhavealotofitemscontained.Directlychangingthetypeofdatatoanarraywouldworkiftherewereonlyverylimitednumberoftheseitems.
Simplyreplacingdatatypewithanarray
Nowlet'schangethetypeofthedatapropertyofDataStoreandDataSyncingInfointerfacestostring[].WiththehelpofTypeScript,youwillgeterrorsforunmatchedtypesthischangewouldcause.Fixthembyannotatingthecorrecttypes.
Butobviously,thisisfarfromanefficientsolution.
Server-centeredsynchronization
Ifthedatastorecontainsalotofdata,theidealapproachwouldbeonlyupdatingitemsthatarenotup-to-date.
Forexample,wecancreateatimestampforeverysingleitemandsendthesetimestampstotheserver,thenlettheserverdecidewhetheraspecificdataitemisup-to-date.Thisisaviableapproachforcertainscenarios,suchascheckingupdatesforsoftwareextensions.ItisokaytooccasionallysendevenhundredsoftimestampswithitemIDsonafastnetwork,butwearegoingtouseanotherapproachfordifferentscenarios,orIwon'thavemuchtowrite.
Userdatasynchronizationofofflineappsonamobilephoneiswhatwearegoingtodealwith,whichmeansweneedtotryourbesttoavoidwastingnetworkresources.
Note
Hereisaninterestingquestion.Whatarethedifferencesbetweenuserdatasynchronizationandcheckingextensionupdates?Thinkaboutthesizeofdata,issueswithmultipledevices,andmore.
Thereasonwhywethoughtaboutsendingtimestampsofallitemsisfortheservertodeterminewhethercertainitemsneedtobeupdated.However,isitnecessarytohavethetimestampsofalldataitemsstoredontheclientside?
Whatifwechoosenottostorethetimestampofdatachanging,butofdatabeingsynchronizedwiththeserver?Thenwecangeteverythingup-to-datebyonlysendingthetimestampofthelastsuccessfulsynchronization.Theserverwillthencomparethistimestampwiththelastmodifiedtimestampsofalldataitemsanddecidehowtorespond.
Asthetitleofthispartsuggests,theprocessisserver-centeredandreliesontheservertogeneratethetimestamps(thoughitdoesnothaveto,andpracticallyshouldnot,bethestampoftheactualtime).
Note
Ifyouaregettingconfusedabouthowthesetimestampswork,let'stryagain.Theserverwillstorethetimestampsofthelasttimeitemsweresynchronized,andtheclientwillstorethetimestampofthelastsuccessfulsynchronizationwiththeserver.Thus,ifnoitemontheserverhasalatertimestampthantheclient,thenthere'snochangetotheserverdatastoreafterthattimestamp.Butiftherearesomechanges,bycomparingthetimestampoftheclientwiththetimestampsofserveritems,we'llknowwhichitemsarenewer.
Synchronizingfromtheservertotheclient
Nowthereseemstobequitealottochange.Firstly,let'shandlesynchronizingdatafromservertoclient.
Thisiswhat'sexpectedtohappenontheserverside:
AddatimestampandidentitytoeverydataitemontheserverComparetheclienttimestampwitheverydataitemontheserver
Note
Wedon'tneedtoactuallycomparetheclienttimestampwitheveryitemonserverifthoseitemshaveasortedindex.Theperformancewouldbeacceptableusingadatabasewithasortedindex.
Respondwithitemsnewerthanwhattheclienthasaswellasanewtimestamp.
Andhere'swhat'sexpectedtohappenontheclientside:
SynchronizewiththelasttimestampsenttotheserverUpdatethelocalstorewithnewdatarespondedbytheserverUpdatethelocaltimestampofthelastsynchronizationifitcompleteswithouterror
Updatinginterfaces
Firstofall,wehavenowanupdateddatastoreonbothsides.Startingwiththeserver,thedatastorenowcontainsanarrayofdataitems.Solet'sdefinetheServerDataIteminterfaceandupdateServerDataStoreaswell:
exportinterfaceServerDataItem{
id:string;
timestamp:number;
value:string;
}
exportinterfaceServerDataStore{
items:{
[id:string]:ServerDataItem;
};
}
Note
The{[id:string]:ServerDataItem}typedescribesanobjectwithidoftypestringasakeyandhasthevalueoftypeServerDataItem.Thus,anitemoftypeServerDataItemcanbeaccessedbyitems['the-id'].
Andfortheclient,wenowhavedifferentdataitemsandadifferentstore.Theresponsecontainsonlyasubsetofalldataitems,soweneedIDsandamapwithIDastheindextostorethedata:
exportinterfaceClientDataItem{
id:string;
value:string;
}
exportinterfaceClientDataStore{
timestamp:number;
items:{
[id:string]:ClientDataItem;
};
}
Previously,theclientandserverweresharingthesameDataSyncingInfo,butthat'sgoingtochange.Aswe'lldealwithserver-to-clientsynchronizingfirst,wecareonlyaboutthetimestampinasynchronizingrequestfornow:
exportinterfaceSyncingRequest{
timestamp:number;
}
Asfortheresponsefromtheserver,itisexpectedtohaveanupdatedtimestampwithdataitemsthathavechangedcomparedtotherequesttimestamp:
exportinterfaceSyncingResponse{
timestamp:number;
changes:{
[id:string]:string;
};
}
IprefixedthoseinterfaceswithServerandClientforbetterdifferentiation.Butit'snotnecessaryifyouarenotexportingeverythingfromserver.tsandclient.ts(inindex.ts).
Updatingtheserverside
Withwell-defineddatastructures,itshouldbeprettyeasytoachievewhatweexpected.Tobeginwith,wehavethesynchronizemethod,whichacceptsaSyncingRequestandreturnsaSyncingResponse;andweneedtohavetheupdatedtimestampaswell:
synchronize(request:SyncingRequest):SyncingResponse{
letlastTimestamp=request.timestamp;
letnow=Date.now();
letserverChanges:ServerChangeMap=Object.create(null);
return{
timestamp:now,
changes:serverChanges
};
}
Tip
FortheserverChangesobject,{}(anobjectliteral)mightbethefirstthing(ifnotanES6Map)thatcomestomind.Butit'snotabsolutelysafetodoso,becauseitwouldrefuse__proto__asakey.ThebetterchoicewouldbeObject.create(null),whichacceptsallstringsasitskey.
NowwearegoingtoadditemsthatarenewerthantheclienttoserverChanges:
letitems=this.store.items;
for(letidofObject.keys(items)){
letitem=items[id];
if(item.timestamp>lastTimestamp){
serverChanges[id]=item.value;
}
}
Updatingtheclientside
Aswe'vechangedthetypeofitemsunderClientDataStoretoamap,weneedtofixtheinitialvalue:
store:ClientDataStore={
timestamp:0,
items:Object.create(null)
};
Nowlet'supdatethesynchronizemethod.Firstly,theclientisgoingtosendarequestwithatimestampandgetaresponsefromtheserver:
synchronize():void{
letstore=this.store;
letresponse=this.server.synchronize({
timestamp:store.timestamp
});
}
Thenwe'llsavethenewerdataitemstothestore:
letclientItems=store.items;
letserverChanges=response.changes;
for(letidofObject.keys(serverChanges)){
clientItems[id]={
id,
value:serverChanges[id]
};
}
Finally,updatethetimestampofthelastsuccessfulsynchronization:
clientStore.timestamp=response.timestamp;
Note
Updatingthesynchronizationtimestampshouldbethelastthingtododuringacompletesynchronizationprocess.Makesureit'snotstoredearlierthandataitems,oryoumighthaveabrokenofflinecopyifthere'sanyerrorsorinterruptionsduringsynchronizinginthefuture.
Note
Toensurethatthisworksasexpected,anoperationwiththesamechangeinformationshouldgivethesameresultsevenifit'sappliedmultipletimes.
Synchronizingfromclienttoserver
Foraserver-centeredsynchronizingprocess,mostofthechangesaremadethroughclients.Consequently,weneedtofigureouthowtoorganizethesechangesbeforesendingthemtotheserver.
Onesingleclientonlycaresaboutitsowncopyofdata.Whatdifferencewouldthismakewhencomparingtotheprocessofsynchronizingdatafromtheservertoclients?Well,thinkaboutwhyweneedthetimestampofeverydataitemontheserverinthefirstplace.Weneedthembecausewewanttoknowwhichitemsarenewcomparedtoaspecificclient.
Now,forchangesonaclient:iftheyeverhappen,theyneedtobesynchronizedtotheserverwithoutrequiringspecifictimestampsforcomparison.
However,wemighthavemorethanoneclientwithchangesthatneedtobesynchronized,whichmeansthatchangesmadelaterintimemightactuallygetsynchronizedearlier,andthuswe'llhavetoresolveconflicts.Toachievethat,weneedtoaddthelastmodifiedtimebacktoeverydataitemontheserverandthechangeditemsontheclient.
I'vementionedthatthetimestampsstoredontheserverforfindingoutwhatneedstobesynchronizedtoaclientdonotneedtobe(andbetternotbe)anactualstampofaphysicaltimepoint.Forexample,itcouldbethecountofsynchronizationsthathappenedbetweenallclientsandtheserver.
Updatingtheclientside
Tohandlethisefficiently,wemaycreateaseparatedmapwiththeIDsofthedataitemsthathavechangedaskeysandthelastmodifiedtimeasthevalueinClientDataStore:
exportinterfaceClientDataStore{
timestamp:number;
items:{
[id:string]:ClientDataItem;
};
changed:{
[id:string]:number;
};
}
YoumayalsowanttoinitializeitsvalueasObject.create(null).
Nowwhenweupdateanitemintheclientstore,weaddthelastmodifiedtimetothechangedmapaswell:
update(id:string,value:string):void{
letstore=this.store;
store.items[id]={
id,
value
};
store.changed[id]=Date.now();
}
AsingletimestampinSyncingRequestcertainlywon'tdothejobanymore;weneedtoaddaplaceforthechangeddata,amapwithdataitemIDastheindex,andthechangedinformationasthevalue:
exportinterfaceClientChange{
lastModifiedTime:number;
value:string;
}
exportinterfaceSyncingRequest{
timestamp:number;
changes:{
[id:string]:ClientChange;
};
}
Herecomesanotherproblem.Whatifachangemadetoaclientdataitemisdoneoffline,withthesystemclockbeingatthewrongtime?Obviously,weneedsometimecalibrationmechanisms.However,there'snowaytomakeperfectcalibration.We'llmakesomeassumptionssowedon'tneedtostartanotherchapterfortimecalibration:
Thesystemclockofaclientmaybelateorearlycomparedtotheserver.Butitticksatanormalspeedandwon'tjumpbetweentimes.Therequestsentfromaclientreachestheserverinarelativelyshorttime.
Withthoseassumptions,wecanaddthosebuildingblockstotheclient-sidesynchronizemethod:
1. Addclient-sidechangestothesynchronizingrequest(ofcourse,beforesendingittotheserver):
letclientItems=store.items;
letclientChanges:ClientChangeMap=Object.create(null);
letchangedTimes=store.changed;
for(letidofObject.keys(changedTimes)){
clientChanges[id]={
lastModifiedTime:changedTimes[id],
value:clientItems[id].value
};
}
2. Synchronizechangestotheserverwiththecurrenttimeoftheclient'sclock:
letresponse=this.server.synchronize({
timestamp:store.timestamp,
clientTime:Date.now(),
changes:clientChanges
});
3. Cleanthechangesafterasuccessfulsynchronization:
store.changed=Object.create(null);
Updatingtheserverside
Iftheclientisworkingasexpected,itshouldsendsynchronizingrequestswithchanges.It'stimetoenabletheservertohandlingthosechangesfromtheclient.
Therearegoingtobetwostepsfortheserver-sidesynchronizationprocess:
1. Applytheclientchangestoserverdatastore.2. Preparethechangesthatneedtobesynchronizedtotheclient.
First,weneedtoaddlastModifiedTimetoserver-sidedataitems,aswementionedbefore:
exportinterfaceServerDataItem{
id:string;
timestamp:number;
lastModifiedTime:number;
value:string;
}
Andweneedtoupdatethesynchronizemethod:
letclientChanges=request.changes;
letnow=Date.now();
for(letidofObject.keys(clientChanges)){
letclientChange=clientChanges[id];
if(
hasOwnProperty.call(items,id)&&
items[id].lastModifiedTime>clientChange.lastModifiedTime
){
continue;
}
items[id]={
id,
timestamp:now,
lastModifiedTime,
value:clientChange.value
};
}
Note
WecanactuallyusetheinoperatorinsteadofhasOwnPropertyherebecausetheitemsobjectiscreatedwithnullasitsprototype.ButareferencetohasOwnPropertywillbeyourfriendifyouareusingobjectscreatedbyobjectliterals,orinotherways,suchasmaps.
Wealreadytalkedaboutresolvingconflictsbycomparingthelastmodifiedtimes.Atthesametime,we'vemadeassumptionssowecancalibratethelastmodifiedtimesfromtheclienteasilybypassingtheclienttimetotheserverwhilesynchronizing.
Whatwearegoingtodoforcalibrationistocalculatetheoffsetoftheclienttimecomparedtotheservertime.Andthat'swhywemadethesecondassumption:therequestneedstoeasilyreachtheserverinarelativelyshorttime.Tocalculatetheoffset,wecansimplysubtracttheclienttimefromtheservertime:
letclientTimeOffset=now-request.clientTime;
Note
Tomakethetimecalibrationmoreaccurate,wewouldwanttheearliesttimestampaftertherequesthitstheservertoberecordedas"now".Soinpractice,youmightwanttorecordthetimestampoftherequesthittingtheserverbeforestartprocessingeverything.Forexample,forHTTPrequest,youmayrecordthetimestamponcetheTCPconnectiongetsestablished.
Andnow,thecalibratedtimeofaclientchangeisthesumoftheoriginaltimeandtheoffset.Wecannowdecidewhethertokeeporignoreachangefromtheclientbycomparingthecalibratedlastmodifiedtime.Itispossibleforthecalibratedtimetobegreaterthantheservertime;youcanchooseeithertousetheservertimeasthemaximumvalueoracceptasmallinaccuracy.Here,wewillgothesimpleway:
letlastModifiedTime=Math.min(
clientChange.lastModifiedTime+clientTimeOffset,
now
);
if(
hasOwnProperty.call(items,id)&&
items[id].lastModifiedTime>lastModifiedTime
){
continue;
}
Tomakethisactuallywork,weneedtoalsoexcludechangesfromtheserverthatconflictwithclientchangesinSyncingResponse.Todoso,weneedtoknowwhatthechangesarethatsurvivetheconflictresolvingprocess.Asimplewayistoexcludeitemswithtimestampthatequalsnow:
for(letidofObject.keys(items)){
letitem=items[id];
if(
item.timestamp>lastTimestamp&&
item.timestamp!==now
){
serverChanges[id]=item.value;
}
}
Sonowwehaveimplementedacompletesynchronizationlogicwiththeabilitytohandlesimpleconflictsinpractice.
SynchronizingmultipletypesofdataAtthispoint,we'vehardcodedthedatawiththestringtype.Butusuallywewillneedtostorevarietiesofdata,suchasnumbers,booleans,objects,andsoon.
IfwewerewritingJavaScript,wewouldnotactuallyneedtochangeanything,astheimplementationdoesnothaveanythingtodowithcertaindatatypes.InTypeScript,wedon'tneedtodomucheither:justchangethetypeofeveryrelatedvaluetoany.Butthatmeansyouarelosingtypesafety,whichwoulddefinitelybeokayifyouarehappywiththat.
Buttakingmyownpreferences,Iwouldlikeeveryvariable,parameter,andpropertytobetypedifit'spossible.Sowemaystillhaveadataitemwithvalueoftypeany:
exportinterfaceClientDataItem{
id:string;
value:any;
}
Wecanalsohavederivedinterfacesforspecificdatatypes:
exportinterfaceClientStringDataItemextendsClientDataItem{
value:string;
}
exportinterfaceClientNumberDataItemextendsClientDataItem{
value:number;
}
Butthisdoesnotseemtobegoodenough.Fortunately,TypeScriptprovidesgenerics,sowecanrewritetheprecedingcodeasfollows:
exportinterfaceClientDataItem<T>{
id:string;
value:T;
}
Assumingwehaveastorethatacceptsmultipletypesofdataitems-forexample,numberandstring-wecandeclareitasfollowswiththehelpoftheuniontype:
exportinterfaceClientDataStore{
items:{
[id:string]:ClientDataItem<number|string>;
};
}
Ifyourememberthatwearedoingsomethingforofflinemobileapps,youmightbequestioningthelongpropertynamesinchangessuchaslastModifiedTime.Thisisafairquestion,andaneasyfixistousetupletypes,maybealongwithenums:
constenumClientChangeIndex{
lastModifiedType,
value
}
typeClientChange<T>=[number,T];
letchange:ClientChange<string>=[0,'foo'];
letvalue=change[ClientChangeIndex.value];
Youcanapplylessormoreofthetypingthingswearetalkingaboutdependingonyourpreferences.Ifyouarenotfamiliarwiththemyet,youcanreadmorehere:http://www.typescriptlang.org/handbook.
SupportingmultipleclientswithincrementaldataMakingthetypingsystemhappywithmultipledatatypesiseasy.Butintherealworld,wedon'tresolveconflictsofalldatatypesbysimplycomparingthelastmodifiedtimes.Anexampleiscountingthedailyactivetimeofausercrossdevices.
It'squiteclearthatweneedtohaveeverypieceofactivetimeinadayonmultipledevicessummedup.Andthisishowwearegoingtoachievethat:
1. Accumulateactivedurationsbetweensynchronizationsontheclient.2. AddaUID(uniqueidentifier)toeverypieceoftimebeforesynchronizingwiththe
server.3. Increasetheserver-sidevalueiftheUIDdoesnotexistyet,andthenaddtheUIDtothat
dataitem.
Butbeforeweactuallygetourhandsonthosesteps,weneedawaytodistinguishincrementaldataitemsfromnormalones,forexample,byaddingatypeproperty.
Asoursynchronizingstrategyisserver-centered,relatedinformationisonlyrequiredforsynchronizingrequestsandconflictmerging.Synchronizingresponsesdoesnotneedtoincludethedetailsofchanges,butjustmergedvalues.
Note
Iwillstoptellinghowtoupdateeveryinterfacestepbystepasweareapproachingthefinalstructure.Butifyouhaveanyproblemswiththat,youcancheckoutthecompletecodebundleforinspiration.
Updatingtheclientside
Firstofall,weneedtheclienttosupportincrementalchanges.Andifyou'vethoughtaboutthis,youmightalreadybeconfusedaboutwheretoputtheextrainformation,suchasUIDs.
Thisisbecauseweweremixinguptheconceptchange(noun)withvalue.Itwasnotaproblembeforebecause,besidesthelastmodifiedtime,thevalueiswhatachangeisabout.Weusedasimplemaptostorethelastmodifiedtimesandkeptthestorecleanfromredundancy,whichbalancedwellunderthatscenario.
Butnowweneedtodistinguishbetweenthesetwoconcepts:
Value:avaluedescribeswhatadataitemisinastaticwayChange:achangedescribestheinformationthatmaytransformthevalueofadataitemfromonetoanother
Weneedtohaveageneraltypeofchangesaswellasanewdatastructureforincrementalchangeswithanumericvalue:
typeDataType='value'|'increment';
interfaceClientChange{
type:DataType;
}
interfaceClientValueChange<T>extendsClientChange{
type:'value';
lastModifiedTime:number;
value:T;
}
interfaceClientIncrementChangeextendsClientChange{
type:'increment';
uid:string;
increment:number;
}
Note
Weareusingthestringliteraltypehere,whichwasintroducedinTypeScript1.8.Tolearnmore,pleaserefertotheTypeScripthandbookaswementionedbefore.
Similarchangestothedatastorestructureshouldbemade.Andwhenweupdateanitemontheclientside,weneedtoapplythecorrectoperationsbasedondifferentdatatypes:
update(id:string,type:'increment',increment:number):void;
update<T>(id:string,type:'value',value:T):void;
update<T>(id:string,type:DataType,value:T):void;
update<T>(id:string,type:DataType,value:T):void{
letstore=this.store;
letitems=store.items;
letstoredChanges=store.changes;
if(type==='value'){
//...
}elseif(type==='increment'){
//...
}else{
thrownewTypeError('Invaliddatatype');
}
}
Usethefollowingcodefornormalchanges(whiletypeequals'value'):
letchange:ClientValueChange<T>={
type:'value',
lastModifiedTime:Date.now(),
value
};
storedChanges[id]=change;
if(hasOwnProperty.call(items,id)){
items[id].value=value;
}else{
items[id]={
id,
type,
value
};
}
Forincrementalchanges,ittakesafewmorelines:
letstoredChange=storedChanges[id]asClientIncrementChange;
if(storedChange){
storedChange.increment+=<any>valueasnumber;
}else{
storedChange={
type:'increment',
uid:Date.now().toString(),
increment:<any>valueasnumber
};
storedChanges[id]=storedChange;
}
Note
It'smypersonalpreferencetouse<T>foranycastingandasTfornon-anycastings.ThoughithasbeenusedinlanguageslikeC#,theasoperatorinTypeScriptwasoriginallyintroducedforcompatibilitieswithXMLtagsinJSX.Youcanalsowrite<number><any>valueorvalueasanyasnumberhereifyoulike.
Don'tforgettoupdatethestoredvalue.Justchange=to+=comparingtoupdatingnormaldataitems:
if(hasOwnProperty.call(items,id)){
items[id].value+=value;
}else{
items[id]={
id,
type,
value
};
}
That'snothardatall.Buthey,weseebranches.
Wearewritingbranchesallthetime,butwhatarethedifferencesbetweenbranchessuchasif(type==='foo'){...}andbranchessuchasif(item.timestamp>lastTimestamp){...}?Let'skeepthisquestioninmindandmoveon.
Withnecessaryinformationaddedbytheupdatemethod,wecannowupdatethesynchronize
methodoftheclient.Butthereisaflawinpracticalscenarios:asynchronizingrequestissenttotheserversuccessfully,buttheclientfailedtoreceivetheresponsefromtheserver.Inthissituation,whenupdateiscalledafterafailedsynchronization,theincrementisaddedtothemight-be-synchronizedchange(identifiedbyitsUID),whichwillbeignoredbytheserverinfuturesynchronizations.Tofixthis,we'llneedtoaddamarktoallincrementalchangesthathavestartedasynchronizingprocess,andavoidaccumulatingthesechanges.Thus,weneedtocreateanotherchangeforthesamedataitem.
Thisisactuallyanicehint:asachangeisaboutinformationthattransformsavaluefromonetoanother,severalchangespendingsynchronizationmighteventuallybeappliedtoonesingledataitem:
interfaceClientChangeList<TextendsClientChange>{
type:DataType;
changes:T[];
}
interfaceSyncingRequest{
timestamp:number;
changeLists:{
[id:string]:ClientChangeList<ClientChange>;
};
}
interfaceClientIncrementChangeextendsClientChange{
type:'increment';
synced:boolean;
uid:string;
increment:number;
}
Nowwhenwearetryingtoupdateanincrementaldataitem,weneedtogetitslastchangefromthechangelist(ifany)andseewhetherithaseverbeensynchronized.Ifithaseverbeeninvolvedinasynchronization,wecreateanewchangeinstance.Otherwise,we'lljustaccumulatetheincrementpropertyvalueofthelastchangeontheclientside:
letchangeList=storedChangeLists[id];
letchanges=changeList.changes;
letlastChange=
changes[changes.length-1]asClientIncrementChange;
if(lastChange.synced){
changes.push({
synced:false,
uid:Date.now().toString(),
increment:<any>valueasnumber
}asClientIncrementChange);
}else{
lastChange.increment+=<any>valueasnumber;
}
Or,ifthechangelistdoesnotexistyet,we'llneedtosetitup:
letchangeList={
type:'increment',
changes:[
{
synced:false,
uid:Date.now().toString(),
increment:<any>valueasnumber
}asClientIncrementChange
]
};
store.changeLists[id]=changeList;
Wealsoneedtoupdatesynchronizemethodtomarkanincrementalchangeassyncedbeforestartingthesynchronizationwiththeserver.Buttheimplementationisforyoutodoonyourown.
Updatingserverside
Beforeweaddthelogicforhandlingincrementalchanges,weneedtomakeserver-sidecodeadapttothenewdatastructure:
for(letidofObject.keys(clientChangeLists)){
letclientChangeList=clientChangeLists[id];
lettype=clientChangeList.type;
letclientChanges=clientChangeList.changes;
if(type==='value'){
//...
}elseif(type==='increment'){
//...
}else{
thrownewTypeError('Invaliddatatype');
}
}
Thechangelistofanormaldataitemwillalwayscontainoneandonlyonechange.Thuswecaneasilymigratewhatwe'vewritten:
letclientChange=changes[0]asClientValueChange<any>;
Nowforincrementalchanges,weneedtocumulativelyapplypossiblymultiplechangesinasinglechangelisttoadataitem:
letitem=items[id];
for(
letclientChange
ofclientChangesasClientIncrementChange[]
){
let{
uid,
increment
}=clientChange;
if(item.uids.indexOf(uid)<0){
item.value+=increment;
item.uids.push(uid);
}
}
ButremembertotakecareofthetimestamporcasesinwhichnoitemwithaspecifiedIDexists:
letitem:ServerDataItem<any>;
if(hasOwnProperty.call(items,id)){
item=items[id];
item.timestamp=now;
}else{
item=items[id]={
id,
type,
timestamp:now,
uids:[],
value:0
};
}
Withoutknowingthecurrentvalueofanincrementaldataitemontheclient,wecannotassurethatthevalueisuptodate.Previously,wedecidedwhethertorespondwithanewvaluebycomparingthetimestampwiththetimestampofthecurrentsynchronization,butthatdoesnotworkanymoreforincrementalchanges.
AsimplewaytomakethisworkisbydeletingkeysfromclientChangeListsthatstillneedtobesynchronizedtotheclient.Andwhenpreparingresponses,itcanskipIDsthatarestillinclientChangeLists:
if(
item.timestamp>lastTimestamp&&
!hasOwnProperty.call(clientChangeLists,id)
){
serverChanges[id]=item.value;
}
RemembertoadddeleteclientChangeLists[id];fornormaldataitemsthatdidnotsurviveconflictsresolvingaswell.
Nowwehaveimplementedasynchronizinglogicthatcandoquitealotjobsforofflineapplications.Earlier,Iraisedaquestionaboutincreasingbranchesthatdonotlookgood.Butifyouknowyourfeaturesaregoingtoendthere,oratleastwithlimitedchanges,it'snotabadimplementation,althoughwe'llsooncrossthebalancepoint,asmeeting80%oftheneedswon'tmakeushappyenough.
SupportingmoreconflictmergingThoughwehavemettheneedsof80%,thereisstillabigchancethatwemightwantsomeextrafeatures.Forexample,wewanttheratioofthedaysmarkedasavailablebytheuserinthecurrentmonth,andtheusershouldbeabletoaddorremovedaysfromthelist.Wecanachievethatindifferentways,andwe'llchooseasimpleway,asusual.
Wearegoingtosupportsynchronizingasetwithoperationssuchasaddandremove,andcalculatetheratioontheclient.
Newdatastructures
Todescribesetchanges,weneedanewClientChangetype.Whenweareaddingorremovinganelementfromaset,weonlycareaboutthelastoperationtothesameelement.Thismeansthatthefollowing:
1. Ifmultipleoperationsaremadetothesameelement,weonlyneedtokeepthelastone.2. Atimepropertyisrequiredforresolvingconflicts.
Soherearethenewtypes:
enumSetOperation{
add,
remove
}
interfaceClientSetChangeextendsClientChange{
element:number;
time:number;
operation:SetOperation;
}
Thesetdatastoredontheserversideisgoingtobealittledifferent.We'llhaveamapwiththeelement(intheformofastring)askey,andastructurewithoperationandtimepropertiesasthevalues:
interfaceServerSetElementOperationInfo{
operation:SetOperation;
time:number;
}
Nowwehaveenoughinformationtoresolveconflictsfrommultipleclients.Andwecangeneratethesetbykeyswithalittlehelpfromthelastoperationsdonetotheelements.
Updatingclientside
Andnow,theclient-sideupdatemethodgetsanewpart-timejob:savingsetchangesjustlikevalueandincrementalchanges.Weneedtoupdatethemethodsignatureforthisnewjob(donotforgettoadd'set'toDataType):
update(
id:string,
type:'set',
element:number,
operation:SetOperation
):void;
update<T>(
id:string,
type:DataType,
value:T,
operation?:SetOperation
):void;
Wealsoneedtoaddanotherelseif:
elseif(type==='set'){
letelement=<any>valueasnumber;
if(hasOwnProperty.call(storedChangeLists,id)){
//...
}else{
//...
}
}
Iftherearealreadyoperationsmadetothisset,weneedtofindandremovethatlastoperationtothetargetelement(ifany).Thenappendanewchangewiththelatestoperation:
letchangeList=storedChangeLists[id];
letchanges=changeList.changesasClientSetChange[];
for(leti=0;i<changes.length;i++){
letchange=changes[i];
if(change.element===element){
changes.splice(i,1);
break;
}
}
changes.push({
element,
time:Date.now(),
operation
});
Ifnochangehasbeenmadesincelastsuccessfulsynchronization,we'llneedtocreateanewchangelistforthelatestoperation:
letchangeList:ClientChangeList<ClientSetChange>={
type:'set',
changes:[
{
element,
time:Date.now(),
operation
}
]
};
storedChangeLists[id]=changeList;
Andagain,donotforgettoupdatethestoredvalue.Thisisalittlebitmorethanjustassigningoraccumulatingthevalue,butitshouldstillbequiteeasytoimplement.
Updatingtheserverside
Justlikewe'vedonewiththeclient,weneedtoaddacorrespondingelseifbranchtomergechangesoftype'set'.WearealsodeletingtheIDfromclientChangeListsregardlessofwhethertherearenewerchangesforasimplerimplementation:
elseif(type==='set'){
letitem:ServerDataItem<{
[element:string]:ServerSetElementOperationInfo;
}>;
deleteclientChangeLists[id];
}
Theconflictresolvinglogicisquitesimilartowhatwedototheconflictsofnormalvalues.Wejustneedtomakecomparisonstoeachelement,andonlykeepthelastoperation.
Andwhenpreparingtheresponsethatwillbesynchronizedtotheclient,wecangeneratethesetbyputtingtogetherelementswithaddastheirlastoperations:
if(item.type==='set'){
letoperationInfos:{
[element:string]:ServerSetElementOperationInfo;
}=item.value;
serverChanges[id]=Object
.keys(operationInfos)
.filter(element=>
operationInfos[element].operation===
SetOperation.add
)
.map(element=>Number(element));
}else{
serverChanges[id]=item.value;
}
Finally,wehaveaworkingmess(ifitactuallyworks).Cheers!
ThingsthatgowrongwhileimplementingeverythingWhenwestartedtoaddfeatures,thingswereactuallyfine,ifyouarenotobsessiveaboutpursuingthefeelingofdesign.Thenwesensedthecodebeingalittleawkwardaswesawmoreandmorenestedbranches.
Sonowit'stimetoanswerthequestion,whatarethedifferencesbetweenthetwokindsofbranchwewrote?MyunderstandingofwhyIamfeelingawkwardabouttheif(type==='foo'){...}branchisthatit'snotstronglyrelatedtothecontext.Comparingtimestamps,ontheotherhand,isamorenaturalpartofacertainsynchronizingprocess.
Again,Iamnotsayingthisisbad.Butthisgivesusahintaboutwherewemightstartoursurgeryfromwhenwestarttolosecontrol(duetoourlimitedbraincapacity,it'sjustamatterofcomplexity).
Pilingupsimilaryetparallelprocesses
Mostofthecodeinthischapteristohandletheprocessofsynchronizingdatabetweenaclientandaserver.Togetadaptedtonewfeatures,wejustkeptaddingnewthingsintomethods,suchasupdateandsynchronize.
Youmighthavealreadyfoundthatmostoutlinesofthelogiccanbe,andshouldbe,sharedacrossmultipledatatypes.Butwedidn'tdothat.
Ifwelookintowhat'swritten,theduplicationisactuallyminorjudgingfromtheaspectofcodetexts.Takingtheupdatemethodoftheclient,forexample,thelogicofeverybranchseemstodiffer.Iffindingabstractionshasnotbecomeyourbuilt-inreaction,youmightjuststopthere.Orifyouarenotafanoflongfunctions,youmightrefactorthecodebysplittingitintosmallonesofthesameclass.Thatcouldmakethingsalittlebetter,butfarfromenough.
Datastoresthataretremendouslysimplified
Intheimplementation,wewereplayingheavilyanddirectlywithidealin-memorystores.Itwouldbeniceifwecouldhaveawrapperforit,andmaketherealstoreinterchangeable.
Thismightnotbethecaseforthisimplementationasitisbasedonextremelyidealandsimplifiedassumptionsandrequirements.Butaddingawrappercouldbeawaytoprovideusefulhelpers.
GettingthingsrightSolet'sgetoutoftheillusionofcomparingcodeonecharacteratatimeandtrytofindanabstractionthatcanbeappliedtoupdatingallofthesedatatypes.Therearetwokeypointsofthisabstractionthathavealreadybeenmentionedintheprevioussection:
AchangecontainstheinformationthatcantransformthevalueofanitemfromonetoanotherMultiplechangescouldbegeneratedorappliedtoonedataitemduringasinglesynchronization
Now,startingfromchanges,let'sthinkaboutwhathappenswhenanupdatemethodofaclientiscalled.
FindingabstractionTakeacloserlooktothemethodupdateofclient:
Fordataofthe'value'type,firstwecreatethechange,includinganewvalue,andthenupdatethechangelisttomakethenewlycreatedchangetheonlyone.Afterthat,weupdatethevalueofdataitem.Fordataofthe'increment'type,weaddachangeincludingtheincrementinthechangelist;orifachangethathasnotbesynchronizedalreadyexists,updatetheincrementoftheexistingchange.Andthen,weupdatethevalueofthedataitem.Finally,fordataofthe'set'type,wecreateachangereflectingthelatestoperation.Afteraddingthenewchangetothechangelist,wealsoremovechangesthatarenolongernecessary.Thenweupdatethevalueofthedataitem.
Thingsaregettingclear.Hereiswhat'shappeningtothesedatatypeswhenupdateiscalled:
1. Createnewchange.2. Mergethenewchangetothechangelist.3. Applythenewchangetothedataitem.
Nowit'sevenbetter.Everystepisdifferentfordifferentdatatypes,butdifferentstepssharethesameoutline;whatweneedtodoistoimplementdifferentstrategiesfordifferentdatatypes.
ImplementingstrategiesDoingallkindofchangeswithasingleupdatefunctioncouldbeconfusing.Andbeforewemoveon,let'ssplititintothreedifferentmethods:updatefornormalvalues,increaseforincrementalvalues,andaddTo/removeFromforsets.
ThenwearegoingtocreateanewprivatemethodcalledapplyChange,whichwilltakethechangecreatedbyothermethodsandcontinuewithstep2andstep3.Itacceptsastrategyobjectwithtwomethods:appendandapply:
interfaceClientChangeStrategy<TextendsClientChange>{
append(list:ClientChangeList<T>,change:T):void;
apply(item:ClientDataItem<any>,change:T):void;
}
Foranormaldataitem,thestrategyobjectcouldbeasfollows:
letstrategy:ClientChangeStrategy<ClientValueChange<any>>={
append(list,change){
list.changes=[change];
},
apply(item,change){
item.value=change.value;
}
};
Andforincrementaldataitem,ittakesafewmorelines.First,theappendmethod:
letchanges=list.changes;
letlastChange=changes[changes.length];
if(!lastChange||lastChange.synced){
changes.push(change);
}else{
lastChange.increment+=change.increment;
}
Theappendmethodisfollowedbytheapplymethod:
if(item.value===undefined){
item.value=change.increment;
}else{
item.value+=change.increment;
}
NowintheapplyChangemethod,weneedtotakecareofthecreationofnon-existingitemsandchangelists,andinvokedifferentappendandapplymethodsbasedondifferentdatatypes.
Thesametechniquecanbeappliedtootherprocesses.Thoughdetailedprocessesthatapplytotheclientandtheserverdiffer,wecanstillwritethemtogetherasmodules.
WrappingstoresWearegoingtomakealightweightwrapperaroundplainin-memorystoreobjectswiththeabilitytoreadandwrite,takingtheserver-sidestoreasanexample:
exportclassServerStore{
privateitems:{
[id:string]:ServerDataItem<any>;
}=Object.create(null);
}
exportclassServer{
constructor(
publicstore:ServerStore
){}
}
Tofitourrequirements,weneedtoimplementget,set,andgetAllmethods(orevenbetter,afindmethodwithconditions)forServerStore:
get<T,TExtraextendsServerDataItemExtra>(id:string):
ServerDataItem<T>&TExtra{
returnhasOwnProperty.call(this.items,id)?
this.items[id]asServerDataItem<T>&TExtra:undefined;
}
set<T,TExtraextendsServerDataItemExtra>(
id:string,
item:ServerDataItem<T>&Textra
):void{
this.items[id]=item;
}
getAll<T,TExtraextendsServerDataItemExtra>():
(ServerDataItem<T>&TExtra)[]{
letitems=this.items;
returnObject
.keys(items)
.map(id=>items[id]asServerDataItem<T>&TExtra);
}
YoumayhavenoticedfromtheinterfacesandgenericsthatI'vealsotorndownServerDataItemintointersectiontypesofthecommonpartandextras.
SummaryInthischapter,we'vebeenpartoftheevolutionofasimplifiedyetreality-relatedproject.Startingwithasimplecodebasethatcouldn'tbewrong,weaddedalotoffeaturesandexperiencedtheprocessofputtingacceptablechangestogetherandmakingthewholethingamess.
Wewerealwaystryingtowritereadablecodebyeithernamingthingsnicelyoraddingsemanticallynecessaryredundancies,butthatwon'thelpmuchasthecomplexitygrows.
Duringtheprocess,we'velearnedhowofflinesynchronizingworks.Andwiththehelpofthemostcommondesignpatterns,suchastheStrategyPattern,wemanagedtosplittheprojectintosmallandcontrollableparts.
Intheupcomingchapters,we'llcatalogmoreusefuldesignpatternswithcodeexamplesinTypeScript,andtrytoapplythosedesignpatternstospecificissues.
Chapter3.CreationalDesignPatternsCreationaldesignpatternsinobject-orientedprogrammingaredesignpatternsthataretobeappliedduringtheinstantiationofobjects.Inthischapter,we'llbetalkingaboutpatternsinthiscategory.
Considerwearebuildingarocket,whichhaspayloadandoneormorestages:
classPayload{
weight:number;
}
classEngine{
thrust:number;
}
classStage{
engines:Engine[];
}
Inold-fashionedJavaScript,therearetwomajorapproachestobuildingsucharocket:
ConstructorwithnewoperatorFactoryfunction
Forthefirstapproach,thingscouldbelikethis:
functionRocket(){
this.payload={
name:'cargoship'
};
this.stages=[
{
engines:[
//...
]
}
];
}
varrocket=newRocket();
Andforthesecondapproach,itcouldbelikethis:
functionbuildRocket(){
varrocket={};
rocket.payload={
name:'cargoship'
};
rocket.stages=[
{
thrusters:[
//...
]
}
];
returnrocket;
}
varrocket=buildRocket();
Fromacertainangle,theyaredoingprettymuchthesamething,butsemanticallytheydifferalot.Theconstructorapproachsuggestsastrongassociationbetweenthebuildingprocessandthefinalproduct.Thefactoryfunction,ontheotherhand,impliesaninterfaceofitsproductandclaimstheabilitytobuildsuchaproduct.
However,neitheroftheprecedingimplementationsprovidestheflexibilitytomodularlyassemblerocketsbasedonspecificneeds;thisiswhatcreationaldesignpatternsareabout.
Inthischapter,we'llcoverthefollowingcreationalpatterns:
Factorymethod:Byusingabstractmethodsofafactoryinsteadoftheconstructortobuildinstances,thisallowssubclassestochangewhat'sbuiltbyimplementingoroverridingthesemethods.Abstractfactory:Definingtheinterfaceofcompatiblefactoriesandtheirproducts.Thusbychangingthefactorypassed,wecanchangethefamilyofbuiltproducts.Builder:Definingthestepsofbuildingcomplexobjects,andchangingwhat'sbuilteitherbychangingthesequenceofsteps,orusingadifferentbuilderimplementation.Prototype:Creatingobjectsbycloningparameterizedprototypes.Thusbyreplacingtheseprototypes,wemaybuilddifferentproducts.Singleton:Ensuringonlyoneinstance(underacertainscope)willbecreated.
ItisinterestingtoseethateventhoughthefactoryfunctionapproachtocreatingobjectsinJavaScriptlooksprimitive,itdoeshavepartsincommonwithsomepatternswearegoingtotalkabout(althoughappliedtodifferentscopes).
FactorymethodUndersomescenarios,aclasscannotpredictexactlywhatobjectsitwillcreate,oritssubclassesmaywanttocreatemorespecifiedversionsoftheseobjects.Then,theFactoryMethodPatterncanbeapplied.
ThefollowingpictureshowsthepossiblestructureoftheFactoryMethodPatternappliedtocreatingrockets:
Afactorymethodisamethodofafactorythatbuildsobjects.Takebuildingrocketsasanexample;afactorymethodcouldbeamethodthatbuildseithertheentirerocketorasinglecomponent.Onefactorymethodmightrelyonotherfactorymethodstobuilditstargetobject.Forexample,ifwehaveacreateRocketmethodundertheRocketclass,itwouldprobablycallfactorymethodslikecreateStagesandcreatePayloadtogetthenecessarycomponents.
TheFactoryMethodPatternprovidessomeflexibilityuponreasonablecomplexity.Itallowsextendableusagebyimplementing(oroverriding)specificfactorymethods.TakingcreateStagesmethod,forexample,wecancreateaone-stagerocketoratwo-stagerocketbyprovidingdifferentcreateStagesmethodthatreturnoneortwostagesrespectively.
ParticipantsTheparticipantsofatypicalFactoryMethodPatternimplementationincludethefollowing:
Product:Rocket
Defineanabstractclassoraninterfaceofarocketthatwillbecreatedastheproduct.
Concreteproduct:FreightRocket
Implementaspecificrocketproduct.
Creator:RocketFactory
Definetheoptionallyabstractfactoryclassthatcreatesproducts.
Concretecreator:FreightRocketFactory
Implementoroverridesspecificfactorymethodstobuildproductsondemand.
PatternscopeTheFactoryMethodPatterndecouplesRocketfromtheconstructorimplementationandmakesitpossibleforsubclassesofafactorytochangewhat'sbuiltaccordingly.Aconcretecreatorstillcaresaboutwhatexactlyitscomponentsareandhowtheyarebuilt.Buttheimplementationoroverridingusuallyfocusesmoreoneachcomponent,ratherthantheentireproduct.
ImplementationLet'sbeginwithbuildingasimpleone-stagerocketthatcarriesa0-weightpayloadasthedefaultimplementation:
classRocketFactory{
buildRocket():Rocket{}
createPayload():Payload{}
createStages():Stage[]{}
}
Westartwithcreatingcomponents.Wewillsimplyreturnapayloadwith0weightforthefactorymethodcreatePayloadandonesinglestagewithonesingleengineforthefactorymethodcreateStages:
createPayload():Payload{
returnnewPayload(0);
}
createStages():Stage[]{
letengine=newEngine(1000);
letstage=newStage([engine]);
return[stage];
}
Afterimplementingmethodstocreatethecomponentsofarocket,wearegoingtoputthemtogetherwiththefactorymethodbuildRocket:
buildRocket():Rocket{
letrocket=newRocket();
letpayload=this.createPayload();
letstages=this.createStages();
rocket.payload=payload;
rocket.stages=stages;
returnrocket;
}
Nowwehavetheblueprintofasimplerocketfactory,yetwithcertainextensibilities.Tobuildarocket(thatdoesnothingsofar),wejustneedtoinstantiatethisveryfactoryandcallitsbuildRocketmethod:
letrocketFactory=newRocketFactory();
letrocket=rocketFactory.buildRocket();
Next,wearegoingtobuildtwo-stagefreightrocketsthatsendsatellitesintoorbit.Thus,therearesomedifferencescomparedtothebasicfactoryimplementation.
First,wehaveadifferentpayload,satellites,insteadofa0-weightplaceholder:
classSatelliteextendsPayload{
constructor(
publicid:number
){
super(200);
}
}
Second,wenowhavetwostages,probablywithdifferentspecifications.Thefirststageisgoingtohavefourengines:
classFirstStageextendsStage{
constructor(){
super([
newEngine(1000),
newEngine(1000),
newEngine(1000),
newEngine(1000)
]);
}
}
Whilethesecondstagehasonlyone:
classSecondStageextendsStage{
constructor(){
super([
newEngine(1000)
]);
}
}
Nowwehavewhatthisnewfreightrocketwouldlooklikeinmind,let'sextendthefactory:
typeFreightRocketStages=[FirstStage,SecondStage];
classFreightRocketFactoryextendsRocketFactory{
createPayload():Satellite{}
createStages():FreightRocketStages{}
}
Tip
Hereweareusingthetypealiasofatupletorepresentthestagessequenceofafreightrocket,namelythefirstandsecondstages.Tofindoutmoreabouttypealiases,pleaserefertohttps://www.typescriptlang.org/docs/handbook/advanced-types.html.
AsweaddedtheidpropertytoSatellite,wemightneedacounterforeachinstanceofthefactory,andthencreateeverysatellitewithauniqueID:
nextSatelliteId=0;
createPayload():Satellite{
returnnewSatellite(this.nextSatelliteId++);
}
Let'smoveonandimplementthecreateStagesmethodthatbuildsfirstandsecondstageoftherocket:
createStages():FreightRocketStages{
return[
newFirstStage(),
newSecondStage()
];
}
Comparingtotheoriginalimplementation,youmayhavenoticedthatwe'veautomaticallydecoupledspecificstagebuildingprocessesfromassemblingthemintoconstructorsofdifferentstages.Itisalsopossibletoapplyanothercreationalpatternfortheinitiationofeverystageifithelps.
ConsequencesIntheprecedingimplementation,thefactorymethodbuildRockethandlestheoutlineofthebuildingsteps.Wewereluckytohavethefreightrocketinthesamestructureastheveryfirstrocketwehaddefined.
Butthatwon'talwayshappen.Ifwewanttochangetheclassofproducts(Rocket),we'llhavetooverridetheentirebuildRocketwitheverythingelsebuttheclassname.Thislooksfrustratingbutitcanbesolved,again,bydecouplingthecreationofarocketinstancefromthebuildingprocess:
buildRocket():Rocket{
letrocket=this.createRocket();
letpayload=this.createPayload();
letstages=this.createStages();
rocket.payload=payload;
rocket.stages=stages;
returnrocket;
}
createRocket():Rocket{
returnnewRocket();
}
ThuswecanchangetherocketclassbyoverridingthecreateRocketmethod.However,thereturntypeofthebuildRocketofasubclass(forexample,FreightRocketFactory)isstillRocketinsteadofsomethinglikeFreightRocket.ButastheobjectcreatedisactuallyaninstanceofFreightRocket,itisvalidtocastthetypebytypeassertion:
letrocket=FreightRocketFactory.buildRocket()asFreightRocket;
Thetrade-offisalittletypesafety,butthatcanbeeliminatedusinggenerics.Unfortunately,inTypeScriptwhatyougetfromagenerictypeargumentisjustatypewithoutanactualvalue.Thismeansthatwemayneedanotherlevelofabstractionorotherpatternsthatcanusethehelpoftypeinferencetomakesureofeverything.
TheformeroptionwouldleadustotheAbstractFactoryPattern.
Note
Typesafetycouldbeonereasontoconsiderwhenchoosingapatternbutusually,itwillnotbedecisive.Pleasenotewearenottryingtoswitchapatternforthissinglereason,butjustexploring.
AbstractFactoryTheAbstractFactoryPatternusuallydefinestheinterfacesofacollectionoffactorymethods,withoutspecifyingconcreteproducts.Thisallowsanentirefactorytobereplaceable,inordertoproducedifferentproductsfollowingthesameproductionoutline:
Thedetailsoftheproducts(components)areomittedfromthediagram,butdonoticethattheseproductsbelongtotwoparallelfamilies:ExperimentalRocketandFreightRocket.
DifferentfromtheFactoryMethodPattern,theAbstractFactoryPatternextractsanotherpartcalledclientthattakecaresofshapingtheoutlineofthebuildingprocess.Thismakesthefactorypartfocusedmoreonproducingeachcomponent.
ParticipantsTheparticipantsofatypicalAbstractFactoryPatternimplementationincludethefollowing:
Abstractfactory:RocketFactory
Definestheindustrialstandardsofafactorywhichprovideinterfacesformanufacturingcomponentsorcomplexproducts.
Concretefactory:ExperimentalRocketFactory,FreightRocketFactory
Implementstheinterfacesdefinedbytheabstractfactoryandbuildsconcreteproducts.
Abstractproducts:Rocket,Payload,Stage[]
Definetheinterfacesoftheproductsthefactoriesaregoingtobuild.
Concreteproducts:ExperimentalRocket/FreightRocket,ExperimentalPayload/Satellite,andsoon.
Presentsactualproductsthataremanufacturedbyaconcretefactory.
Client:
Arrangestheproductionprocessacrossfactories(onlyifthesefactoriesconformtoindustrialstandards).
PatternscopeAbstractFactoryPatternmakestheabstractionontopofdifferentconcretefactories.Atthescopeofasinglefactoryorasinglebranchoffactories,itjustworksliketheFactoryMethodPattern.However,thehighlightofthispatternistomakeawholefamilyofproductsinterchangeable.AgoodexamplecouldbecomponentsofthemesforaUIimplementation.
ImplementationIntheAbstractFactoryPattern,itistheclientinteractingwithaconcretefactoryforbuildingintegralproducts.However,theconcreteclassofproductsisdecoupledfromtheclientduringdesigntime,whiletheclientcaresonlyaboutwhatafactoryanditsproductslooklikeinsteadofwhatexactlytheyare.
Let'sstartbysimplifyingrelatedclassestointerfaces:
interfacePayload{
weight:number;
}
interfaceStage{
engines:Engine[];
}
interfaceRocket{
payload:Payload;
stages:Stage[];
}
Andofcoursetheabstractfactoryitselfis:
interfaceRocketFactory{
createRocket():Rocket;
createPayload():Payload;
createStages():Stage[];
}
Thebuildingstepsareabstractedfromthefactoryandputintotheclient,butwestillneedtoimplementitanyway:
classClient{
buildRocket(factory:RocketFactory):Rocket{
letrocket=factory.createRocket();
rocket.payload=factory.createPayload();
rocket.stages=factory.createStages();
returnrocket;
}
}
NowwehavethesameissuewepreviouslyhadwhenweimplementedtheFactoryMethodPattern.Asdifferentconcretefactoriesbuilddifferentrockets,theclassoftheproductchanges.However,nowwehavegenericstotherescue.
First,weneedaRocketFactoryinterfacewithagenerictypeparameterthatdescribesaconcreterocketclass:
interfaceRocketFactory<TextendsRocket>{
createRocket():T;
createPayload():Payload;
createStages():Stage[];
}
Andsecond,updatethebuildRocketmethodoftheclienttosupportgenericfactories:
buildRocket<TextendsRocket>(
factory:RocketFactory<T>
):T{}
Thus,withthehelpofthetypesystem,wewillhaverockettypeinferredbasedonthetypeofaconcretefactory,startingwithExperimentalRocketandExperimentalRocketFactory:
classExperimentalRocketimplementsRocket{}
classExperimentalRocketFactory
implementsRocketFactory<ExperimentalRocket>{}
IfwecallthebuildRocketmethodofaclientwithaninstanceofExperimentalRocketFactory,thereturntypewillautomaticallybeExperimentalRocket:
letclient=newClient();
letfactory=newExperimentalRocketFactory();
letrocket=client.buildRocket(factory);
BeforewecancompletetheimplementationoftheExperimentalRocketFactoryobject,weneedtodefineconcreteclassesfortheproductsofthefamily:
classExperimentalPayloadimplementsPayload{
weight:number;
}
classExperimentalRocketStageimplementsStage{
engines:Engine[];
}
classExperimentalRocketimplementsRocket{
payload:ExperimentalPayload;
stages:[ExperimentalRocketStage];
}
Note
Trivialinitializationsofpayloadandstageareomittedformorecompactcontent.Thesamekindsofomissionmaybeappliediftheyarenotnecessaryforthisbook.
Andnowwemaydefinethefactorymethodsofthisconcretefactoryclass:
classExperimentalRocketFactory
implementsRocketFactory<ExperimentalRocket>{
createRocket():ExperimentalRocket{
returnnewExperimentalRocket();
}
createPayload():ExperimentalPayload{
returnnewExperimentalPayload();
}
createStages():[ExperimentalRocketStage]{
return[newExperimentalRocketStage()];
}
}
Let'smoveontoanotherconcretefactorythatbuildsafreightrocketandproductsofitsfamily,startingwiththerocketcomponents:
classSatelliteimplementsPayload{
constructor(
publicid:number,
publicweight:number
){}
}
classFreightRocketFirstStageimplementsStage{
engines:Engine[];
}
classFreightRocketSecondStageimplementsStage{
engines:Engine[];
}
typeFreightRocketStages=
[FreightRocketFirstStage,FreightRocketSecondStage];
Continuewiththerocketitself:
classFreightRocketimplementsRocket{
payload:Satellite;
stages:FreightRocketStages;
}
Withthestructuresorclassesofthefreightrocketfamilydefined,wearereadytoimplementitsfactory:
classFreightRocketFactory
implementsRocketFactory<FreightRocket>{
nextSatelliteId=0;
createRocket():FreightRocket{
returnnewFreightRocket();
}
createPayload():Satellite{
returnnewSatellite(this.nextSatelliteId++,100);
}
createStages():FreightRocketStages{
return[
newFreightRocketFirstStage(),
newFreightRocketSecondStage()
];
}
}
Nowweonceagainhavetwofamiliesofrocketsandtheirfactories,andwecanusethesameclienttobuilddifferentrocketsbypassingdifferentfactories:
letclient=newClient();
letexperimentalRocketFactory=newExperimentalRocketFactory();
letfreightRocketFactory=newFreightRocketFactory();
letexperimentalRocket=
client.buildRocket(experimentalRocketFactory);
letfreightRocket=client.buildRocket(freightRocketFactory);
ConsequencesTheAbstractFactoryPatternmakesiteasyandsmoothtochangetheentirefamilyofproducts.Thisisthedirectbenefitbroughtbythefactorylevelabstraction.Asaconsequence,italsobringsotherbenefits,aswellassomedisadvantagesatthesametime.
Ontheonehand,itprovidesbettercompatibilitywithintheproductsinaspecificfamily.Astheproductsbuiltbyasinglefactoryareusuallymeanttoworktogether,wecanassumethattheytendtocooperatemoreeasily.
Butontheotherhand,itreliesonacommonoutlineofthebuildingprocess,althoughforawell-abstractedbuildingprocess,thiswon'talwaysbeanissue.Wecanalsoparameterizefactorymethodsonbothconcretefactoriesandtheclienttomaketheprocessmoreflexible.
Ofcourse,anabstractfactorydoesnothavetobeapureinterfaceoranabstractclasswithnomethodsimplemented.Animplementationinpracticeshouldbedecidedbasedondetailedcontext.
AlthoughtheAbstractFactoryPatternandFactoryMethodPatternhaveabstractionsofdifferentlevels,whattheyencapsulatearesimilar.Forbuildingaproductwithmultiplecomponents,thefactoriessplittheproductsintocomponentstogainflexibility.However,afixedfamilyofproductsandtheirinternalcomponentsmaynotalwayssatisfytherequirements,andthuswemayconsidertheBuilderPatternasanotheroption.
BuilderWhileFactoryPatternsexposetheinternalcomponents(suchasthepayloadandstagesofarocket),theBuilderPatternencapsulatesthembyexposingonlythebuildingstepsandprovidesthefinalproductsdirectly.Atthesametime,theBuilderPatternalsoencapsulatestheinternalstructuresofaproduct.Thismakesitpossibleforamoreflexibleabstractionandimplementationofbuildingcomplexobjects.
TheBuilderPatternalsointroducesanewrolecalleddirector,asshowninthefollowingdiagram.ItisquiteliketheclientintheAbstractFactoryPattern,althoughitcaresonlyaboutbuildstepsorpipelines:
NowtheonlyconstraintfromRocketBuilderthatappliestoaproductofitssubclassistheoverallshapeofaRocket.ThismightnotbringalotofbenefitswiththeRocketinterfacewepreviouslydefined,whichexposessomedetailsoftherocketthattheclients(byclientsImeanthosewhowanttosendtheirsatellitesorotherkindsofpayloadtospace)maynotcareaboutthatmuch.Fortheseclients,whattheywanttoknowmightjustbewhichorbittherocketiscapableofsendingtheirpayloadsto,ratherthanhowmanyandwhatstagesthisrockethas.
ParticipantsTheparticipantsofatypicalBuilderPatternimplementationincludethefollowing:
Builder:RocketBuilder
Definestheinterfaceofabuilderthatbuildsproducts.
Concretebuilder:FalconBuilder
Implementsmethodsthatbuildpartsoftheproducts,andkeepstrackofthecurrentbuildingstate.
Director
Definesthestepsandcollaborateswithbuilderstobuildproducts.
Finalproduct:Falcon
Theproductbuiltbyabuilder.
PatternscopeTheBuilderPatternhasasimilarscopetotheAbstractFactoryPattern,whichextractsabstractionfromacompletecollectionofoperationsthatwillfinallyinitiatetheproducts.ComparedtotheAbstractFactoryPattern,abuilderintheBuilderPatternfocusesmoreonthebuildingstepsandtheassociationbetweenthosesteps,whiletheAbstractFactoryPatternputsthatpartintotheclientsandmakesitsfactoryfocusonproducingcomponents.
ImplementationAsnowweareassumingthatstagesarenottheconcernoftheclientswhowanttobuyrocketstocarrytheirpayloads,wecanremovethestagespropertyfromthegeneralRocketinterface:
interfaceRocket{
payload:Payload;
}
Thereisarocketfamilycalledsoundingrocketthatsendsprobestonearspace.Andthismeanswedon'tevenneedtohavetheconceptofstages.SoundingRocketisgoingtohaveonlyoneenginepropertyotherthanpayload(whichwillbeaProbe),andtheonlyenginewillbeaSolidRocketEngine:
classProbeimplementsPayload{
weight:number;
}
classSolidRocketEngineextendsEngine{}
classSoundingRocketimplementsRocket{
payload:Probe;
engine:SolidRocketEngine;
}
Butstillweneedrocketstosendsatellites,whichusuallyuseLiquidRocketEngine:
classLiquidRocketEngineextendsEngine{
fuelLevel=0;
refuel(level:number):void{
this.fuelLevel=level;
}
}
AndwemightwanttohavethecorrespondingLiquidRocketStageabstractclassthathandlesrefuelling:
abstractclassLiquidRocketStageimplementsStage{
engines:LiquidRocketEngine[]=[];
refuel(level=100):void{
for(letengineofthis.engines){
engine.refuel(level);
}
}
}
NowwecanupdateFreightRocketFirstStageandFreightRocketSecondStageassubclassesofLiquidRocketStage:
classFreightRocketFirstStageextendsLiquidRocketStage{
constructor(thrust:number){
super();
letenginesNumber=4;
letsingleEngineThrust=thrust/enginesNumber;
for(leti=0;i<enginesNumber;i++){
letengine=
newLiquidRocketEngine(singleEngineThrust);
this.engines.push(engine);
}
}
}
classFreightRocketSecondStageextendsLiquidRocketStage{
constructor(thrust:number){
super();
this.engines.push(newLiquidRocketEngine(thrust));
}
}
TheFreightRocketwillremainthesameasitwas:
typeFreightRocketStages=
[FreightRocketFirstStage,FreightRocketSecondStage];
classFreightRocketimplementsRocket{
payload:Satellite;
stages=[]asFreightRocketStages;
}
And,ofcourse,thereisthebuilder.Thistime,wearegoingtouseanabstractclassthathasthebuilderpartiallyimplemented,withgenericsapplied:
abstractclassRocketBuilder<
TRocketextendsRocket,
TPayloadextendsPayload
>{
createRocket():void{}
addPayload(payload:TPayload):void{}
addStages():void{}
refuelRocket():void{}
abstractgetrocket():TRocket;
}
Note
There'sactuallynoabstractmethodinthisabstractclass.Oneofthereasonsisthatspecificstepsmightbeoptionaltocertainbuilders.Byimplementingno-opmethods,thesubclassescanjustleavethestepstheydon'tcareaboutempty.
HereistheimplementationoftheDirectorclass:
classDirector{
prepareRocket<
TRocketextendsRocket,
TPayloadextendsPayload
>(
builder:RocketBuilder<TRocket,TPayload>,
payload:TPayload
):TRocket{
builder.createRocket();
builder.addPayload(payload);
builder.addStages();
builder.refuelRocket();
returnbuilder.rocket;
}
}
Note
Becautious,withoutexplicitlyprovidingabuildingcontext,thebuilderinstancereliesonthebuildingpipelinesbeingqueued(eithersynchronouslyorasynchronously).Onewaytoavoidrisk(especiallywithasynchronousoperations)istoinitializeabuilderinstanceeverytimeyoupreparearocket.
Nowit'stimetoimplementconcretebuilders,startingwithSoundingRocketBuilder,whichbuildsaSoundingRocketwithonlyoneSolidRocketEngine:
classSoundingRocketBuilder
extendsRocketBuilder<SoundingRocket,Probe>{
privatebuildingRocket:SoundingRocket;
createRocket():void{
this.buildingRocket=newSoundingRocket();
}
addPayload(probe:Probe):void{
this.buildingRocket.payload=probe;
}
addStages():void{
letpayload=this.buildingRocket.payload;
this.buildingRocket.engine=
newSolidRocketEngine(payload.weight);
}
getrocket():SoundingRocket{
returnthis.buildingRocket;
}
}
Thereareseveralnotablethingsinthisimplementation:
TheaddStagesmethodreliesonthepreviouslyaddedpayloadtoaddanenginewiththe
correctthrustspecification.Therefuelmethodisnotoverridden(soitremainsno-op)becauseasolidrocketenginedoesnotneedtoberefueled.
We'vesensedalittleaboutthecontextprovidedbyabuilder,anditcouldhaveasignificantinfluenceontheresult.Forexample,let'stakealookatFreightRocketBuilder.ItcouldbesimilartoSoundingRocketifwedon'ttaketheaddStagesandrefuelmethodsintoconsideration:
classFreightRocketBuilder
extendsRocketBuilder<FreightRocket,Satellite>{
privatebuildingRocket:FreightRocket;
createRocket():void{
this.buildingRocket=newFreightRocket();
}
addPayload(satellite:Satellite):void{
this.buildingRocket.payload=satellite;
}
getrocket():FreightRocket{
returnthis.buildingRocket;
}
}
Assumethatapayloadthatweighslessthan1000takesonlyonestagetosendintospace,whilepayloadsweighingmoretaketwoormorestages:
addStages():void{
letrocket=this.buildingRocket;
letpayload=rocket.payload;
letstages=rocket.stages;
stages[0]=newFreightRocketFirstStage(payload.weight*4);
if(payload.weight>=FreightRocketBuilder.oneStageMax){
stages[1]=FreightRocketSecondStage(payload.weight);
}
}
staticoneStageMax=1000;
Whenitcomestorefueling,wecanevendecidehowmuchtorefuelbasedontheweightofthepayloads:
refuel():void{
letrocket=this.buildingRocket;
letpayload=rocket.payload;
letstages=rocket.stages;
letoneMax=FreightRocketBuilder.oneStageMax;
lettwoMax=FreightRocketBuilder.twoStagesMax;
letweight=payload.weight;
stages[0].refuel(Math.min(weight,oneMax)/oneMax*100);
if(weight>=oneMax){
stages[1]
.refuel((weight-oneMax)/(twoMax-oneMax)*100);
}
}
staticoneStageMax=1000;
statictwoStagesMax=2000;
Nowwecanpreparedifferentrocketsreadytolaunch,withdifferentbuilders:
letdirector=newDirector();
letsoundingRocketBuilder=newSoundingRocketBuilder();
letprobe=newProbe();
letsoundingRocket
=director.prepareRocket(soundingRocketBuilder,probe);
letfreightRocketBuilder=newFreightRocketBuilder();
letsatellite=newSatellite(0,1200);
letfreightRocket
=director.prepareRocket(freightRocketBuilder,satellite);
ConsequencesAstheBuilderPatterntakesgreatercontroloftheproductstructuresandhowthebuildingstepsinfluenceeachother,itprovidesthemaximumflexibilitybysubclassingthebuilderitself,withoutchangingthedirector(whichplaysasimilarroletoaclientintheAbstractFactoryPattern).
PrototypeAsJavaScriptisaprototype-basedprogramminglanguage,youmightbeusingprototyperelatedpatternsallthetimewithoutknowingit.
We'vetalkedaboutanexampleintheAbstractFactoryPattern,andpartofthecodeislikethis:
classFreightRocketFactory
implementsRocketFactory<FreightRocket>{
createRocket():FreightRocket{
returnnewFreightRocket();
}
}
Sometimeswemayneedtoaddasubclassjustforchangingtheclassnamewhileperformingthesamenewoperation.Instancesofasingleclassusuallysharethesamemethodsandproperties,sowecancloneoneexistinginstancefornewinstancestobecreated.Thatistheconceptofaprototype.
ButinJavaScript,withtheprototypeconceptbuilt-in,newConstructor()doesbasicallywhataclonemethodwoulddo.Soactuallyaconstructorcanplaytheroleofaconcretefactoryinsomeway:
interfaceConstructor<T>{
new():T;
}
functioncreateFancyObject<T>(constructor:Constructor<T>):T{
returnnewconstructor();
}
Withthisprivilege,wecanparameterizeproductorcomponentclassesaspartofotherpatternsandmakecreationevenmoreflexible.
ThereissomethingthatcouldeasilybeignoredwhentalkingaboutthePrototypePatterninJavaScript:cloningwiththestate.WiththeclasssyntaxsugarintroducedinES6,whichhidestheprototypemodifications,wemayoccasionallyforgetthatwecanactuallymodifyprototypesdirectly:
classBase{
state:number;
}
letbase=newBase();
base.state=0;
classDerivedextendsBase{}
Derived.prototype=base;
letderived=newDerived();
Now,thederivedobjectwillkeepthestateofthebaseobject.Thiscouldbeusefulwhenyouwanttocreatecopiesofaspecificinstance,butkeepinmindthatpropertiesinaprototypeofthesecopiesarenottheownpropertiesoftheseclonedobjects.
SingletonTherearescenariosinwhichonlyoneinstanceofthespecificclassshouldeverexist,andthatleadstoSingletonPattern.
BasicimplementationsThesimplestsingletoninJavaScriptisanobjectliteral;itprovidesaquickandcheapwaytocreateauniqueobject:
constsingleton={
foo():void{
console.log('bar');
}
};
Butsometimeswemightwantprivatevariables:
constsingleton=(()=>{
letbar='bar';
return{
foo():void{
console.log(bar);
}
};
})();
OrwewanttotaketheadvantageofananonymousconstructorfunctionorclassexpressioninES6:
constsingleton=newclass{
private_bar='bar';
foo():void{
console.log(this._bar);
}
}();
Note
Rememberthattheprivatemodifieronlyhasaneffectatcompiletime,andsimplydisappearsafterbeingcompiledtoJavaScript(althoughofcourseitsaccessibilitywillbekeptin.d.ts).
However,itispossibletohavetherequirementsforcreatingnewinstancesof"singletons"sometimes.Thusanormalclasswillstillbehelpful:
classSingleton{
bar='bar';
foo():void{
console.log(bar);
}
privatestatic_default:Singleton;
staticgetdefault():Singleton{
if(!Singleton._default){
Singleton._default=newSingleton();
}
returnSingleton._default;
}
}
Anotherbenefitbroughtbythisapproachislazyinitialization:theobjectonlygetsinitializedwhenitgetsaccessedthefirsttime.
ConditionalsingletonsSometimeswemightwanttoget"singletons"basedoncertainconditions.Forexample,everycountryusuallyhasonlyonecapitalcity,thusacapitalcitycouldbetreatedasasingletonunderthescopeofthespecificcountry.
Theconditioncouldalsobetheresultofcontextratherthanexplicitarguments.AssumingwehaveaclassEnvironmentanditsderivedclasses,WindowsEnvironmentandUnixEnvironment,wewouldliketoaccessthecorrectenvironmentsingletonacrossplatformsbyusingEnvironment.defaultandapparently,aselectioncouldbemadebythedefaultgetter.
Formorecomplexscenarios,wemightwantaregistration-basedimplementationtomakeitextendable.
SummaryInthischapter,we'vetalkedaboutseveralimportantcreationaldesignpatternsincludingtheFactoryMethod,AbstractFactory,Builder,Prototype,andSingleton.
StartingwiththeFactoryMethodPattern,whichprovidesflexibilitywithlimitedcomplexity,wealsoexploredtheAbstractFactoryPattern,theBuilderPatternandthePrototypePattern,whichsharesimilarlevelsofabstractionbutfocusondifferentaspects.ThesepatternshavemoreflexibilitythantheFactoryMethodPattern,butaremorecomplexatthesametime.Withtheknowledgeoftheideabehindeachofthepatterns,weshouldbeabletochooseandapplyapatternaccordingly.
Whilecomparingthedifferences,wealsofoundmanythingsincommonbetweendifferentcreationalpatterns.Thesepatternsareunlikelytobeisolatedfromothersandsomeofthemcanevencollaboratewithorcompleteeachother.
Inthenextchapter,we'llcontinuetodiscussstructuralpatternsthathelptoformlargeobjectswithcomplexstructures.
Chapter4.StructuralDesignPatternsWhilecreationalpatternsplaythepartofflexiblycreatingobjects,structuralpatterns,ontheotherhand,arepatternsaboutcomposingobjects.Inthischapter,wearegoingtotalkaboutstructuralpatternsthatfitdifferentscenarios.
Ifwetakeacloserlookatstructuralpatterns,theycanbedividedintostructuralclasspatternsandstructuralobjectpatterns.Structuralclasspatternsarepatternsthatplaywith"interestedparties"themselves,whilestructuralobjectpatternsarepatternsthatweavepiecestogether(likeCompositePattern).Thesetwokindsofstructuralpatternscomplementeachothertosomedegree.
Herearethepatternswe'llwalkthroughinthischapter:
Composite:Buildstree-likestructuresusingprimitiveandcompositeobjects.AgoodexamplewouldbetheDOMtreethatformsacompletepage.Decorator:Addsfunctionalitytoclassesorobjectsdynamically.Adapter:Providesageneralinterfaceandworkwithdifferentadapteesbyimplementingdifferentconcreteadapters.Considerprovidingdifferentdatabasechoicesforasinglecontentmanagementsystem.Bridge:Decouplestheabstractionfromitsimplementation,andmakebothoftheminterchangeable.Façade:Providesasimplifiedinterfaceforthecombinationofcomplexsubsystems.Flyweight:Sharesstatelessobjectsthatarebeingusedmanytimestoimprovememoryefficiencyandperformance.Proxy:Actsasthesurrogatethattakesextraresponsibilitieswhenaccessingobjectsitmanages.
CompositePatternObjectsunderthesameclasscouldvaryfromtheirpropertiesorevenspecificsubclasses,butacomplexobjectcanhavemorethanjustnormalproperties.TakingDOMelements,forexample,alltheelementsareinstancesofclassNode.Thesenodesformtreestructurestorepresentdifferentpages,buteverynodeinthesetreesiscompleteanduniformcomparedtothenodeattheroot:
<html>
<head>
<title>TypeScript</title>
</head>
<body>
<h1>TypeScript</h1>
<img/>
</body>
</html>
TheprecedingHTMLrepresentsaDOMstructurelikethis:
AlloftheprecedingobjectsareinstancesofNode,theyimplementtheinterfaceofacomponentinCompositePattern.SomeofthesenodeslikeHTMLelements(exceptforHTMLImageElement)inthisexamplehavechildnodes(components)whileothersdon't.
ParticipantsTheparticipantsofCompositePatternimplementationinclude:
Component:Node
Definestheinterfaceandimplementthedefaultbehaviorforobjectsofthecomposite.Itshouldalsoincludeaninterfacetoaccessandmanagethechildcomponentsofaninstance,andoptionallyareferencetoitsparent.Composite:IncludessomeHTMLelements,likeHTMLHeadElementandHTMLBodyElement
Storeschildcomponentsandimplementsrelatedoperations,andofcourseitsownbehaviors.Leaf:TextNode,HTMLImageElement
Definesbehaviorsofaprimitivecomponent.Client:
Manipulatesthecompositeanditscomponents.
PatternscopeCompositePatternapplieswhenobjectscanandshouldbeabstractedrecursivelyascomponentsthatformtreestructures.Usually,itwouldbeanaturalchoicewhenacertainstructureneedstobeformedasatree,suchastreesofviewcomponents,abstractsyntaxtrees,ortreesthatrepresentfilestructures.
ImplementationWearegoingtocreateacompositethatrepresentssimplefilestructuresandhaslimitedkindsofcomponents.
Firstofall,let'simportrelatednodemodules:
import*asPathfrom'path';
import*asFSfrom'fs';
Note
Modulepathandfsarebuilt-inmodulesofNode.js,pleaserefertoNode.jsdocumentationformoreinformation:https://nodejs.org/api/.
Note
Itismypersonalpreferencetohavethefirstletterofanamespace(ifit'snotafunctionatthesametime)inuppercase,whichreducesthechanceofconflictswithlocalvariables.ButamorepopularnamingstylefornamespaceinJavaScriptdoesnot.
Nowweneedtomakeabstractionofthecomponents,sayFileSystemObject:
abstractclassFileSystemObject{
constructor(
publicpath:string,
publicparent?:FileSystemObject
){}
getbasename():string{
returnPath.basename(this.path);
}
}
WeareusingabstractclassbecausewearenotexpectingtouseFileSystemObjectdirectly.Anoptionalparentpropertyisdefinedtoallowustovisittheuppercomponentofaspecificobject.Andthebasenamepropertyisaddedasahelperforgettingthebasenameofthepath.
TheFileSystemObjectisexpectedtohavesubclasses,FolderObjectandFileObject.ForFolderObject,whichisacompositethatmaycontainotherfoldersandfiles,wearegoingtoaddanitemsproperty(getter)thatreturnsotherFileSystemObjectitcontains:
classFolderObjectextendsFileSystemObject{
items:FileSystemObject[];
constructor(path:string,parent?:FileSystemObject){
super(path,parent);
}
}
Wecaninitializetheitemspropertyintheconstructorwithactualfilesandfoldersexistingatgivenpath:
this.items=FS
.readdirSync(this.path)
.map(path=>{
letstats=FS.statSync(path);
if(stats.isFile()){
returnnewFileObject(path,this);
}elseif(stats.isDirectory()){
returnnewFolderObject(path,this);
}else{
thrownewError('Notsupported');
}
});
Youmayhavenoticedweareformingitemswithdifferentkindsofobjects,andwearealsopassingthisastheparentofnewlycreatedchildcomponents.
AndforFileObject,we'lladdasimplereadAllmethodthatreadsallbytesofthefile:
classFileObjectextendsFileSystemObject{
readAll():Buffer{
returnFS.readFileSync(this.path);
}
}
Currently,wearereadingthechilditemsinsideafolderfromtheactualfilesystemwhenafolderobjectgetsinitiated.Thismightnotbenecessaryifwewanttoaccessthisstructureondemand.Wemayactuallycreateagetterthatcallsreaddironlywhenit'saccessed,thustheobjectwouldactlikeaproxytotherealfilesystem.
ConsequencesBoththeprimitiveobjectandcompositeobjectinCompositePatternsharethecomponentinterface,whichmakesiteasyfordeveloperstobuildacompositestructurewithfewerthingstoremember.
ItalsoenablesthepossibilityofusingmarkuplanguageslikeXMLandHTMLtorepresentareallycomplexobjectwithextremeflexibility.CompositePatterncanalsomaketherenderingeasierbyhavingcomponentsrenderedrecursively.
Asmostcomponentsarecompatiblewithhavingchildcomponentsorbeingchildcomponentsoftheirparentsthemselves,wecaneasilycreatenewcomponentsthatworkgreatwithexistingones.
DecoratorPatternDecoratorPatternaddsnewfunctionalitytoanobjectdynamically,usuallywithoutcompromisingtheoriginalfeatures.TheworddecoratorinDecoratorPatterndoessharesomethingwiththeworddecoratorintheES-nextdecoratorsyntax,buttheyarenotexactlythesame.ClassicalDecoratorPatternasaphrasewoulddifferevenmore.
TheclassicalDecoratorPatternworkswithacomposite,andthebriefideaistocreatedecoratorsascomponentsthatdothedecoratingwork.Ascompositeobjectsareusuallyprocessedrecursively,thedecoratorcomponentswouldgetprocessedautomatically.Soitbecomesyourchoicetodecidewhatitdoes.
Theinheritancehierarchycouldbelikethefollowingstructureshown:
Thedecoratorsareappliedrecursivelylikethis:
Therearetwoprerequisitesforthedecoratorstoworkcorrectly:theawarenessofcontextorobjectthatadecoratorisdecorating,andtheabilityofthedecoratorsbeingapplied.TheCompositePatterncaneasilycreatestructuresthatsatisfythosetwoprerequisites:
ThedecoratorknowswhatitdecoratesasthecomponentpropertyThedecoratorgetsappliedwhenitisrenderedrecursively
However,itdoesn'treallyneedtotakeastructurelikeacompositetogainthebenefitsfromDecoratorPatterninJavaScript.AsJavaScriptisadynamiclanguage,ifyoucangetyourdecoratorscalled,youmayaddwhateveryouwanttoanobject.
Takingmethodlogunderconsoleobjectasanexample,ifwewantatimestampbeforeeverylog,wecansimplyreplacethelogfunctionwithawrapperthathasthetimestampprefixed:
const_log=console.log;
console.log=function(){
lettimestamp=`[${newDate().toTimeString()}]`;
return_log.apply(this,[timestamp,...arguments]);
};
Certainly,thisexamplehaslittletodowiththeclassicalDecoratorPattern,butitenablesadifferentwayforthispatterntobedoneinJavaScript.Especiallywiththehelpofnewdecoratorsyntax:
classTarget{
@decorator
method(){
//...
}
}
Note
TypeScriptprovidesthedecoratorsyntaxtransformationasanexperimentalfeature.Tolearnmoreaboutdecoratorsyntax,pleasecheckoutthefollowinglink:http://www.typescriptlang.org/docs/handbook/decorators.html.
ParticipantsTheparticipantsofclassicalDecoratorPatternimplementationinclude:
Component:UIComponent
Definestheinterfaceoftheobjectsthatcanbedecorated.ConcreteComponent:TextComponent
Definesadditionalfunctionalitiesoftheconcretecomponent.Decorator:Decorator
Definesareferencetothecomponenttobedecorated,andmanagesthecontext.Conformstheinterfaceofacomponentwithproperbehaviors.ConcreteDecorator:ColorDecorator,FontDecorator
DefinesadditionalfeaturesandexposesAPIifnecessary.
PatternscopeDecoratorPatternusuallycaresaboutobjects,butasJavaScriptisprototype-based,decoratorswouldworkwellwiththeclassesofobjectsthroughtheirprototypes.
TheclassicalimplementationofDecoratorPatterncouldhavemuchincommonwithotherpatternswearegoingtotalkaboutlater,whilethefunctiononeseemstoshareless.
ImplementationInthispart,we'lltalkabouttwoimplementationsofDecoratorPattern.ThefirstonewouldbeclassicalDecoratorPatternthatdecoratesthetargetbywrappingwithnewclassesthatconformtotheinterfaceofUIComponent.Thesecondonewouldbedecoratorswritteninnewdecoratorsyntaxthatprocessestargetobjects.
Classicaldecorators
Let'sgetstartedbydefiningtheoutlineofobjectstobedecorated.First,we'llhavetheUIComponentasanabstractclass,definingitsabstractfunctiondraw:
abstractclassUIComponent{
abstractdraw():void;
}
ThenaTextComponentthatextendstheUIComponent,aswellasitstextcontentsofclassText:
classText{
content:string;
setColor(color:string):void{}
setFont(font:string):void{}
draw():void{}
}
classTextComponentextendsUIComponent{
texts:Text[];
draw():void{
for(lettextofthis.texts){
text.draw();
}
}
}
What'snextistodefinetheinterfaceofdecoratorstodecorateobjectsthatareinstancesofclassTextComponent:
classDecoratorextendsUIComponent{
constructor(
publiccomponent:TextComponent
){
super();
}
gettexts():Text[]{
returnthis.component.texts;
}
draw():void{
this.component.draw();
}
}
Nowwehaveeverythingforconcretedecorators.Inthisexample,ColorDecoratorandFontDecoratorlooksimilar:
classColorDecoratorextendsDecorator{
constructor(
component:TextComponent,
publiccolor:string
){
super(component);
}
draw():void{
for(lettextofthis.texts){
text.setColor(this.color);
}
super.draw();
}
}
classFontDecoratorextendsDecorator{
constructor(
component:TextComponent,
publicfont:string
){
super(component);
}
draw():void{
for(lettextofthis.texts){
text.setFont(this.font);
}
super.draw();
}
}
Note
Intheimplementationjustdescribed,this.textsindrawmethodcallsthegetterdefinedonclassDecorator.AsthisinthatcontextwouldideallybeaninstanceofclassColorDecoratororFontDecorator;thetextsitaccesseswouldfinallybethearrayinitscomponentproperty.
Note
Thiscouldbeevenmoreinterestingorconfusingifwehavenesteddecoratorslikewewillsoon.Trytodrawaschematicdiagramifitconfusesyoulater.
Nowit'stimetoactuallyassemblethem:
letdecoratedComponent=newColorDecorator(
newFontDecorator(
newTextComponent(),
'sans-serif'
),
'black'
);
Theorderofnestingdecoratorsdoesnotmatterinthisexample.AseitherColorDecoratororFontDecoratorisavalidUIComponent,theycanbeeasilydroppedinandreplacepreviousTextComponent.
DecoratorswithES-nextsyntax
ThereisalimitationwithclassicalDecoratorPatternthatcanbepointedoutdirectlyviaitsnestingformofdecorating.ThatappliestoES-nextdecoratorsaswell.Takealookatthefollowingexample:
classFoo{
@prefix
@suffix
getContent():string{
return'...';
}
}
Tip
Whatfollowsthe@characterisanexpressionthatevaluatestoadecorator.Whileadecoratorisafunctionthatprocessestargetobjects,weusuallyusehigher-orderfunctionstoparameterizeadecorator.
WenowhavetwodecoratorsprefixandsuffixdecoratingthegetContentmethod.Itseemsthattheyarejustparallelatfirstglance,butifwearegoingtoaddaprefixandsuffixontothecontentreturned,likewhatthenamesuggests,theprocedurewouldactuallyberecursiveratherthanparalleljustliketheclassicalimplementation.
Tomakedecoratorscooperatewithothersaswe'dexpect,weneedtohandlethingscarefully:
functionprefix(
target:Object,
name:string,
descriptor:PropertyDescriptor
):PropertyDescriptor{
letmethod=descriptor.valueasFunction;
if(typeofmethod!=='function'){
thrownewError('Expectingdecoratingamethod');
}
return{
value:function(){
return'[prefix]'+method.apply(this,arguments);
},
enumerable:descriptor.enumerable,
configurable:descriptor.configurable,
writable:descriptor.writable
};
}
Note
IncurrentECMAScriptdecoratorproposal,whendecoratingamethodorproperty(usuallywithgetterorsetter),youwillhavethethirdargumentpassedinasthepropertydescriptor.
Note
Checkoutthefollowinglinkformoreinformationaboutpropertydescriptors:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty.
Thesuffixdecoratorwouldbejustliketheprefixdecorator.SoI'llsavethecodelineshere.
ConsequencesThekeytotheDecoratorPatternisbeingabletoaddfunctionalitiesdynamically,anddecoratorsareusuallyexpectedtoplaynicewitheachother.ThoseexpectationsofDecoratorPatternmakeitreallyflexibletoformacustomizedobject.However,itwouldbehardforcertaintypesofdecoratorstoactuallyworkwelltogether.
Considerdecoratinganobjectwithmultipledecoratorsjustlikethesecondexampleofimplementation,wouldthedecoratingordermatter?Orshouldthedecoratingordermatter?
Aproperlywrittendecoratorshouldalwaysworknomatterwhereitisinthedecoratorslist.Andit'susuallypreferredthatthedecoratedtargetbehavesalmostthesamewithdecoratorsdecoratedindifferentorders.
AdapterPatternAdapterPatternconnectsexistingclassesorobjectswithanotherexistingclient.Itmakesclassesthatarenotdesignedtoworktogetherpossibletocooperatewitheachother.
Anadaptercouldbeeitheraclassadapteroranobjectadapter.AclassadapterextendstheadapteeclassandexposesextraAPIsthatwouldworkwiththeclient.Anobjectadapter,ontheotherhand,doesnotextendtheadapteeclass.Instead,itstorestheadapteeasadependency.
Theclassadapterisusefulwhenyouneedtoaccessprotectedmethodsorpropertiesoftheadapteeclass.However,italsohassomerestrictionswhenitcomestotheJavaScriptworld:
TheadapteeclassneedstobeextendableIftheclienttargetisanabstractclassotherthanpureinterface,youcan'textendtheadapteeclassandtheclienttargetwiththesameadapterclasswithoutamixinAsingleclasswithtwosetsofmethodsandpropertiescouldbeconfusing
Duetothoselimitations,wearegoingtotalkmoreaboutobjectadapters.Takingbrowser-sidestorageforexample,we'llassumewehaveaclientworkingwithstorageobjectsthathavebothmethodsgetandsetwithcorrectsignatures(forexample,astoragethatstoresdataonlinethroughAJAX).NowwewanttheclienttoworkwithIndexedDBforfasterresponseandofflineusage;we'llneedtocreateanadapterforIndexedDBthatgetsandsetsdata:
WearegoingtousePromiseforreceivingresultsorerrorsofasynchronousoperations.SeethefollowinglinkformoreinformationifyouarenotyetfamiliarwithPromise:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise.
ParticipantsTheparticipantsofAdapterPatterninclude:
Target:Storage
DefinestheinterfaceofexistingtargetsthatworkswithclientAdaptee:IndexedDB
TheimplementationthatisnotdesignedtoworkwiththeclientAdapter:IndexedDBStorage
ConformstheinterfaceoftargetandinteractswithadapteeClient.
Manipulatesthetarget
PatternscopeAdapterPatterncanbeappliedwhentheexistingclientclassisnotdesignedtoworkwiththeexistingadaptees.Itfocusesontheuniqueadapterpartwhenapplyingtodifferentcombinationsofclientsandadaptees.
ImplementationStartwiththeStorageinterface:
interfaceStorage{
get<T>(key:string):Promise<T>;
set<T>(key:string,value:T):Promise<void>;
}
Note
Wedefinedthegetmethodwithgeneric,sothatifweneitherspecifythegenerictype,norcastthevaluetypeofareturnedPromise,thetypeofthevaluewouldbe{}.Thiswouldprobablyfailfollowingtypechecking.
WiththehelpofexamplesfoundonMDN,wecannowsetuptheIndexedDBadapter.VisitIndexedDBStorage:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB.
ThecreationofIndexedDBinstancesisasynchronous.Wecouldputtheopeningoperationinsideagetorsetmethodsothedatabasecanbeopenedondemand.Butfornow,let'smakeiteasierbycreatinganinstanceofIndexedDBStoragethathasadatabaseinstancewhichisalreadyopened.
However,constructorsusuallydon'thaveasynchronouscode.Eveniftheydo,itcannotapplychangestotheinstancebeforecompletingtheconstruction.Fortunately,FactoryMethodPatternworkswellwithasynchronousinitiation:
classIndexedDBStorageimplementsStorage{
constructor(
publicdb:IDBDatabase,
publicstoreName='default'
){}
open(name:string):Promise<IndexedDBStorage>{
returnnewPromise<IndexedDBStorage>(
(resolve,reject)=>{
letrequest=indexedDB.open(name);
//...
});
}
}
InsidethePromiseresolverofmethodopen,we'llgettheasynchronousworkdone:
letrequest=indexedDB.open(name);
request.onsuccess=event=>{
letdb=request.resultasIDBDatabase;
letstorage=newIndexedDBStorage(db);
resolve(storage);
};
request.onerror=event=>{
reject(request.error);
};
NowwhenweareaccessinganinstanceofIndexedDBStorage,wecanassumeithasanopeneddatabaseandisreadytomakequeries.Tomakechangesortogetvaluesfromthedatabase,weneedtocreateatransaction.Here'show:
get<T>(key:string):Promise<T>{
returnnewPromise<T>((resolve,reject)=>{
lettransaction=this.db.transaction(this.storeName);
letstore=transaction.objectStore(this.storeName);
letrequest=store.get(key);
request.onsuccess=event=>{
resolve(request.result);
};
request.onerror=event=>{
reject(request.error);
};
});
}
Methodsetissimilar.Butwhilethetransactionisbydefaultread-only,weneedtoexplicitlyspecify'readwrite'mode.
set<T>(key:string,value:T):Promise<void>{
returnnewPromise<void>((resolve,reject)=>{
lettransaction=
this.db.transaction(this.storeName,'readwrite');
letstore=transaction.objectStore(this.storeName);
letrequest=store.put(value,key);
request.onsuccess=event=>{
resolve();
};
request.onerror=event=>{
reject(request.error);
};
});
}
Andnowwecanhaveadrop-inreplacementforthepreviousstorageusedbytheclient.
ConsequencesByapplyingAdapterPattern,wecanfillthegapbetweenclassesthatoriginallywouldnotworktogether.Inthissituation,AdapterPatternisquiteastraightforwardsolutionthatmightcometomind.
ButinotherscenarioslikeadebuggeradapterfordebuggingextensionsofanIDE,theimplementationofAdapterPatterncouldbemorechallenging.
BridgePatternBridgePatterndecouplestheabstractionmanipulatedbyclientsfromfunctionalimplementationsandmakesitpossibletoaddorreplacetheseabstractionsandimplementationseasily.
Takeasetofcross-APIUIelementsasanexample:
WehavetheabstractionUIElementthatcanaccessdifferentimplementationsofUIToolkitforcreatingdifferentUIbasedoneitherSVGorcanvas.Intheprecedingstructure,thebridgeistheconnectionbetweenUIElementandUIToolkit.
ParticipantsTheparticipantsofBridgePatterninclude:
Abstraction:UIElement
Definestheinterfaceofobjectstobemanipulatedbytheclientandstoresthereferencetoitsimplementer.Refinedabstraction:TextElement,ImageElement
Extendsabstractionwithspecializedbehaviors.Implementer:UIToolkit
Definestheinterfaceofageneralimplementerthatwilleventuallycarryouttheoperationsdefinedinabstractions.Theimplementerusuallycaresonlyaboutbasicoperationswhiletheabstractionwillhandlehigh-leveloperations.Concreteimplementer:SVGToolkit,CanvasToolkit
Implementstheimplementerinterfaceandmanipulateslow-levelAPIs.
PatternscopeAlthoughhavingabstractionandimplementerdecoupledprovidesBridgePatternwiththeabilitytoworkwithseveralabstractionsandimplementers,mostofthetime,bridgepatternsworkonlywithasingleimplementer.
Ifyoutakeacloserlook,youwillfindBridgePatternisextremelysimilartoAdapterPattern.However,whileAdapterPatterntriestomakeexistingclassescooperateandfocusesontheadapterspart,BridgePatternforeseesthedivergencesandprovidesawell-thought-outanduniversalinterfaceforitsabstractionsthatplaythepartofadapters.
ImplementationAworkingimplementationcouldbenon-trivialintheexamplewearetalkingabout.Butwecanstillsketchouttheskeletoneasily.
StartwithimplementerUIToolkitandabstractionUIElementthataredirectlyrelatedtothebridgeconcept:
interfaceUIToolkit{
drawBorder():void;
drawImage(src:string):void;
drawText(text:string):void;
}
abstractclassUIElement{
constructor(
publictoolkit:UIToolkit
){}
abstractrender():void;
}
AndnowwecanextendUIElementforrefinedabstractionswithdifferentbehaviors.FirsttheTextElementclass:
classTextElementextendsUIElement{
constructor(
publictext:string,
toolkit:UIToolkit
){
super(toolkit);
}
render():void{
this.toolkit.drawText(this.text);
}
}
AndtheImageElementclasswithsimilarcode:
classImageElementextendsUIElement{
constructor(
publicsrc:string,
toolkit:UIToolkit
){
super(toolkit);
}
render():void{
this.toolkit.drawImage(this.src);
}
}
BycreatingconcreteUIToolkitsubclasses,wecanmanagetomakeeverythingtogetherwiththeclient.Butasitcouldleadtohardworkwewouldnotwanttotouchnow,we'llskipitbyusingavariablepointingtoundefinedinthisexample:
lettoolkit:UIToolkit;
letimageElement=newImageElement('foo.jpg',toolkit);
lettextElement=newTextElement('bar',toolkit);
imageElement.render();
textElement.render();
Intherealworld,therenderpartcouldalsobeaheavylift.Butasit'scodedatarelativelyhigher-level,ittorturesyouinadifferentway.
ConsequencesDespitehavingcompletelydifferentnamesfortheabstraction(UIElement)intheexampleaboveandtheadapterinterface(Storage),theyplaysimilarrolesinastaticcombination.
However,aswementionedinthepatternscopesection,theintentionsofBridgePatternandAdapterPatterndiffer.
Bydecouplingtheabstractionandimplementer,BridgePatternbringsgreatextensibilitytothesystem.Theclientdoesnotneedtoknowabouttheimplementationdetails,andthishelpstobuildmorestablesystemsasitformsahealthierdependencystructure.
AnotherbonusthatmightbebroughtbyBridgePatternisthat,withaproperlyconfiguredbuildprocess,itcanreducecompilationtimeasthecompilerdoesnotneedtoknowinformationontheotherendofthebridgewhenchangesaremadetoarefinedabstractionorconcreteimplementer.
FaçadePatternTheFaçadePatternorganizessubsystemsandprovidesaunifiedhigher-levelinterface.Anexamplethatmightbefamiliartoyouisamodularsystem.InJavaScript(andofcourseTypeScript),peopleusemodulestoorganizecode.Amodularsystemmakesprojectseasiertomaintain,asacleanprojectstructurecanhelprevealtheinterconnectionsamongdifferentpartsoftheproject.
Itiscommonthatoneprojectgetsreferencedbyothers,butobviouslytheprojectthatreferencesotherprojectsdoesn'tandshouldn'tcaremuchabouttheinnerstructuresofitsdependencies.Thusafaçadecanbeintroducedforadependencyprojecttoprovideahigher-levelAPIandexposewhatreallymatterstoitsdependents.
Takearobotasanexample.Peoplewhobuildarobotanditscomponentswillneedtocontroleverypartseparatelyandletthemcooperateatthesametime.However,peoplewhowanttousethisrobotwouldonlyneedtosendsimplecommandslike"walk"and"jump".
Forthemostflexibleusage,therobot"SDK"canprovideclasseslikeMotionController,FeedbackController,Thigh,Shank,Footandsoon.Possiblylikethefollowingimageshows:
Butcertainly,mostofthepeoplewhowanttocontrolorprogramthisrobotdonotwanttoknowasmanydetailsasthis.Whattheyreallywantisnotafancytoolboxwitheverything
inbox,butjustanintegralrobotthatfollowstheircommands.Thustherobot"SDK"canactuallyprovideafaçadethatcontrolstheinnerpiecesandexposesmuchsimplerAPIs:
Unfortunately,FaçadePatternleavesusanopenquestionofhowtodesignthefaçadeAPIandsubsystems.Answeringthisquestionproperlyisnoteasywork.
ParticipantsTheparticipantsofaFaçadePatternarerelativelysimplewhenitcomestotheircategories:
Façade:Robot
Definesasetofhigher-levelinterfaces,andmakessubsystemscooperate.Subsystems:MotionController,FeedbackController,Thigh,ShankandFoot
Implementstheirownfunctionalitiesandcommunicatesinternallywithothersubsystemsifnecessary.Subsystemsaredependenciesofafaçade,andtheydonotdependonthefaçade.
PatternscopeFaçadesusuallyactasjunctionsthatconnectahigher-levelsystemanditssubsystems.ThekeytotheFaçadePatternistodrawalinebetweenwhatadependentshouldorshouldn'tcareaboutofitsdependencies.
ImplementationConsiderputtinguparobotwithitsleftandrightlegs,wecanactuallyaddanotherabstractionlayercalledLegthatmanagesThigh,Shank,andFoot.Ifwearegoingtoseparatemotionandfeedbackcontrollerstodifferentlegsrespectively,wemayalsoaddthosetwoaspartoftheLeg:
classLeg{
thigh:Thigh;
shank:Shank;
foot:Foot;
motionController:MotionController;
feedbackController:FeedbackController;
}
BeforeweaddmoredetailstoLeg,let'sfirstdefineMotionControllerandFeedbackController.
TheMotionControllerissupposedtocontrolawholelegbasedonavalueorasetofvalues.Herewearesimplifyingthatasasingleanglefornotbeingdistractedbythisimpossiblerobot:
classMotionController{
constructor(
publicleg:Leg
){}
setAngle(angle:number):void{
let{
thigh,
shank,
foot
}=this.leg;
//...
}
}
AndtheFeedbackControllerissupposedtobeaninstanceofEventEmitterthatreportsthestatechangesorusefulevents:
import{EventEmitter}from'events';
classFeedbackControllerextendsEventEmitter{
constructor(
publicfoot:Foot
){
super();
}
}
NowwecanmakeclassLegrelativelycomplete:
classLeg{
thigh=newThigh();
shank=newShank();
foot=newFoot();
motionController:MotionController;
feedbackController:FeedbackController;
constructor(){
this.motionController=
newMotionController(this);
this.feedbackController=
newFeedbackController(this.foot);
this.feedbackController.on('touch',()=>{
//...
});
}
}
Let'sputtwolegstogethertosketchtheskeletonofarobot:
classRobot{
leftLegMotion:MotionController;
rightLegMotion:MotionController;
leftFootFeedback:FeedbackController;
rightFootFeedback:FeedbackController;
walk(steps:number):void{}
jump(strength:number):void{}
}
I'momittingthedefinitionofclassesThigh,Shank,andFootaswearenotactuallygoingtowalktherobot.NowforauserthatonlywantstowalkorjumparobotviasimpleAPI,theycanmakeitviatheRobotobjectthathaseverythingconnected.
ConsequencesFaçadePatternloosensthecouplingbetweenclientandsubsystems.Thoughitdoesnotdecouplethemcompletelyasyouwillprobablystillneedtoworkwithobjectsdefinedinsubsystems.
Façadesusuallyforwardoperationsfromclienttopropersubsystemsorevendoheavyworktomakethemworktogether.
WiththehelpofFaçadePattern,thesystemandtherelationshipandstructurewithinthesystemcanstaycleanandintuitive.
FlyweightPatternAflyweightinFlyweightPatternisastatelessobjectthatcanbesharedacrossobjectsormaybeclassesmanytimes.Obviously,thatsuggestsFlyweightPatternisapatternaboutmemoryefficiencyandmaybeperformanceiftheconstructionofobjectsisexpensive.
Takingdrawingsnowflakesasanexample.Despiterealsnowflakesbeingdifferenttoeachother,whenwearetryingtodrawthemontocanvas,weusuallyhavealimitednumberofstyles.However,byaddingpropertieslikesizesandtransformations,wecancreateabeautifulsnowscenewithlimitedsnowflakestyles.
Asaflyweightisstateless,ideallyitallowsmultipleoperationssimultaneously.Youmightneedtobecautiouswhenworkingwithmulti-threadstuff.Fortunately,JavaScriptisusuallysingle-threadedandavoidsthisissueifallrelatedcodeissynchronous.Youwillstillneedtotakecareindetailedscenariosifyourcodeisworkingasynchronously.
AssumewehavesomeflyweightsofclassSnowflake:
Whenitsnows,itwouldlooklikethis:
Intheimageabove,snowflakesindifferentstylesaretheresultofrenderingwithdifferentproperties.
It'scommonthatwewouldhavestylesandimageresourcesbeingloadeddynamically,thuswecoulduseaFlyweightFactoryforcreatingandmanagingflyweightobjects.
ParticipantsThesimplestimplementationofFlyweightPatternhasthefollowingparticipants:
Flyweight:Snowflake
Definestheclassofflyweightobjects.Flyweightfactory:FlyweightFactory
Createsandmanagesflyweightobjects.Client.
Storesstatesoftargetsandusesflyweightobjectstomanipulatethesetargets.
Withtheseparticipants,weassumethatthemanipulationcouldbeaccomplishedthroughflyweightswithdifferentstates.Itwouldalsobehelpfulsometimestohaveconcreteflyweightclassallowingcustomizedbehaviors.
PatternscopeFlyweightPatternisaresultofeffortstoimprovingmemoryefficiencyandperformance.Theimplementationcaresabouthavingtheinstancesbeingstateless,anditisusuallytheclientthatmanagesdetailedstatesfordifferenttargets.
ImplementationWhatmakesFlyweightPatternusefulinthesnowflakeexampleisthatasnowflakewiththesamestyleusuallysharesthesameimage.Theimageiswhatconsumestimetoloadandoccupiesnotablememory.
WearestartingwithafakeImageclassthatpretendstoloadimages:
classImage{
constructor(url:string){}
}
TheSnowflakeclassinourexamplehasonlyasingleimageproperty,andthatisapropertythatwillbesharedbymanysnowflakestobedrawn.Astheinstanceisnowstateless,parametersfromcontextarerequiredforrendering:
classSnowflake{
image:Image;
constructor(
publicstyle:string
){
leturl=style+'.png';
this.image=newImage(url);
}
render(x:number,y:number,angle:number):void{
//...
}
}
Theflyweightsaremanagedbyafactoryforeasieraccessing.We'llhaveaSnowflakeFactorythatcachescreatedsnowflakeobjectswithcertainstyles:
consthasOwnProperty=Object.prototype.hasOwnProperty;
classSnowflakeFactory{
cache:{
[style:string]:Snowflake;
}={};
get(style:string):Snowflake{
letcache=this.cache;
letsnowflake:Snowflake;
if(hasOwnProperty.call(cache,style)){
snowflake=cache[style];
}else{
snowflake=newSnowflake(style);
cache[style]=snowflake;
}
returnsnowflake;
}
}
Withbuildingblocksready,we'llimplementtheclient(Sky)thatsnows:
constSNOW_STYLES=['A','B','C'];
classSky{
constructor(
publicwidth:number,
publicheight:number
){}
snow(factory:SnowflakeFactory,count:number){}
}
Wearegoingtofilltheskywithrandomsnowflakesatrandompositions.Beforethatlet'screateahelperfunctionthatgeneratesanumberbetween0andamaxvaluegiven:
functiongetRandomInteger(max:number):number{
returnMath.floor(Math.random()*max);
}
AndthencompletemethodsnowofSky:
snow(factory:SnowflakeFactory,count:number){
letstylesCount=SNOW_STYLES.length;
for(leti=0;i<count;i++){
letstyle=SNOW_STYLES[getRandomInteger(stylesCount)];
letsnowflake=factory.get(style);
letx=getRandomInteger(this.width);
lety=getRandomInteger(this.height);
letangle=getRandomInteger(60);
snowflake.render(x,y,angle);
}
}
NowwemayhavethousandsofsnowflakesintheskybutwithonlythreeinstancesofSnowflakecreated.Youcancontinuethisexamplebystoringthestateofsnowflakesandanimatingthesnowing.
ConsequencesFlyweightPatternreducesthetotalnumberofobjectsinvolvedinasystem.Asadirectresult,itmaysavequitealotmemory.Thissavingbecomesmoresignificantwhentheflyweightsgetusedbytheclientthatprocessesalargenumberoftargets.
FlyweightPatternalsobringsextralogicintothesystem.Whentouseornottousethispatternisagainabalancinggamebetweendevelopmentefficiencyandruntimeefficiencyfromthispointofview.Thoughmostofthetime,ifthere'snotagoodreason,wegowithdevelopmentefficiency.
ProxyPatternProxyPatternapplieswhentheprogramneedstoknowaboutortointervenethebehaviorofaccessingobjects.ThereareseveraldetailedscenariosinProxyPattern,andwecandistinguishthosescenariosbytheirdifferentpurposes:
Remoteproxy:Aproxywithinterfacetomanipulateremoteobjects,suchasdataitemsonaremoteserverVirtualproxy:AproxythatmanagesexpensiveobjectswhichneedtobeloadedondemandProtectionproxy:Aproxythatcontrolsaccesstotargetobjects,typicallyitverifiespermissionsandvalidatesvaluesSmartproxy:Aproxythatdoesadditionaloperationswhenaccessingtargetobjects
InthesectionofAdapterPattern,weusedfactorymethodopenthatcreatesanobjectasynchronously.Asatrade-off,wehadtolettheclientwaitbeforetheobjectgetscreated.
WithProxyPattern,wecouldnowopendatabaseondemandandcreatestorageinstancessynchronously.
Note
Aproxyisusuallydedicatedtoobjectorobjectswithknownmethodsandproperties.Butwith
thenewProxyAPIprovidedinES6,wecangetmoreinterestingthingsdonebygettingtoknowwhatmethodsorpropertiesarebeingaccessed.Pleaserefertothefollowinglinkformoreinformation:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy.
ParticipantsTheparticipantsofProxyPatterninclude:
Proxy:IndexedDBStorage
Definesinterfaceandimplementsoperationstomanageaccesstothesubject.Subject:IndexedDB
Thesubjecttobeaccessedbyproxy.Client:Accessessubjectviaproxy.
PatternscopeDespitehavingasimilarstructuretoAdapterPattern,thekeyofProxyPatternistointervenetheaccesstotargetobjectsratherthantoadaptanincompatibleinterface.Sometimesitmightchangetheresultofaspecificmethodorthevalueofacertainproperty,butthatisprobablyforfallingbackorexceptionhandlingpurposes.
ImplementationTherearetwodifferenceswe'llhaveinthisimplementationcomparedtotheexampleforpureAdapterPattern.First,we'llcreatetheIndexedDBStorageinstancewithaconstructor,andhavethedatabaseopenedondemand.Second,wearegoingtoaddauselesspermissioncheckingformethodsgetandset.
Nowwhenwecallthemethodgetorset,thedatabasecouldeitherhavebeenopenedornot.Promiseisagreatchoiceforrepresentingavaluethatmighteitherbependingorsettled.Considerthisexample:
letready=newPromise<string>(resolve=>{
setTimeout(()=>{
resolve('biu~');
},Math.random()*1000);
});
setTimeout(()=>{
ready.then(text=>{
console.log(text);
});
},999);
It'shardtotellwhetherPromisereadyisfulfilledwhenthesecondtimeoutfires.Buttheoverallbehavioriseasytopredict:itwilllogthe'biu~'textinaround1second.ByreplacingthePromisevariablereadywithamethodorgetter,itwouldbeabletostarttheasynchronousoperationonlywhenneeded.
Solet'sstarttherefactoringofclassIndexedDBStoragewiththegetterthatcreatesthePromiseofthedatabasetobeopened:
privatedbPromise:Promise<IDBDatabase>;
constructor(
publicname:string,
publicstoreName='default'
){}
privategetdbReady():Promise<IDBDatabase>{
if(!this.dbPromise){
this.dbPromise=
newPromise<IDBDatabase>((resolve,reject)=>{
letrequest=indexedDB.open(name);
request.onsuccess=event=>{
resolve(request.result);
};
request.onerror=event=>{
reject(request.error);
};
});
}
returnthis.dbPromise;
}
NowthefirsttimeweaccesspropertydbReady,itwillopenthedatabaseandcreateaPromisethatwillbefulfilledwiththedatabasebeingopened.Tomakethisworkwithmethodsgetandset,wejustneedtowrapwhatwe'veimplementedintoathenmethodfollowingthedbReadyPromise.
Firstformethodget:
get<T>(key:string):Promise<T>{
returnthis
.dbReady
.then(db=>newPromise<T>((resolve,reject)=>{
lettransaction=db.transaction(this.storeName);
letstore=transaction.objectStore(this.storeName);
letrequest=store.get(key);
request.onsuccess=event=>{
resolve(request.result);
};
request.onerror=event=>{
reject(request.error);
};
}));
}
Andfollowedbyupdatedmethodset:
set<T>(key:string,value:T):Promise<void>{
returnthis
.dbReady
.then(db=>newPromise<void>((resolve,reject)=>{
lettransaction=db
.transaction(this.storeName,'readwrite');
letstore=transaction.objectStore(this.storeName);
letrequest=store.put(value,key);
request.onsuccess=event=>{
resolve();
};
request.onerror=event=>{
reject(request.error);
};
}));
}
NowwefinallyhavetheIndexedDBStoragepropertythatcandoarealdrop-inreplacement
fortheclientthatsupportstheinterface.Wearealsogoingtoaddsimplepermissioncheckingwithaplainobjectthatdescribesthepermissionofreadandwrite:
interfacePermission{
write:boolean;
read:boolean;
}
Thenwewilladdpermissioncheckingformethodgetandsetseparately:
get<T>(key:string):Promise<T>{
if(!this.permission.read){
returnPromise.reject<T>(newError('Permissiondenied'));
}
//...
}
set<T>(key:string,value:T):Promise<void>{
if(!this.permission.write){
returnPromise.reject(newError('Permissiondenied'));
}
//...
}
YoumayrecallDecoratorPatternwhenyouarethinkingaboutthepermissioncheckingpart,anddecoratorscouldbeusedtosimplifythelineswritten.Trytousedecoratorsyntaxtoimplementthispermissioncheckingyourself.
ConsequencesTheimplementationofProxyPatterncanusuallybetreatedastheencapsulationoftheoperationstospecificobjectsortargets.Itiseasytohavetheencapsulationaugmentedwithoutextraburdenontheclient.
Forexample,aworkingonlinedatabaseproxycoulddomuchmorethanjustactinglikeaplainsurrogate.Itmaycachedataandchangeslocally,orsynchronizeonschedulewithouttheclientbeingaware.
SummaryInthischapter,welearnedaboutstructuraldesignpatternsincludingComposite,Decorator,Adapter,Bridge,Façade,Flyweight,andProxy.Againwefoundsomeofthesepatternsarehighlyinterrelatedandevensimilartoeachothertosomedegree.
Forexample,wemixedCompositePatternwithDecoratorPattern,AdapterPatternwithProxyPattern,comparedAdapterPatternandBridgePattern.Duringthejourneyofexploring,wesometimesfounditwasjustanaturalresulttohaveourcodeendinapatternthat'ssimilartowhatwe'velistedifwetookwritingbettercodeintoconsideration.
TakingAdapterPatternandBridgePatternasanexample,whenwearetryingtomaketwoclassescooperate,itcomesoutwithAdapterPatternandwhenweareplanningonconnectingwithdifferentclassesinadvance,itgoeswithBridgePattern.Therearenoactuallinesbetweeneachpatternandtheapplicationsofthosepatterns,thoughthetechniquesbehindpatternscouldusuallybeuseful.
Inthenextchapter,wearegoingtotalkaboutbehavioralpatternsthathelptoformalgorithmsandassigntheresponsibilities.
Chapter5.BehavioralDesignPatternsAsthenamesuggests,behavioraldesignpatternsarepatternsabouthowobjectsorclassesinteractwitheachother.Theimplementationofbehavioraldesignpatternsusuallyrequirescertaindatastructurestosupporttheinteractioninasystem.However,behavioralpatternsandstructuralpatternsfocusondifferentaspectswhenapplied.Asaresult,youmightfindpatternsinthecategoryofbehavioraldesignpatternsusuallyhavesimplerormorestraightforwardstructurescomparedtostructuraldesignpatterns.
Inthischapter,wearegoingtotalkaboutsomeofthefollowingcommonbehavioralpatterns:
ChainofResponsibility:OrganizesbehaviorswithdifferentscopesCommand:ExposescommandsfromtheinternalwithencapsulatedcontextMemento:ProvidesanapproachformanagingstatesoutsideoftheirownerswithoutexposingdetailedimplementationsIterator:ProvidesauniversalinterfacefortraversingMediator:Itgroupscouplingandlogicallyrelatedobjectsandmakesinterconnectionscleanerinasystemthatmanagesmanyobjects
ChainofResponsibilityPatternTherearemanyscenariosunderwhichwemightwanttoapplycertainactionsthatcanfallbackfromadetailedscopetoamoregeneralone.
AniceexamplewouldbethehelpinformationofaGUIapplication:whenauserrequestshelpinformationforacertainpartoftheuserinterface,itisexpectedtoshowinformationasspecificaspossible.Thiscanbedonewithdifferentimplementations,andthemostintuitiveoneforawebdevelopercouldbeeventsbubbling.
ConsideraDOMstructurelikethis:
<divclass="outer">
<divclass="inner">
<spanclass="origin"></span>
</div>
</div>
Ifauserclicksonthespan.originelement,aclickeventwouldstartbubblingfromthespanelementtothedocumentroot(ifuseCaptureisfalse):
$('.origin').click(event=>{
console.log('Clickon`span.origin`.');
});
$('.outer').click(event=>{
console.log('Clickon`div.outer`.');
});
Bydefault,itwilltriggerbotheventlistenersaddedintheprecedingcode.Tostopthepropagationassoonasaneventgetshandled,wecancallitsstopPropagationmethod:
$('.origin').click(event=>{
console.log('Clickon`span.origin`.');
event.stopPropagation();
});
$('.outer').click(event=>{
Console.log('Clickon`div.outer`.');
});
Thoughaclickeventisnotexactlythesameasthehelpinformationrequest,withthesupportofcustomevents,it'squiteeasytohandlehelpinformationwithnecessarydetailedorgeneralinformationinthesamechain.
AnotherimportantimplementationoftheChainofResponsibilityPatternisrelatedtoerrorhandling.Aprimitiveexampleforthiscouldbeusingtry...catch.Considercodelikethis:wehavethreefunctions:foo,bar,andbiu,fooiscalledbybarwhilebariscalledbybiu:
functionfoo(){
//throwsomeerrors.
}
functionbar(){
foo();
}
functionbiu(){
bar();
}
biu();
Insidebothfunctionsbarandbiu,wecandosomeerrorcatching.Assumingfunctionfoothrowstwokindsoferrors:
functionfoo(){
letvalue=Math.random();
if(value<0.5){
thrownewError('Awesomeerror');
}elseif(value<0.8){
thrownewTypeError('Awesometypeerror');
}
}
InfunctionbarwewouldliketohandletheTypeErrorandleaveothererrorsthrowingon:
functionbar(){
try{
foo();
}catch(error){
if(errorinstanceofTypeError){
console.log('Sometypeerroroccurs',error);
}else{
throwerror;
}
}
}
Andinfunctionbiu,wewouldliketoaddmoregeneralhandlingthatcatchesalltheerrorssothattheprogramwillnotcrash:
functionbiu(){
try{
bar();
}catch(error){
console.log('Someerroroccurs',error);
}
}
Sobyusingtry...catchstatements,youmayhavebeenusingtheChainofResponsibilityPatternconstantlywithoutpayinganyattentiontoit.Justlikeyoumayhavebeenusingotherwell-knowndesignpatternsallthetime.
IfweabstractthestructureofChainofResponsibilityPatternintoobjects,wecouldhavesomethingasillustratedinthefigure:
ParticipantsTheparticipantsoftheChainofResponsibilityPatterninclude:
Handler:Definestheinterfaceofthehandlerwithsuccessorandmethodtohandlerequests.ThisisdoneimplicitlywithclasseslikeEventEmitterandtry...catchsyntax.Concretehandler:EventListener,catchblockandHandlerA/HandlerBintheclassversion.Defineshandlersintheformofcallbacks,codeblocksandclassesthathandlerequests.Client:Initiatestherequeststhatgothroughthechain.
PatternscopeTheChainofResponsibilityPatternitselfcouldbeappliedtomanydifferentscopesinaprogram.Itrequiresamulti-levelchaintowork,butthischaincouldbeindifferentforms.We'vebeenplayingwitheventsaswellastry...catchstatementsthathavestructurallevels,thispatterncouldalsobeappliedtoscenariosthathavelogicallevels.
Considerobjectsmarkedwithdifferentscopesusingstring:
letobjectA={
scope:'user.installation.package'
};
letobjectB={
scope:'user.installation'
};
Nowwehavetwoobjectswithrelatedscopesspecifiedbystring,andbyaddingfilterstothesescopestrings,wecanapplyoperationsfromspecificonestogeneralones.
ImplementationInthispart,wearegoingtoimplementtheclassversionwe'vementionedattheendoftheintroductiontotheChainofResponsibilityPattern.Considerrequeststhatcouldeitheraskforhelpinformationorfeedbackprompts:
typeRequestType='help'|'feedback';
interfaceRequest{
type:RequestType;
}
Note
Weareusingstringliteraltypeherewithuniontype.ItisaprettyusefulfeatureprovidedinTypeScriptthatplayswellwithexistingJavaScriptcodingstyles.Seethefollowinglinkformoreinformation:http://www.typescriptlang.org/docs/handbook/advanced-types.html.
Oneofthekeyprocessesforthispatternisgoingthroughthehandlers'chainandfindingoutthemostspecifichandlerthat'savailablefortherequest.Thereareseveralwaystoachievethis:byrecursivelyinvokingthehandlemethodofasuccessor,orhavingaseparatelogicwalkingthroughthehandlersuccessorchainuntiltherequestisconfirmedashandled.
Thelogicwalkingthroughthechaininthesecondwayrequirestheacknowledgmentofwhetherarequesthasbeenproperlyhandled.Thiscanbedoneeitherbyastateindicatorontherequestobjectorbythereturnvalueofthehandlemethod.
We'llgowiththerecursiveimplementationinthispart.Firstly,wewantthedefaulthandlingbehaviorofahandlertobeforwardingrequeststoitssuccessorifany:
classHandler{
privatesuccessor:Handler;
handle(request:Request):void{
if(this.successor){
this.successor.handle(request);
}
}
}
AndnowforHelpHandler,ithandleshelprequestsbutforwardsothers:
classHelpHandlerextendsHandler{
handle(request:Request):void{
if(request.type==='help'){
//Showhelpinformation.
}else{
super.handle(request);
}
}
}
ThecodeforFeedbackHandlerissimilar:
classFeedbackHandlerextendsHandler{
handle(request:Request):void{
if(request.type==='feedback'){
//Promptforfeedback.
}else{
super.handle(request);
}
}
}
Thus,achainofhandlerscouldbemadeupinsomeway.Andifarequestgotinthischain,itwouldbepassedonuntilahandlerrecognizesandhandlesit.However,itisnotnecessarytohaveallrequestshandledafterprocessingthem.Thehandlerscanalwayspassarequestonwhetherthisrequestgetsprocessedbythishandlerornot.
ConsequencesTheChainofResponsibilityPatterndecouplestheconnectionbetweenobjectsthatissuetherequestsandlogicthathandlesthoserequests.Thesenderassumesthatitsrequestscould,butnotnecessarily,beproperlyhandledwithoutknowingthedetails.Forsomeimplementations,itisalsoveryeasytoaddnewresponsibilitiestoaspecifichandleronthechain.Thisprovidesnotableflexibilityforhandlingrequests.
Besidestheexampleswe'vebeentalkingabout,thereisanotherimportantmutationoftry...catchthatcanbetreatedintheChainofResponsibilityPattern-Promise.Withinasmallerscope,thechaincouldberepresentedas:
promise
.catch(TypeError,reason=>{
//handlesTypeError.
})
.catch(ReferenceError,reason=>{
//handlesReferenceError.
})
.catch(reason=>{
//handlesothererrors.
});
Note
ThestandardcatchmethodonanESPromiseobjectdoesnotprovidetheoverloadthatacceptsanerrortypeasaparameter,butmanyimplementationsdo.
Inalargerscope,thischainwouldusuallyappearwhenthecodeisplayingwiththird-partylibraries.Acommonusagewouldbeconvertingerrorsproducedbyotherlibrariestoerrorsknowntothecurrentproject.We'lltalkmoreabouterrorhandlingofasynchronouscodelaterinthisbook.
CommandPatternCommandPatterninvolvesencapsulatingoperationsasexecutablecommandsandcouldeitherbeintheformofobjectsorfunctionsinJavaScript.Itiscommonthatwemaywanttomakeoperationsrelyoncertaincontextandstatesthatarenotaccessiblefortheinvokers.Bystoringthosepiecesofinformationwithacommandandpassingitout,thissituationcouldbeproperlyhandled.
Consideranextremelysimpleexample:wewanttoprovideafunctioncalledwait,whichreturnsacancelhandler:
functionwait(){
let$layer=$('.wait-layer');
$layer.show();
return()=>{
$layer.hide();
};
}
letcancel=wait();
setTimeout(()=>cancel(),1000);
Thecancelhandlerintheprecedingcodeisjustacommandweweretalkingabout.Itstoresthecontext($layer)usingclosureandispassedoutasthereturnvalueoffunctionwait.
ClosureinJavaScriptprovidesareallysimplewaytostorecommandcontextandstates,however,thedirectdisadvantagewouldbecompromisedflexibilitybetweencontext/statesandcommandfunctionsbecauseclosureislexicallydeterminedandcannotbechangedatruntime.Thiswouldbeokayifthecommandisonlyexpectedtobeinvokedwithfixedcontextandstates,butformorecomplexsituations,wemightneedtoconstructthemasobjectswithaproperdatastructure.
ThefollowingdiagramshowstheoverallrelationsbetweenparticipantsofCommandPattern:
Byproperlysplittingapartcontextandstateswiththecommandobject,CommandPatterncouldalsoplaywellwithFlyweightPatternifyouwantedtoreusecommandobjectsmultipletimes.
OthercommonextensionsbasedonCommandPatternincludeundosupportandmacroswithmultiplecommands.Wearegoingtoplaywiththoselaterintheimplementationpart.
ParticipantsTheparticipantsofCommandPatterninclude:
Command:Definesthegeneralinterfaceofcommandspassingaround,itcouldbeafunctionsignatureifthecommandsareintheformoffunctions.Concretecommand:Definesthespecificbehaviorsandrelateddatastructure.ItcouldalsobeafunctionthatmatchesthesignaturedeclaredasCommand.Thecancelhandlerintheveryfirstexampleisaconcretecommand.Context:Thecontextorreceiverthatthecommandisassociatedwith.Inthefirstexample,itisthe$layer.Client:Createsconcretecommandsandtheircontexts.Invoker:Executesconcretecommands.
PatternscopeCommandPatternsuggeststwoseparatepartsinasingleapplicationoralargersystem:clientandinvoker.Inthesimplifiedexamplewaitandcancel,itcouldbehardtodistinguishthedifferencebetweenthoseparts.Butthelineisclear:clientknowsorcontrolsthecontextofcommandstobeexecutedwith,whileinvokerdoesnothaveaccessordoesnotneedtocareaboutthatinformation.
ThekeytotheCommandPatternistheseparationandbridgingbetweenthosetwopartsthroughcommandsthatstorecontextandstates.
ImplementationIt'scommonforaneditortoexposecommandsforthird-partyextensionstomodifythetextcontent.ConsideraTextContextthatcontainsinformationaboutthetextfilebeingeditedandanabstractTextCommandclassassociatedwiththatcontext:
classTextContext{
content='textcontent';
}
abstractclassTextCommand{
constructor(
publiccontext:TextContext
){}
abstractexecute(...args:any[]):void;
}
Certainly,TextContextcouldcontainmuchmoreinformationlikelanguage,encoding,andsoon.Youcanaddtheminyourownimplementationformorefunctionality.Nowwearegoingtocreatetwocommands:ReplaceCommandandInsertCommand.
classReplaceCommandextendsTextCommand{
execute(index:number,length:number,text:string):void{
letcontent=this.context.content;
this.context.content=
content.substr(0,index)+
text+
content.substr(index+length);
}
}
classInsertCommandextendsTextCommand{
execute(index:number,text:string):void{
letcontent=this.context.content;
this.context.content=
content.substr(0,index)+
text+
content.substr(index);
}
}
ThosetwocommandssharesimilarlogicandactuallyInsertCommandcanbetreatedasasubsetofReplaceCommand.Orifwehaveanewdeletecommand,thenreplacecommandcanbetreatedasthecombinationofdeleteandinsertcommands.
Nowlet'sassemblethosecommandswiththeclientandinvoker:
classClient{
privatecontext=newTextContext();
replaceCommand=newReplaceCommand(this.context);
insertCommand=newInsertCommand(this.context);
}
letclient=newClient();
$('.replace-button').click(()=>{
client.replaceCommand.execute(0,4,'the');
});
$('.insert-button').click(()=>{
client.insertCommand.execute(0,'awesome');
});
Ifwegofurther,wecanactuallyhaveacommandthatexecutesothercommands.Namely,wecanhavemacrocommands.Thoughtheprecedingexamplealonedoesnotmakeitnecessarytocreateamacrocommand,therewouldbescenarioswheremacrocommandshelp.Asthosecommandsarealreadyassociatedwiththeircontexts,amacrocommandusuallydoesnotneedtohaveanexplicitcontext:
interfaceTextCommandInfo{
command:TextCommand,
args:any[];
}
classMacroTextCommand{
constructor(
publicinfos:TextCommandInfo[]
){}
execute():void{
for(letinfoofthis.infos){
info.command.execute(...info.args);
}
}
}
ConsequencesCommandPatterndecouplestheclient(whoknowsorcontrolscontext)andtheinvoker(whohasnoaccesstoordoesnotcareaboutdetailedcontext).
ItplayswellwithCompositePattern.Considertheexampleofmacrocommandswementionedabove:amacrocommandcanhaveothermacrocommandsasitscomponents,thuswemakeitacompositecommand.
AnotherimportantcaseofCommandPatternisaddingsupportforundooperations.Adirectapproachistoaddtheundomethodtoeverycommand.Whenanundooperationisrequested,invoketheundomethodofcommandsinreverseorder,andwecanpraythateverycommandwouldbeundonecorrectly.However,thisapproachreliesheavilyonaflawlessimplementationoftheundomethodaseverymistakewillaccumulate.Toimplementmorestableundosupport,redundantinformationorsnapshotscouldbestored.
MementoPatternWe'vetalkedaboutanundosupportimplementationintheprevioussectionontheCommandPattern,andfounditwasnoteasytoimplementthemechanismpurelybasedonreversingalltheoperations.However,ifwetakesnapshotsofobjectsastheirhistory,wemaymanagetoavoidaccumulatingmistakesandmakethesystemmorestable.Butthenwehaveaproblem:weneedtostorethestatesofobjectswhilethestatesareencapsulatedwithobjectsthemselves.
MementoPatternhelpsinthissituation.Whileamementocarriesthestateofanobjectatacertaintimepoint,italsocontrolstheprocessofsettingthestatebacktoanobject.Thismakestheinternalstateimplementationhiddenfromtheundomechanisminthefollowingexample:
Wehavetheinstancesofthemementocontrollingthestaterestorationintheprecedingstructure.Itcanalsobecontrolledbythecaretaker,namelytheundomechanism,forsimplestaterestoringcases.
ParticipantsTheparticipantsofMementoPatterninclude:
Memento:StoresthestateofanobjectanddefinesmethodrestoreorotherAPIsforrestoringthestatestospecificobjectsOriginator:DealswithobjectsthatneedtohavetheirinternalstatesstoredCaretaker:Managesmementoswithoutinterveningwithwhat'sinside
PatternscopeMementoPatternmainlydoestwothings:itpreventsthecaretakerfromknowingtheinternalstateimplementationanddecouplesthestateretrievingandrestoringprocessfromstatesmanagedbytheCaretakerorOriginator.
Whenthestateretrievingandrestoringprocessesaresimple,havingseparatedmementosdoesnothelpmuchifyouarealreadykeepingthedecouplingideainmind.
ImplementationStartwithanemptyStateinterfaceandMementoclass.AswedonotwantCaretakertoknowthedetailsaboutstateinsideanOriginatororMemento,wewouldliketomakestatepropertyofMementoprivate.HavingrestorationlogicinsideMementodoesalsohelpwiththis,andthusweneedmethodrestore.Sothatwedon'tneedtoexposeapublicinterfaceforreadingstateinsideamemento.
AndasanobjectassignmentinJavaScriptassignsonlyitsreference,wewouldliketodoaquickcopyforthestates(assumingstateobjectsaresingle-level):
interfaceState{}
classMemento{
privatestate:State;
constructor(state:State){
this.state=Object.assign({}asState,state);
}
restore(state:State):void{
Object.assign(state,this.state);
}
}
ForOriginatorweuseagetterandasetterforcreatingandrestoringspecificmementos:
classOriginator{
state:State;
getmemento():Memento{
returnnewMemento(this.state);
}
setmemento(memento:Memento){
memento.restore(this.state);
}
}
NowtheCaretakerwouldmanagethehistoryaccumulatedwithmementos:
classCaretaker{
originator:Originator;
history:Memento[]=[];
save():void{
this.history.push(this.originator.memento);
}
restore():void{
this.originator.memento=this.history.shift();
}
}
InsomeimplementationsofMementoPattern,agetStatemethodisprovidedforinstancesofOriginatortoreadstatefromamemento.ButtopreventclassesotherthanOriginatorfromaccessingthestateproperty,itmayrelyonlanguagefeatureslikeafriendmodifiertorestricttheaccess(whichisnotyetavailableinTypeScript).
ConsequencesMementoPatternmakesiteasierforacaretakertomanagethestatesoforiginatorsanditbecomespossibletoextendstateretrievingandrestoring.However,aperfectimplementationthatsealseverythingmightrelyonlanguagefeaturesaswe'vementionedbefore.Usingmementoscouldalsobringaperformancecostastheyusuallycontainredundantinformationintradeofstability.
IteratorPatternIteratorPatternprovidesauniversalinterfaceforaccessinginternalelementsofanaggregatewithoutexposingtheunderlyingdatastructure.Atypicaliteratorcontainsthefollowingmethodsorgetters:
first():movesthecursortothefirstelementintheaggregatesnext():movesthecursortothenextelementend:agetterthatreturnsaBooleanindicateswhetherthecursorisattheenditem:agetterthatreturnstheelementatthepositionofthecurrentcursorindex:agetterthatreturnstheindexoftheelementatthecurrentcursor
Iteratorsforaggregateswithdifferentinterfacesorunderlyingstructuresusuallyendwithdifferentimplementationsasshowninthefollowingfigure:
Thoughtheclientdoesnothavetoworryaboutthestructureofanaggregate,aniteratorwouldcertainlyneedto.Assumingwehaveeverythingweneedtobuildaniterator,therecouldbeavarietyofwaysforcreatingone.Thefactorymethodiswidelyusedwhencreatingiterators,orafactorygetterifnoparameterisrequired.
StartingwithES6,syntaxsugarfor...ofisaddedandworksforallobjectswithproperty
Symbol.iterator.Thismakesiteveneasierandmorecomfortablefordeveloperstoworkwithcustomizedlistsandotherclassesthatcanbeiterated.
ParticipantsTheparticipantsofIteratorPatterninclude:
Iterator:AbstractListIterator
Definestheuniversaliteratorinterfacethatisgoingtotransversedifferentaggregates.Concreteiterator:ListIterator,SkipListIteratorandReversedListIterator
Implementsspecificiteratorthattransversesandkeepstrackofaspecificaggregate.Aggregate:AbstractList
Definesabasicinterfaceofaggregatesthatiteratorsaregoingtoworkwith.Concreateaggregate:ListandSkipList
Definesthedatastructureandfactorymethod/getterforcreatingassociatediterators.
PatternscopeIteratorPatternprovidesaunifiedinterfacefortraversingaggregates.Inasystemthatdoesn'trelyoniterators,themainfunctionalityprovidedbyiteratorscouldbeeasilytakenoverbysimplehelpers.However,thereusabilityofthosehelperscouldbereducedasthesystemgrows.
ImplementationInthispart,wearegoingtoimplementastraightforwardarrayiterator,aswellasanES6iterator.
Simplearrayiterator
Let'sstartbycreatinganiteratorforaJavaScriptarray,whichshouldbeextremelyeasy.Firstly,theuniversalinterface:
interfaceIterator<T>{
first():void;
next():void;
end:boolean;
item:T;
index:number;
}
Note
PleasenoticethattheTypeScriptdeclarationforES6hasalreadydeclaredaninterfacecalledIterator.Considerputtingthecodeinthispartintoanamespaceormoduletoavoidconflicts.
Andtheimplementationofasimplearrayiteratorcouldbe:
classArrayIterator<T>implementsIterator<T>{
index=0;
constructor(
publicarray:T[]
){}
first():void{
this.index=0;
}
next():void{
this.index++;
}
getend():boolean{
returnthis.index>=this.array.length;
}
getitem():T{
returnthis.array[this.index];
}
}
NowweneedtoextendtheprototypeofnativeArraytoaddaniteratorgetter:
Object.defineProperty(Array.prototype,'iterator',{
get(){
returnnewArrayIterator(this);
}
});
TomakeiteratoravalidpropertyoftheArrayinstance,wealsoneedtoextendtheinterfaceofArray:
interfaceArray<T>{
iterator:IteratorPattern.Iterator<T>;
}
Note
Thisshouldbewrittenoutsidethenamespaceundertheglobalscope.Orifyouareinamoduleorambientmodule,youmightwanttotrydeclareglobal{...}foraddingnewpropertiestoexistingglobalinterfaces.
ES6iterator
ES6providessyntaxsugarfor...ofandotherhelpersforiterableobjects,namelytheobjectsthathaveimplementedtheIterableinterfaceofthefollowing:
interfaceIteratorResult<T>{
done:boolean;
value:T;
}
interfaceIterator<T>{
next(value?:any):IteratorResult<T>;
return?(value?:any):IteratorResult<T>;
throw?(e?:any):IteratorResult<T>;
}
interfaceIterable<T>{
[Symbol.iterator]():Iterator<T>;
}
Assumewehaveaclasswiththefollowingstructure:
classSomeData<T>{
array:T[];
}
Andwewouldliketomakeititerable.Morespecifically,wewouldliketomakeititeratesreversely.AstheIterableinterfacesuggests,wejustneedtoaddamethodwithaspecialnameSymbol.iteratorforcreatinganIterator.Let'scalltheiteratorSomeIterator:
classSomeIterator<T>implementsIterator<T>{
index:number;
constructor(
publicarray:T[]
){
this.index=array.length-1;
}
next():IteratorResult<T>{
if(this.index<=this.array.length){
return{
value:undefined,
done:true
};
}else{
return{
value:this.array[this.index--],
done:false
}
}
}
}
Andthendefinetheiteratormethod:
classSomeData<T>{
array:T[];
[Symbol.iterator](){
returnnewSomeIterator<T>(this.array);
}
}
NowwewouldhaveSomeDatathatworkswithfor...of.
Note
Iteratorsalsoplaywellwithgenerators;seethefollowinglinkformoreexamples:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols.
ConsequencesIteratorPatterndecouplesiterationusagefromthedatastructurethatisbeingiterated.Thedirectbenefitofthisisenablinganinterchangeabledataclassthatmayhavecompletelydifferentinternalstructures,likeanarrayandbinarytree.Also,onedatastructurecanbeiteratedviadifferentiteratorswithdifferenttraversalmechanismsandresultsindifferentordersandefficiencies.
Aunifiediteratorinterfaceinonesystemcouldalsohelpthedeveloperfrombeingconfusedwhenfacingdifferentaggregates.Aswementionedpreviously,somelanguagelikeyourbelovedJavaScriptprovidesalanguagelevelabstractionforiteratorsandmakeslifeeveneasier.
MediatorPatternTheconnectionsbetweenUIcomponentsandrelatedobjectscouldbeextremelycomplex.Object-orientedprogrammingdistributesfunctionalitiesamongobjects.Thismakescodingeasierwithcleanerandmoreintuitivelogic;however,itdoesnotensurethereusabilityandsometimesmakesitdifficulttounderstandifyoulookatthecodeagainaftersomedays(youmaystillunderstandeverysingleoperationbutwouldbeconfusedabouttheinterconnectionsifthenetworkbecomesreallyintricate).
Considerapageforeditinguserprofile.Therearestandaloneinputslikenicknameandtagline,aswellasinputsthatarerelatedtoeachother.Takinglocationselectionforexample,therecouldeasilybeatree-levellocationandtheoptionsavailableinlowerlevelsaredeterminedbytheselectionofhigherlevels.However,ifthoseobjectsaremanageddirectlybyasinglehugecontroller,itwillresultinapagethathaslimitedreusability.Thecodeformedunderthissituationwouldalsotendtohaveahierarchythat'slesscleanforpeopletounderstand.
MediatorPatterntriestosolvethisproblembyseparatingcouplingelementsandobjectsasgroups,andaddingadirectorbetweenagroupofelementsandotherobjectsasshowninthefollowingfigure:
Thoseobjectsformamediatorwiththeircolleaguesthatcaninteractwithotherobjectsasasingleobject.Withproperencapsulation,themediatorwillhavebetterreusabilityasithasjusttherightsizeandproperlydividedfunctionality.Intheworldofwebfrontenddevelopment,thereareconceptsorimplementationsthatfitMediatorPatternwell,likeWebComponentandReact.
ParticipantsTheparticipantsofMediatorPatterninclude:
Mediator:
Usually,theabstractionorskeletonpredefinedbyaframework.Definestheinterfacethatcolleaguesinamediatorcommunicatethrough.Concretemediator:LocationPicker
Managesthecolleaguesandmakesthemcooperate,providingahigherlevelinterfaceforobjectsoutside.Colleagueclasses:CountryInput,ProvinceInput,CityInput
Definesreferencestotheirmediatorandnotifiesitschangestothemediatorandacceptsmodificationsissuedbythemediator.
PatternscopeMediatorPatterncouldconnectmanypartsofaproject,butdoesnothavedirectorenormousimpactontheoutline.Mostofthecreditisgivenbecauseofincreasedusabilityandcleanerinterconnectionsintroducedbymediators.However,alongwithaniceoverallarchitecture,MediatorPatterncanhelpalotwithrefinedcodequality,andmaketheprojecteasiertomaintain.
ImplementationUsinglibrarieslikeReactwouldmakeitveryeasytoimplementMediatorPattern,butfornow,wearegoingwitharelativelyprimitivewayandhandlechangesbyhand.Let'sthinkabouttheresultwewantforaLocationPickerwe'vediscussed,andhopefully,itincludescountry,provinceandcityfields:
interfaceLocationResult{
country:string;
province:string;
city:string;
}
AndnowwecansketchtheoverallstructureofclassLocationPicker:
classLocationPicker{
$country=$(document.createElement('select'));
$province=$(document.createElement('select'));
$city=$(document.createElement('select'));
$element=$(document.createElement('div'))
.append(this.$country)
.append(this.$province)
.append(this.$city);
getvalue():LocationResult{
return{
country:this.$country.val(),
province:this.$province.val(),
city:this.$city.val()
};
}
}
Beforewecantellhowthecolleaguesaregoingtocooperate,wewouldliketoaddahelpermethodsetOptionsforupdatingoptionsinaselectelement:
privatestaticsetOptions(
$select:JQuery,
values:string[]
):void{
$select.empty();
let$options=values.map(value=>{
return$(document.createElement('option'))
.text(value)
.val(value);
});
$select.append($options);
}
Ipersonallytendtohavemethodsthatdonotdependonaspecificinstancestaticmethodsand
thisappliestomethodsgetCountries,getProvincesByCountry,andgetCitiesByCountryAndProvincethatsimplyreturnalistbytheinformationgivenasfunctionarguments(thoughwearenotgoingtoactuallyimplementthatpart):
privatestaticgetCountries():string[]{
return['-'].concat([/*countries*/]);
}
privatestaticgetProvincesByCountry(country:string):string[]{
return['-'].concat([/*provinces*/]);
}
privatestaticgetCitiesByCountryAndProvince(
country:string,
province:string
):string[]{
return['-'].concat([/*cities*/]);
}
Nowwemayaddmethodsforupdatingoptionsintheselectelements:
updateProvinceOptions():void{
letcountry:string=this.$country.val();
letprovinces=LocationPicker.getProvincesByCountry(country);
LocationPicker.setOptions(this.$province,provinces);
this.$city.val('-');
}
updateCityOptions():void{
letcountry:string=this.$country.val();
letprovince:string=this.$province.val();
letcities=LocationPicker
.getCitiesByCountryAndProvince(country,province);
LocationPicker.setOptions(this.$city,cities);
}
Finally,weavethosecolleaguestogetherandaddlistenerstothechangeevents:
constructor(){
LocationPicker
.setOptions(this.$country,LocationPicker.getCountries());
LocationPicker.setOptions(this.$province,['-']);
LocationPicker.setOptions(this.$city,['-']);
this.$country.change(()=>{
this.updateProvinceOptions();
});
this.$province.change(()=>{
this.updateCityOptions();
});
}
ConsequencesMediatorPattern,likemanyotherdesignpatterns,downgradesalevel-100problemintotwolevel-10problemsandsolvesthemseparately.Awell-designedmediatorusuallyhasapropersizeandusuallytendstobereusedinthefuture.Forexample,wemightnotwanttoputnicknameinputtogetherwiththecountry,province,andcityinputsasthiscombinationdoesn'ttendtooccurinothersituations(whichmeanstheyarenotstronglyrelated).
Astheprojectevolves,amediatormaygrowtoasizethat'snotefficientanymore.Soaproperlydesignedmediatorshouldalsotakethedimensionoftimeintoconsideration.
SummaryInthischapter,wetalkedaboutsomecommonbehavioralpatternsfordifferentscopesanddifferentscenarios.ChainofResponsibilityPatternandCommandPatterncanapplytoarelativelywiderangeofscopes,whileotherpatternsmentionedinthischapterusuallycaremoreaboutthescopewithobjectsandclassesdirectlyrelated.
Behavioralpatternswe'vetalkedaboutinthischapterarelesslikeeachothercomparedtocreationalpatternsandstructuralpatternswepreviouslywalkedthrough.Someofthebehavioralpatternscouldcompetewithothers,butmanyofthemcouldcooperate.Forexample,wetalkedaboutCommandPatternwithMementoPatterntoimplementundosupport.Manyothersmaycooperateinparallelanddotheirownpart.
Inthenextchapter,we'llcontinuetalkingaboutotherbehavioraldesignpatternsthatareusefulandwidelyused.
Chapter6.BehavioralDesignPatterns:ContinuousInthepreviouschapter,we'vealreadytalkedaboutsomeofthebehavioraldesignpatterns.We'llbecontinuingwithmorepatternsinthiscategoryinthischapter,including:StrategyPattern,StatePattern,TemplateMethodPattern,ObserverPattern,andVisitorPattern.
Manyofthesepatternssharethesameidea:unifytheshapeandvarythedetails.Hereisaquickoverview:
StrategyPatternandTemplatePattern:DefinesthesameoutlineofalgorithmsStatePattern:ProvidesdifferentbehaviorforobjectsindifferentstateswiththesameinterfaceObserverPattern:ProvidesaunifiedprocessofhandlingsubjectchangesandnotifyingobserversVisitorPattern:DoessimilarjobsasStrategyPatternsometimes,butavoidsanovercomplexinterfacethatmightberequiredforStrategyPatterntohandleobjectsinmanydifferenttypes
Patternsthatwillbediscussedinthischaptercouldbeappliedindifferentscopesjustasmanypatternsinothercategories.
StrategyPatternIt'scommonthataprogramhassimilaroutlinesforprocessingdifferenttargetswithdifferentdetailedalgorithms.StrategyPatternencapsulatesthosealgorithmsandmakestheminterchangeablewithinthesharedoutline.
Considerconflictingmergingprocessesofdatasynchronization,whichwetalkedaboutinChapter2,TheChallengeofIncreasingComplexity.Beforerefactoring,thecodewaslikethis:
if(type==='value'){
//...
}elseif(type==='increment'){
//...
}elseif(type==='set'){
//...
}
Butlaterwefoundoutthatwecouldactuallyextractthesameoutlinesfromdifferentphasesofthesynchronizationprocess,andencapsulatethemasdifferentstrategies.Afterrefactoring,theoutlineofthecodebecameasfollows:
letstrategy=strategies[type];
strategy.operation();
WegetalotofwaystocomposeandorganizethosestrategyobjectsorclassessometimesinJavaScript.ApossiblestructureforStrategyPatterncouldbe:
Inthisstructure,theclientisresponsibleforfetchingspecificstrategiesfromthetableand
applyingoperationsofthecurrentphase.
Anotherstructureisusingcontextualobjectsandlettingthemcontroltheirownstrategies:
Thustheclientneedsonlytolinkaspecificcontextwiththecorrespondingstrategy.
ParticipantsWe'vementionedtwopossiblestructuresforStrategyPattern,solet'sdiscusstheparticipantsseparately.Forthefirststructure,theparticipantsincludethefollowing:
Strategy
Definestheinterfaceofstrategyobjectsorclasses.Concretestrategy:ConcreteStrategyAandConcreteStrategyB
ImplementsconcretestrategyoperationsdefinedbytheStrategyinterface.Strategymanager:Strategies
Definesadatastructuretomanagestrategyobjects.Intheexample,it'sjustasimplehashtablethatusesdatatypenamesaskeysandstrategyobjectsasvalues.Itcouldbemorecomplexondemand:forexample,withmatchingpatternsorconditions.Target
Thetargettoapplyalgorithmsdefinedinstrategyobjects.Client
Makestargetsandstrategiescooperate.
Theparticipantsofthesecondstructureincludethefollowing:
Strategyandconcretestrategy
Thesameasintheprecedingsection.Context
Definesareferencetothestrategyobjectapplied.Providesrelatedmethodsorpropertygettersforclientstooperate.Client
Managescontextobjects.
PatternscopeStrategyPatternisusuallyappliedtoscopeswithsmallormediumsizes.Itprovidesawaytoencapsulatealgorithmsandmakesthosealgorithmseasiertomanageunderthesameoutline.StrategyPatterncanalsobethecoreofanentiresolutionsometimes,andagoodexampleisthesynchronizationimplementationwe'vebeenplayingwith.Inthiscase,StrategyPatternbuildsthebridgeofpluginsandmakesthesystemextendable.Butmostofthetime,thefundamentalworkdonebyStrategyPatternisdecouplingconcretestrategies,contexts,ortargets.
ImplementationTheimplementationstartswithdefiningtheinterfacesofobjectswe'llbeplayingwith.Wehavetwotargettypesinstringliteraltype'a'and'b'.Targetsoftype'a'havearesultpropertywithtypestring,whiletargetsoftype'b'haveavaluepropertywithtypenumber.
Theinterfaceswe'llhavelook,arelike:
typeTargetType='a'|'b';
interfaceTarget{
type:TargetType;
}
interfaceTargetAextendsTarget{
type:'a';
result:string;
}
interfaceTargetBextendsTarget{
type:'b';
value:number;
}
interfaceStrategy<TTargetextendsTarget>{
operationX(target:TTarget):void;
operationY(target:TTarget):void;
}
Nowwe'lldefinetheconcretestrategyobjectswithoutaconstructor:
letstrategyA:Strategy<TargetA>={
operationX(target){
target.result=target.result+target.result;
},
operationY(target){
target.result=target
.result
.substr(Math.floor(target.result.length/2));
}
};
letstrategyB:Strategy<TargetB>={
operationX(target){
target.value=target.value*2;
},
operationY(target){
target.value=Math.floor(target.value/2);
}
};
Tomakeiteasierforaclienttofetchthosestrategies,we'llputthemintoahashtable:
letstrategies:{
[type:string]:Strategy<Target>
}={
a:strategyA,
b:strategyB
};
Andnowwecanmakethemworkwithtargetsindifferenttypes:
lettargets:Target[]=[
{type:'a'},
{type:'a'},
{type:'b'}
];
for(lettargetoftargets){
letstrategy=strategies[target.type];
strategy.operationX(target);
strategy.operationY(target);
}
ConsequencesStrategyPatternmakestheforeseeableadditionofalgorithmsforcontextsortargetsundernewcategorieseasier.Italsomakestheoutlineofaprocessevencleanerbyhidingtrivialbranchesofbehaviorsselection.
However,theabstractionofalgorithmsdefinedbytheStrategyinterfacemaykeepgrowingwhilewearetryingtoaddmorestrategiesandsatisfytheirrequirementsofparameters.ThiscouldbeaproblemforaStrategyPatternwithclientsthataremanagingtargetsandstrategies.Butfortheotherstructureswhichthereferencesofstrategyobjectsarestoredbycontextsthemselves,wecanmanagetotrade-offtheinterchangeability.ThiswouldresultinVisitorPattern,whichwearegoingtotalkaboutlaterinthischapter.
Andaswe'vementionedbefore,StrategyPatterncanalsoprovidenotableextensibilityifanextendablestrategymanagerisavailableortheclientofcontextsisdesignedto.
StatePatternIt'spossibleforsomeobjectstobehavecompletelydifferentlywhentheyareindifferentstates.Let'sthinkaboutaneasyexamplefirst.Considerrenderingandinteractingwithacustombuttonintwostates:enabledanddisabled.Whenthebuttonisenabled,itlightsupandchangesitsstyletoactiveonamousehover,andofcourse,ithandlesclicks;whendisabled,itdimsandnolongercaresaboutmouseevents.
Wemaythinkofanabstractionwithtwooperations:render(withaparameterthatindicateswhetherthemouseishovering)andclick;alongwithtwostates:enabledanddisabled.Wecanevendividedeeperandhavestateactive,butthatwon'tbenecessaryinourcase.
AndnowwecanhaveStateEnabledwithbothrenderandclickmethodsimplemented,whilehavingStateDisabledwithonlyrendermethodimplementedbecauseitdoesnotcareaboutthehoverparameter.Inthisexample,weareexpectingeverymethodofthestatesbeingcallable.SowecanhavetheabstractclassStatewithemptyrenderandclickmethods.
ParticipantsTheparticipantsofStatePatternincludethefollowing:
State
Definestheinterfaceofstateobjectsthatarebeingswitchedtointernally.Concretestate:StateEnabledandStateDisabled
ImplementstheStateinterfacewithbehaviorcorrespondingtoaspecificstateofthecontext.Mayhaveanoptionalreferencebacktoitscontext.Context
Managesreferencestodifferentstates,andmakesoperationsdefinedontheactiveone.
PatternscopeStatePatternusuallyappliestothecodeofscopeswiththesizeofafeature.Itdoesnotspecifywhomtotransferthestateofcontext:itcouldbeeitherthecontextitself,thestatemethods,orcodethatcontrolscontext.
ImplementationStartwiththeStateinterface(itcouldalsobeanabstractclassifthereareoperationsorlogictoshare):
interfaceState{
render(hover:boolean):void;
click():void;
}
WiththeStateinterfacedefined,wecanmovetoContextandsketchitsoutline:
classContext{
$element:JQuery;
state:State;
privaterender(hover:boolean):void{
this.state.render(hover);
}
privateclick():void{
this.state.click();
}
onclick():void{
console.log('Iamclicked.');
}
}
Nowwearegoingtohavethetwostates,StateEnabledandStateDisabledimplemented.First,let'saddressStateEnabled,itcaresabouthoverstatusandhandlesclickevent:
classStateEnabledimplementsState{
constructor(
publiccontext:Context
){}
render(hover:boolean):void{
this
.context
.$element
.removeClass('disabled')
.toggleClass('hover',hover);
}
click():void{
this.context.onclick();
}
}
Next,forStateDisableditjustignoreshoverparameteranddoesnothingwhenclickeventemits:
classStateDisabledimplementsState{
constructor(
publiccontext:Context
){}
render():void{
this
.context
.$element
.addClass('disabled')
.removeClass('hover');
}
click():void{
//Donothing.
}
}
Nowwehaveclassesofstatesenabledanddisabledready.Astheinstancesofthoseclassesareassociatedwiththecontext,weneedtoinitializeeverystatewhenanewContextisinitiated:
classContext{
...
privatestateEnabled=newStateEnabled(this);
privatestateDisabled=newStateDisabled(this);
state:State=this.stateEnabled;
...
}
Itispossibletouseflyweightsbypassingcontextinwheninvokingeveryoperationontheactivestateaswell.
Nowlet'sfinishtheContextbylisteningtoandforwardingproperevents:
constructor(){
this
.$element
.hover(
()=>this.render(true),
()=>this.render(false)
)
.click(()=>this.click());
this.render(false);
}
ConsequencesStatePatternreducesconditionalbranchesinpotentiallymultiplemethodsofcontextobjects.Asatrade-off,extrastateobjectsareintroduced,thoughitusuallywon'tbeabigproblem.
ThecontextobjectinStatePatternusuallydelegatesoperationsandforwardsthemtothecurrentstateobject.Thusoperationsdefinedbyaconcretestatemayhaveaccesstothecontextitself.Thismakesreusingstateobjectspossiblewithflyweights.
TemplateMethodPatternWhenwearetalkingaboutsubclassingorinheriting,thebuildingisusuallybuiltfromthebottomup.Subclassesinheritthebasisandthenprovidemore.However,itcouldbeusefultoreversethestructuresometimesaswell.
ConsiderStrategyPatternwhichdefinestheoutlineofaprocessandhasinterchangeablealgorithmsasstrategies.Ifweapplythisstructureunderthehierarchyofclasses,wewillhaveTemplateMethodPattern.
Atemplatemethodisanabstractmethod(optionallywithdefaultimplementation)andactsasaplaceholderundertheoutlineofalargerprocess.Subclassesoverrideorimplementrelatedmethodstomodifyorcompletethebehaviors.ImagingtheskeletonofaTextReader,weareexpectingitssubclassestohandletextfilesfromdifferentstoragemedia,detectdifferentencodingsandreadallthetext.Wemayconsiderastructurelikethis:
TheTextReaderinthisexamplehasamethodreadAllTextthatreadsalltextfromaresourcebytwosteps:readingallbytesfromtheresource(readAllBytes),andthendecodingthosebyteswithcertainencoding(decodeBytes).
Thestructurealsosuggeststhepossibilityofsharingimplementationsamongconcreteclassesthatimplementtemplatemethods.WemaycreateanabstractclassAsciiTextReaderthatextendsTextReaderandimplementsmethoddecodeBytes.AndbuildconcreteclassesFileAsciiTextReaderandHttpAsciiTextReaderthatextendAsciiTextReaderandimplementmethodreadAllBytestohandleresourcesondifferentstoragemedia.
ParticipantsTheparticipantsofTemplateMethodPatternincludethefollowing:
Abstractclass:TextReader
Definesthesignaturesoftemplatemethods,aswellastheoutlineofalgorithmsthatweaveeverythingtogether.Concreteclasses:AsciiTextReader,FileAsciiTextReaderandHttpAsciiTextReader
Implementstemplatemethodsdefinedinabstractclasses.TypicalconcreteclassesareFileAsciiTextReaderandHttpAsciiTextReaderinthisexample.However,comparedtobeingabstract,definingtheoutlineofalgorithmsweighsmoreinthecategorization.
PatternscopeTemplateMethodPatternisusuallyappliedinarelativelysmallscope.Itprovidesanextendablewaytoimplementfeaturesandavoidredundancyfromtheupperstructureofaseriesofalgorithms.
ImplementationTherearetwolevelsoftheinheritinghierarchy:theAsciiTextReaderwillsubclassTextReaderasanotherabstractclass.ItimplementsmethoddecodeBytesbutleavesreadAllBytestoitssubclasses.StartingwiththeTextReader:
abstractclassTextReader{
asyncreadAllText():Promise<string>{
letbytes=awaitthis.readAllBytes();
lettext=this.decodeBytes(bytes);
returntext;
}
abstractasyncreadAllBytes():Promise<Buffer>;
abstractdecodeBytes(bytes:Buffer):string;
}
Tip
WeareusingPromiseswithasyncandawaitwhicharecomingtoECMAScriptnext.Pleaserefertothefollowinglinksformoreinformation:https://github.com/Microsoft/TypeScript/issues/1664https://tc39.github.io/ecmascript-asyncawait/
Andnowlet'ssubclassTextReaderasAsciiTextReaderwhichstillremainsabstract:
abstractclassAsciiTextReaderextendsTextReader{
decodeBytes(bytes:Buffer):string{
returnbytes.toString('ascii');
}
}
ForFileAsciiTextReader,we'llneedtoimportfilesystem(fs)moduleofNode.jstoperformfilereading:
import*asFSfrom'fs';
classFileAsciiTextReaderextendsAsciiTextReader{
constructor(
publicpath:string
){
super();
}
asyncreadAllBytes():Promise<Buffer>{
returnnewPromise<Buffer>((resolve,reject)=>{
FS.readFile(this.path,(error,bytes)=>{
if(error){
reject(error);
}else{
resolve(bytes);
}
});
});
}
}
ForHttpAsciiTextReader,wearegoingtouseapopularpackagerequesttosendHTTPrequests:
import*asrequestfrom'request';
classHttpAsciiTextReaderextendsAsciiTextReader{
constructor(
publicurl:string
){
super();
}
asyncreadAllBytes():Promise<Buffer>{
returnnewPromise<Buffer>((resolve,reject)=>{
request(this.url,{
encoding:null
},(error,bytes,body)=>{
if(error){
reject(error);
}else{
resolve(body);
}
});
});
}
}
Tip
BothconcretereaderimplementationspassresolverfunctionstothePromiseconstructorforconvertingasynchronousNode.jsstylecallbackstoPromises.Formoreinformation,readmoreaboutthePromiseconstructor:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise.
ConsequencesComparedtoStrategyPattern,TemplateMethodPatternprovidesconvenienceforbuildingobjectswiththesameoutlineofalgorithmsoutsideoftheexistingsystem.ThismakesTemplateMethodPatternausefulwaytobuildtoolingclassesinsteadoffixedprocessesbuilt-in.
ButTemplateMethodPatternhaslessruntimeflexibilityasitdoesnothaveamanager.Italsoreliesontheclientwho'susingthoseobjectstodothework.AndastheimplementationofTemplateMethodPatternreliesonsubclassing,itcouldeasilyresultinhierarchiesthathaveasimilarcodeondifferentbranches.Thoughthiscouldbeoptimizedbyusingtechniqueslikemixin.
ObserverPatternObserverPatternisanimportantPatternbackedbyanimportantideainsoftwareengineering.AnditisusuallyakeypartofMVCarchitectureanditsvariantsaswell.
IfyouhaveeverwrittenanapplicationwitharichuserinterfacewithoutaframeworklikeAngularorasolutionwithReact,youmightprobablyhavestruggledwithchangingclassnamesandotherpropertiesofUIelements.Morespecifically,thecodethatcontrolsthosepropertiesofthesamegroupofelementslieseverybranchrelatedtotheelementsinrelatedeventlisteners,justtokeeptheelementsbeingcorrectlyupdated.
Considera"Do"buttonofwhichthedisabledpropertyshouldbedeterminedbythestatusofaWebSocketconnectiontoaserverandwhetherthecurrentlyactiveitemisdone.Everytimethestatusofeithertheconnectionortheactiveitemgetsupdated,we'llneedtoupdatethebuttoncorrespondingly.Themost"handy"waycouldbetwosomewhatidenticalgroupsofcodebeingputintwoeventlisteners.Butinthisway,theamountofsimilarcodewouldjustkeepgrowingasmorerelevantobjectsgetinvolved.
Theprobleminthis"Do"buttonexampleisthat,thebehaviorofcodethat'scontrollingthebuttonisdrivenbyprimitiveevents.Theheavyloadofmanagingtheconnectionsandbehaviorsamongdifferenteventsisdirectlytakenbythedeveloperwho'swritingthatcode.Andunfortunately,thecomplexityinthiscase,growsexponentially,whichmeansitcouldeasilyexceedourbraincapacity.Writingcodethiswaymightresultinmorebugsandmakemaintainingmuchlikelytointroducenewbugs.
Butthebeautifulthingis,wecanfindthefactorsthatmultiplyandoutputthedesiredresult,andthereferencefordividingthosefactorsaregroupsofrelatedstates.Stillspeakingofthe"Do"buttonexample,whatthebuttoncaresaboutis:connectionstatusandtheactiveitemstatus(assumingtheyarebooleansconnectedandloaded).Wecanhavethecodewrittenastwoparts:onepartthatchangesthosestates,andanotherpartthatupdatesthebutton:
letbutton=document.getElementById('do-button');
letconnected=false;
letloaded=false;
functionupdateButton(){
letdisabled=!connected&&!loaded;
button.disabled=disabled;
}
connection.on('statuschange',event=>{
connected=event.connected;
updateButton();
});
activeItem.on('statuschange',event=>{
loaded=event.loaded;
updateButton();
});
TheprecedingsamplecodealreadyhastheembryoofObserverPattern:thesubjects(statesconnectedandloaded)andtheobserver(updateButtonfunction),thoughwestillneedtocallupdateButtonmanuallyeverytimeanyrelatedstatechanges.Animprovedstructurecouldlooklikethefollowingfigure:
Butjustliketheexamplewe'vebeentalkingabout,observersinmanysituationscareaboutmorethanonestate.Itcouldbelesssatisfyingtohavesubjectsattachobserversseparately.
Asolutiontothiscouldbemulti-statesubjects,toachievethat,wecanformacompositesubjectthatcontainssub-subjects.Ifasubjectreceivesanotifycall,itwakesupitsobserversandatthesametimenotifiesitsparent.Thustheobservercanattachonecompositesubjectfornotificationsofchangesthathappentomultiplestates.
However,theprocessofcreatingthecompositeitselfcouldstillbeannoying.Indynamic
programminglanguageslikeJavaScript,wemayhaveastatemanagerthatcontainsspecificstateshandlingnotificationsandattachingobserversdirectlywithimplicitcreationsofsubjects:
letstateManager=newStateManager({
connected:false,
loaded:false,
foo:'abc',
bar:123
});
stateManager.on(['connected','loaded'],()=>{
letdisabled=
!stateManager.connected&&!stateManager.loaded;
button.disabled=disabled;
});
Note
InmanyMV*frameworks,thestatestobeobservedareanalyzedautomaticallyfromrelatedexpressionsbybuilt-inparsersorsimilarmechanisms.
Andnowthestructuregetsevensimpler:
ParticipantsWe'vetalkedaboutthebasicstructureofObserverPatternwithsubjectsandobservers,andavariantwithimplicitsubjects.Theparticipantsofthebasicstructureincludethefollowing:
Subject
Subjecttobeobserved.Definesmethodstoattachornotifyobservers.Asubjectcouldalsobeacompositethatcontainssub-subjects,whichallowsmultiplestatestobeobservedwiththesameinterface.Concretesubject:ConnectedSubjectandLoadedSubject
Containsstaterelatedtothesubject,andimplementsmethodsorpropertiestogetandsettheirstate.Observer
Definestheinterfaceofanobjectthatreactswhenanobservationnotifies.InJavaScript,itcouldalsobeaninterface(orsignature)ofafunction.Concreteobserver:DoButtonObserver
Definestheactionthatreactstothenotificationsofsubjectsbeingobserved.Couldbeacallbackfunctionthatmatchesthesignaturedefined.
Inthevariantversion,theparticipantsincludethefollowing:
Statemanager
Managesacomplex,possiblymulti-levelstateobjectcontainingmultiplestates.Definestheinterfacetoattachobserverswithsubjects,andnotifiesthoseobserverswhenasubjectchanges.Concretesubject
Keystospecificstates.Forexample,string"connected"mayrepresentstatestateManager.connected,whilestring"foo.bar"mayrepresentstatestateManager.foo.bar.
Observerandconcreteobserverarebasicallythesameasdescribedintheformerstructure.Butobserversarenownotifiedbythestatemanagerinsteadofsubjectobjects.
PatternscopeObserverPatternisapatternthatmayeasilystructurehalfoftheproject.InMV*architectures,ObserverPatterncandecoupletheviewfrombusinesslogic.Theconceptofviewcanbeappliedtootherscenariosrelatedtodisplayinginformationaswell.
ImplementationBothofthestructureswe'vementionedshouldnotbehardtoimplement,thoughmoredetailsshouldbeputintoconsiderationforproductioncode.We'llgowiththesecondimplementationthathasacentralstatemanager.
Note
Tosimplifytheimplementation,wewillusegetandsetmethodstoaccessspecificstatesbytheirkeys.Butmanyframeworksavailablemighthandlethosethroughgettersandsetters,orothermechanisms.
Note
TolearnabouthowframeworkslikeAngularhandlestateschanging,pleasereadtheirdocumentationorsourcecodeifnecessary.
WearegoingtohaveStateManagerinheritEventEmitter,sowedon'tneedtocaremuchaboutissueslikemultiplelisteners.Butasweareacceptingmultiplestatekeysassubjects,anoverloadtomethodonwillbeadded.ThustheoutlineofStateManagerwouldbeasfollows:
typeObserver=()=>void;
classStateManagerextendsEventEmitter{
constructor(
privatestate:any
){
super();
}
set(key:string,value:any):void{}
get(key:string):any{}
on(state:string,listener:Observer):this;
on(states:string[],listener:Observer):this;
on(states:string|string[],listener:Observer):this{}
}
Tip
Youmighthavenoticedthatmethodonhasthereturntypethis,whichmaykeepreferringtothetypeofcurrentinstance.Typethisisveryhelpfulforchainingmethods.
Thekeyswillbe"foo"and"foo.bar",weneedtosplitakeyasseparateidentifiersforaccessingthevaluefromthestateobject.Let'shaveaprivate_getmethodthattakesanarrayofidentifiersasinput:
private_get(identifiers:string[]):any{
letnode=this.state;
for(letidentifierofidentifiers){
node=node[identifier];
}
returnnode;
}
Nowwecanimplementmethodgetupon_get:
get(key:string):any{
letidentifiers=key.split('.');
returnthis._get(identifiers);
}
Formethodset,wecangettheparentobjectofthelastidentifierofpropertytobeset,sothingsworklikethis:
set(key:string,value:any):void{
letidentifiers=key.split('.');
letlastIndex=identifiers.length-1;
letnode=this._get(identifiers.slice(0,lastIndex));
node[identifiers[lastIndex]]=value;
}
Butthere'sonemorething,weneedtonotifyobserversthatareobservingacertainsubject:
set(key:string,value:any):void{
letidentifiers=key.split('.');
letlastIndex=identifiers.length-1;
letnode=this._get(identifiers.slice(0,lastIndex));
node[identifiers[lastIndex]]=value;
for(leti=identifiers.length;i>0;i--){
letkey=identifiers.slice(0,i).join('.');
this.emit(key);
}
}
Whenwe'redonewiththenotifyingpart,let'saddanoverloadformethodontosupportmultiplekeys:
on(state:string,listener:Observer):this;
on(states:string[],listener:Observer):this;
on(states:string|string[],listener:Observer):this{
if(typeofstates==='string'){
super.on(states,listener);
}else{
for(letstateofstates){
super.on(state,listener);
}
}
returnthis;
}
Problemsolved.Nowwehaveastatemanagerthatwillworkforsimplescenarios.
ConsequencesObserverPatterndecouplessubjectswithobservers.Whileanobservermaybeobservingmultiplestatesinsubjectsatthesametime,itusuallydoesnotcareaboutwhichstatetriggersthenotification.Asaresult,theobservermaymakeunnecessaryupdatesthatactuallydonothingto-forexample-theview.
However,theimpactonperformancecouldbenegligiblemostofthetime,notevenneedtomentionthebenefitsitbrings.
Bysplittingviewandlogicapart,ObserverPatternmayreducepossiblebranchessignificantly.Thiswillhelpeliminatebugscausedatthecouplingpartbetweenviewandlogic.Thus,byproperlyapplyingObserverPattern,theprojectwillbemademuchmorerobustandeasiertomaintain.
However,therearesomedetailswestillneedcareabout:
1. Theobserverthatupdatesthestatecouldcausecircularinvocation.2. Formorecomplexdatastructureslikecollections,itmightbeexpensivetore-render
everything.Observersinthisscenariomayneedmoreinformationaboutthechangetoonlyperformnecessaryupdates.ViewimplementationslikeReactdothisinanotherway;theyintroduceaconceptcalledVirtualDOM.ByupdatinganddiffingthevirtualDOMbeforere-renderingtheactualDOM(whichcouldusuallybethebottleneckofperformance),itprovidesarelativelygeneralsolutionfordifferentdatastructures.
VisitorPatternVisitorPatternprovidesauniformedinterfaceforvisitingdifferentdataorobjectswhileallowingdetailedoperationsinconcretevisitorstovary.VisitorPatternisusuallyusedwithcomposites,anditiswidelyusedforwalkingthroughdatastructureslikeabstractsyntaxtree(AST).Buttomakeiteasierforthosewhoarenotfamiliarwithcompilerstuff,wewillprovideasimplerexample.
ConsideraDOM-liketreecontainingmultipleelementstorender:
[
Text{
content:"Hello,"
},
BoldText{
content:"TypeScript"
},
Text{
content:"!Populareditors:\n"
},
UnorderedList{
items:[
ListItem{
content:"VisualStudioCode"
},
ListItem{
content:"VisualStudio"
},
ListItem{
content:"WebStorm"
}
]
}
]
TherenderingresultinHTMLwouldlooklikethis:
WhileinMarkdown,itwouldlooklikethis:
VisitorPatternallowsoperationsinthesamecategorytobecodedinthesameplace.We'llhaveconcretevisitors,HTMLVisitorandMarkdownVisitorthattaketheresponsibilitiesoftransformingdifferentnodesbyvisitingthemrespectivelyandrecursively.Thenodesbeingvisitedhaveamethodacceptforacceptingavisitortoperformthetransformation.AnoverallstructureofVisitorPatterncouldbesplitintotwoparts,thefirstpartisthevisitorabstractionanditsconcretesubclasses:
Thesecondpartistheabstractionandconcretesubclassesofnodestobevisited:
ParticipantsTheparticipantsofVisitorPatternincludethefollowing:
Visitor:NodeVisitor
Definestheinterfaceofoperationscorrespondingtoeachelementclass.Inlanguageswithstatictypesandmethodoverloading,themethodnamescanbeunified.ButasittakesextraruntimecheckinginJavaScript,we'llusedifferentmethodnamestodistinguishthem.Theoperationmethodsareusuallynamedaftervisit,buthereweuseappendasitsmorerelatedtothecontext.Concretevisitor:HTMLVisitorandMarkdownVisitor
Implementseveryoperationoftheconcretevisitor,andhandlesinternalstatesifany.Element:Node
Definestheinterfaceoftheelementacceptingthevisitorinstance.Themethodisusuallynamedaccept,thoughhereweareusingappendToforabettermatchingwiththecontext.Elementscouldthemselvesbecompositesandpassvisitorsonwiththeirchildelements.Concreteelement:Text,BoldText,UnorderedListandListItem
Implementsacceptmethodandcallsthemethodfromthevisitorinstancecorrespondingtotheelementinstanceitself.Client:
Enumerateselementsandappliesvisitorstothem.
PatternscopeVisitorPatterncanformalargefeatureinsideasystem.Forsomeprogramsundercertaincategories,itmayalsoformthecorearchitecture.Forexample,BabelusesVisitorPatternforASTtransformingandapluginforBabelisactuallyavisitorthatcanvisitandtransformelementsitcaresabout.
ImplementationWearegoingtoimplementHTMLVisitorandMarkdownVisitorwhichmaytransformnodestotext,aswe'vetalkedabout.Startwiththeupperabstraction:
interfaceNode{
appendTo(visitor:NodeVisitor):void;
}
interfaceNodeVisitor{
appendText(text:Text):void;
appendBold(text:BoldText):void;
appendUnorderedList(list:UnorderedList):void;
appendListItem(item:ListItem):void;
}
Continuewithconcretenodesthatdosimilarthings,TextandBoldText:
classTextimplementsNode{
constructor(
publiccontent:string
){}
appendTo(visitor:NodeVisitor):void{
visitor.appendText(this);
}
}
classBoldTextimplementsNode{
constructor(
publiccontent:string
){}
appendTo(visitor:NodeVisitor):void{
visitor.appendBold(this);
}
}
Andliststuff:
classUnorderedListimplementsNode{
constructor(
publicitems:ListItem[]
){}
appendTo(visitor:NodeVisitor):void{
visitor.appendUnorderedList(this);
}
}
classListItemimplementsNode{
constructor(
publiccontent:string
){}
appendTo(visitor:NodeVisitor):void{
visitor.appendListItem(this);
}
}
Nowwehavetheelementsofastructuretobevisited,we'llbegintoimplementconcretevisitors.Thosevisitorswillhaveanoutputpropertyforthetransformedstring.HTMLVisitorgoesfirst:
classHTMLVisitorimplementsNodeVisitor{
output='';
appendText(text:Text){
this.output+=text.content;
}
appendBold(text:BoldText){
this.output+=`<b>${text.content}</b>`;
}
appendUnorderedList(list:UnorderedList){
this.output+='<ul>';
for(letitemoflist.items){
item.appendTo(this);
}
this.output+='</ul>';
}
appendListItem(item:ListItem){
this.output+=`<li>${item.content}</li>`;
}
}
PayattentiontotheloopinsideappendUnorderedList,ithandlesvisitingofitsownlistitems.
AsimilarstructureappliestoMarkdownVisitor:
classMarkdownVisitorimplementsNodeVisitor{
output='';
appendText(text:Text){
this.output+=text.content;
}
appendBold(text:BoldText){
this.output+=`**${text.content}**`;
}
appendUnorderedList(list:UnorderedList){
this.output+='\n';
for(letitemoflist.items){
item.appendTo(this);
}
}
appendListItem(item:ListItem){
this.output+=`-${item.content}\n`;
}
}
Nowtheinfrastructuresareready,let'screatethetree-likestructurewe'vebeenimaginingsincethebeginning:
letnodes=[
newText('Hello,'),
newBoldText('TypeScript'),
newText('!Populareditors:\n'),
newUnorderedList([
newListItem('VisualStudioCode'),
newListItem('VisualStudio'),
newListItem('WebStorm')
])
];
Andfinally,buildtheoutputswithvisitors:
lethtmlVisitor=newHTMLVisitor();
letmarkdownVisitor=newMarkdownVisitor();
for(letnodeofnodes){
node.appendTo(htmlVisitor);
node.appendTo(markdownVisitor);
}
console.log(htmlVisitor.output);
console.log(markdownVisitor.output);
ConsequencesBothStrategyPatternandVisitorPatterncouldbeappliedtoscenariosofprocessingobjects.ButStrategyPatternreliesonclientstohandleallrelatedargumentsandcontexts,thismakesithardtocomeoutwithanexquisiteabstractioniftheexpectedbehaviorsofdifferentobjectsdifferalot.VisitorPatternsolvesthisproblembydecouplingvisitactionsandoperationstobeperformed.
Bypassingdifferentvisitors,VisitorPatterncanapplydifferentoperationstoobjectswithoutchangingothercodealthoughitusuallymeansaddingnewelementsandwouldresultinaddingrelatedoperationstoanabstractvisitorandallofitsconcretesubclasses.
VisitorsliketheNodeVisitorinthepreviousexamplemaystorestateitself(inthatexample,westoredtheoutputoftransformednodes)andmoreadvancedoperationscanbeappliedbasedonthestateaccumulated.Forexample,it'spossibletodeterminewhathasbeenappendedtotheoutput,andthuswecanapplydifferentbehaviorswiththenodecurrentlybeingvisited.
However,tocompletecertainoperations,extrapublicmethodsmayneedtobeexposedfromtheelements.
SummaryInthischapter,we'vetalkedaboutotherbehaviordesignpatternsascomplementstotheformerchapter,includingStrategy,State,TemplateMethod,ObserverandVisitorPattern.
StrategyPatternissocommonandusefulthatitmayappearinaprojectseveraltimes,withdifferentforms.AndyoumightnotknowyouwereusingObserverPatternwithimplementationinadailyframework.
Afterwalkingthroughthosepatterns,youmightfindtherearemanyideasincommonbehindeachpattern.Itisworththinkingwhat'sbehindthemandevenlettingtheoutlinegoinyourmind.
Inthenextchapter,we'llcontinuewithsomehandypatternsrelatedtoJavaScriptandTypeScript,andimportantscenariosofthoselanguages.
Chapter7.PatternsandArchitecturesinJavaScriptandTypeScriptInthepreviousfourchapters,we'vewalkedthroughcommonandclassicaldesignpatternsanddiscussedsomeoftheirvariantsinJavaScriptorTypeScript.Inthischapter,we'llcontinuewithsomearchitectureandpatternscloselyrelatedtothelanguageandtheircommonapplications.Wedon'thavemanypagestoexpandandcertainlycannotcovereverythinginasinglechapter,sopleasetakeitasanappetizerandfeelfreetoexploremore.
Manytopicsinthischapterarerelatedtoasynchronousprogramming.We'llstartwithawebarchitectureforNode.jsthat'sbasedonPromise.Thisisalargertopicthathasinterestingideasinvolved,includingabstractionsofresponsesandpermissions,aswellaserrorhandlingtips.Thenwe'lltalkabouthowtoorganizemoduleswithECMAScript(ES)modulesyntax.Andthischapterwillendwithseveralusefulasynchronoustechniques.
Overall,we'llhavethefollowingtopicscoveredinthischapter:
ArchitectureandtechniquesrelatedtoPromiseAbstractionofresponsesandpermissionsinawebapplicationModularizingaprojecttoscaleOtherusefulasynchronoustechniques
Note
Again,duetothelimitedlength,someoftherelatedcodeisaggressivelysimplifiedandnothingmorethantheideaitselfcanbeappliedpractically.
Promise-basedwebarchitectureTohaveabetterunderstandingofthedifferencesbetweenPromisesandtraditionalcallbacks,consideranasynchronoustasklikethis:
functionprocess(callback){
stepOne((error,resultOne)=>{
if(error){
callback(error);
return;
}
stepTwo(resultOne,(error,resultTwo)=>{
if(error){
callback(error);
return;
}
callback(undefined,resultTwo+1);
});
});
}
IfwewriteprecedingaboveinPromisestyle,itwouldbeasfollows:
functionprocess(){
returnstepOne()
.then(result=>stepTwo(result))
.then(result=>result+1);
}
Asintheprecedingexample,Promisemakesiteasyandnaturaltowriteasynchronousoperationswithaflatchaininsteadofnestedcallbacks.ButthemostexcitingthingaboutPromisemightbethebenefitsitbringstoerrorhandling.InaPromise-basedarchitecture,throwinganerrorcanbesafeandpleasant.Youdon'thavetoexplicitlyhandleerrorswhenchainingasynchronousoperations,andthismakesmistakeslesslikelytohappen.
WiththegrowingusagewithES6compatibleruntimes,Promiseisalreadythereoutofthebox.AndweactuallyhaveplentyofpolyfillsforPromises(includingmyThenFailwritteninTypeScript),aspeoplewhowriteJavaScriptroughlyrefertothesamegroupofpeoplewhocreatedwheels.
PromisesworkwellwithotherPromises:
APromises/A+-compatibleimplementationshouldworkwithotherPromises/A+-compatibleimplementationsPromisesworkbestinaPromise-basedarchitecture
IfyouarenewtoPromise,youmightbecomplainingaboutusingPromiseswithacallback-basedproject.UsingasynchronoushelperssuchasPromise.each(non-standard)providedbyPromiselibrariesisacommonreasonforpeopletotryoutPromise,butitturnsouttheyhavebetteralternatives(foracallback-basedproject)suchasthepopularasynclibrary.
Thereasonthatmakesyoudecidetoswitchshouldnotbethesehelpers(astherearealotofthemforold-schoolcallbacksaswell),butaneasierwaytohandleerrorsortotakeadvantageoftheESasync/awaitfeature,whichisbasedonPromise.
PromisifyingexistingmodulesorlibrariesThoughPromisesdobestinaPromise-basedarchitecture,itisstillpossibletobeginusingPromisewithasmallerscopebypromisifyingexistingmodulesorlibraries.
Let'stakeNode.jsstylecallbacksasanexample:
import*asFSfrom'fs';
FS.readFile('some-file.txt','utf-8',(error,text)=>{
if(error){
console.error(error);
return;
}
console.log('Content:',text);
});
YoumayexpectapromisifiedversionofthereadFilefunctiontolooklikethefollowing:
FS
.readFile('some-file.txt','utf-8')
.then(text=>{
console.log('Content:',text);
})
.catch(reason=>{
Console.error(reason);
});
TheimplementationofthepromisifiedfunctionreadFilecanbeeasy:
functionreadFile(path:string,options:any):Promise<string>{
returnnewPromise((resolve,reject)=>{
FS.readFile(path,options,(error,result)=>{
if(error){
reject(error);
}else{
resolve(result);
}
});
});
}
Note
Iamusingthetypeanyhereforparameteroptionstoreducethesizeofthecodeexample,butIwouldsuggestnotusinganywheneverpossibleinpractice.
Therearelibrariesthatareabletopromisifymethodsautomatically.Though,unfortunately,youmightneedtowritedeclarationfilesyourselfforthepromisifiedmethodsiftherearenopromisifiedversionavailable.
ViewsandcontrollersinExpressManyofusmayhavealreadyworkedwithframeworkssuchasExpress.AndthisishowwerenderavieworresponsewithJSONinExpress:
import*asPathfrom'path';
import*asexpressfrom'express';
letapp=express();
app.set('engine','hbs');
app.set('views',Path.join(__dirname,'../views'));
app.get('/page',(req,res)=>{
res.render('page',{
title:'Hello,Express!',
content:'...'
});
});
app.get('/data',(req,res)=>{
res.json({
version:'0.0.0',
items:[]
});
});
app.listen(1337);
Wewillusuallyseparatecontrollersfromtheroutingconfiguration:
import{Request,Response}from'express';
exportfunctionpage(req:Request,res:Response):void{
res.render('page',{
title:'Hello,Express!',
content:'...'
});
}
Thuswemayhaveabetterideaofexistingroutes,andhavecontrollersmanagedmoreeasily.Furthermore,automatedroutingcouldbeintroducedsothatwedon'talwaysneedtoupdateroutingmanually:
import*asglobfrom'glob';
letcontrollersDir=Path.join(__dirname,'controllers');
letcontrollerPaths=glob.sync('**/*.js',{
cwd:controllersDir
});
for(letpathofcontrollerPaths){
letcontroller=require(Path.join(controllersDir,path));
leturlPath=path.replace(/\\/g,'/').replace(/\.js$/,'');
for(letactionNameofObject.keys(controller)){
app.get(
`/${urlPath}/${actionName}`,
controller[actionName]
);
}
}
Theimplementationaboveiscertainlytoosimpletocoverdailyuse,butitshowsaroughideaofhowautomatedroutingcouldwork:viaconventionsbasedonfilestructures.
Now,ifweareworkingwithasynchronouscodewritteninPromises,anactioninthecontrollercouldbelikethefollowing:
exportfunctionfoo(req:Request,res:Response):void{
Promise
.all([
Post.getContent(),
Post.getComments()
])
.then(([post,comments])=>{
res.render('foo',{
post,
comments
});
});
}
Note
Wearedestructuringanarraywithinaparameter.Promise.allreturnsaPromiseofanarraywithelementscorrespondingtothevaluesoftheresolvablespassedin.(AresolvablemeansanormalvalueoraPromise-likeobjectthatmayresolvetoanormalvalue.)
Butthat'snotenough;westillneedtohandleerrorsproperly,orinsomePromiseimplementations,theprecedingcodemayfailinsilencebecausethePromisechainisnothandledbyarejectionhandler(whichisterrible).InExpress,whenanerroroccurs,youshouldcallnext(thethirdargumentpassedintothecallback)withtheerrorobject:
import{Request,Response,NextFunction}from'express';
exportfunctionfoo(
req:Request,
res:Response,
next:NextFunction
):void{
Promise
//...
.catch(reason=>next(reason));
}
Now,wearefinewiththecorrectnessofthisapproach,butthat'ssimplynothowPromiseswork.Expliciterrorhandlingwithcallbackscouldbeeliminatedinthescopeofcontrollers,andtheeasiestwayistoreturnthePromisechainandhandovertocodethatwaspreviouslydoingroutinglogic.Sothecontrollercouldbewrittenlikethis:
exportfunctionfoo(req:Request,res:Response){
returnPromise
.all([
Post.getContent(),
Post.getComments()
])
.then(([post,comments])=>{
res.render('foo',{
post,
comments
});
});
}
But,couldwemakeitevenbetter?
AbstractionofresponsesWe'vealreadybeenreturningaPromisetotellwhetheranerroroccurs.SonowthereturnedPromiseindicatesthestatusoftheresponse:successorfailure.Butwhywearestillcallingres.render()forrenderingtheview?Thereturnedpromiseobjectcouldbetheresponseitselfratherthanjustanerrorindicator.
Thinkaboutthecontrolleragain:
exportclassResponse{}
exportclassPageResponseextendsResponse{
constructor(view:string,data:any){}
}
exportfunctionfoo(req:Request){
returnPromise
.all([
Post.getContent(),
Post.getComments()
])
.then(([post,comments])=>{
returnnewPageResponse('foo',{
post,
comments
});
});
}
Theresponseobjectreturnedcouldvaryfordifferentresponseoutputs.Forexample,itcouldbeeitheraPageResponselikeitisintheprecedingexample,aJSONResponse,aStreamResponse,orevenasimpleRedirection.
As,inmostcases,PageResponseorJSONResponseisapplied,andtheviewofaPageResponsecanusuallybeimpliedbythecontrollerpathandactionname,itisusefultohavethosetworesponsesautomaticallygeneratedfromaplaindataobjectwithaproperviewtorenderwith:
exportfunctionfoo(req:Request){
returnPromise
.all([
Post.getContent(),
Post.getComments()
])
.then(([post,comments])=>{
return{
post,
comments
};
});
}
Andthat'showaPromise-basedcontrollershouldrespond.Withthisidea,let'supdatetheroutingcodewiththeabstractionofresponses.Previously,wewerepassingcontrolleractionsdirectlyasExpressrequesthandlers.Nowweneedtodosomewrappingupwiththeactionsbyresolvingthereturnvalue,andapplyingoperationsbasedontheresolvedresult:
1. Ifitfulfilsandit'saninstanceofResponse,applyittotheresobjectpassedinbyExpress.2. Ifitfulfilsandit'saplainobject,constructaPageResponseoraJSONResponseifnoview
foundandapplyittotheresobject.3. Ifitrejects,callthenextfunctionwiththereason.
Previously,itwaslikethis:
app.get(`/${urlPath}/${actionName}`,controller[actionName]);
Nowitgetsafewmorelines:
letaction=controller[actionName];
app.get(`/${urlPath}/${actionName}`,(req,res,next)=>{
Promise
.resolve(action(req))
.then(result=>{
if(resultinstanceofResponse){
result.applyTo(res);
}elseif(existsView(actionName)){
newPageResponse(actionName,result).applyTo(res);
}else{
newJSONResponse(result).applyTo(res);
}
})
.catch(reason=>next(reason));
});
However,sofarwecanhandleonlyGETrequestsaswehardcodedapp.get()inourrouterimplementation.Thepoorview-matchinglogiccanhardlybeusedinpracticeeither.Weneedtomaketheactionsconfigurable,andESdecoratorscoulddoniceworkhere:
exportdefaultclassController{
@get({
view:'custom-view-path'
})
foo(req:Request){
return{
title:'Actionfoo',
content:'Contentofactionfoo'
};
}
}
I'llleavetheimplementationtoyou,andfeelfreetomakeitawesome.
AbstractionofpermissionsPermissionsplayanimportantroleinaproject,especiallyinsystemsthathavedifferentusergroups,forexample,aforum.Theabstractionofpermissionsshouldbeextendabletosatisfychangingrequirements,anditshouldbeeasytouseaswell.
Here,wearegoingtotalkabouttheabstractionofpermissioninthelevelofcontrolleractions.Considerthelegibilityofperformingoneormoreactionsasaprivilege.Thepermissionofausermayconsistofseveralprivilegesandusuallymostusersatthesamelevelwouldhavethesamesetofprivileges.Sowemayhavealargerconcept,namelygroups.
Theabstractioncouldeitherworkbasedonbothgroupsandprivilegesorbasedonprivilegesonly(groupsarethenjustaliasestosetsofprivileges):
Abstractionsthatvalidatebasedonprivilegesandgroupsatthesametimeiseasiertobuild.Youdonotneedtocreatealargelistofwhichactionscanbeperformedforacertaingroupofusers;granularprivilegesareonlyrequiredwhennecessary.Abstractionsthatvalidatebasedonprivilegeshavebettercontrolandmoreflexibilityfordescribingthepermission.Forexample,youcanremoveasmallsetofprivilegesfromthepermissionofausereasily.
However,bothapproacheshavesimilarupper-levelabstractionsanddiffermostlyinimplementation.Thegeneralstructureofthepermissionabstractionswe'vetalkedaboutisasfollows:
Theparticipantsincludethefollowing:
Privilege:Describesdetailedprivilegescorrespondingtospecificactions
Group:DefinesasetofprivilegesPermission:Describeswhatauseriscapableofdoing;consistsofgroupstheuserbelongstoandprivilegestheuserhasPermissiondescriptor:Describeshowthepermissionofauserwouldbesufficient;consistsofpossiblegroupsandprivileges
ExpectederrorsAgreatconcernwipedawaybyusingPromisesisthatwedonotneedtoworryaboutthrowinganerrorinacallbackwouldcrashtheapplicationmostofthetime.TheerrorwillflowthroughthePromiseschainand,ifnotcaught,willbehandledbyourrouter.Errorscanberoughlydividedintoexpectederrorsandunexpectederrors.Expectederrorsareusuallycausedbyincorrectinputorforeseeableexceptions,andunexpectederrorsareusuallycausedbybugsorotherlibrariestheprojectrelieson.
Forexpectederrors,weusuallywanttogiveuser-friendlyresponseswithreadableerrormessagesandcodes,sothatuserscanhelpthemselvestofindsolutionsorreporttouswithusefulcontext.Forunexpectederrors,wewouldalsowantreasonableresponses(usuallymessagesdescribedasunknownerrors),adetailedserver-sidelog(includingtherealerrorname,message,stackinformation,andsoon),andevenalarmsforgettingtheteamnotifiedassoonaspossible.
Definingandthrowingexpectederrors
Therouterwillneedtohandledifferenttypesoferrors,andaneasywaytoachievethatistosubclassauniversalExpectedErrorclassandthrowitsinstancesout:
importExtendableErrorfrom'extendable-error';
classExpectedErrorextendsExtendableError{
constructor(
message:string,
publiccode:number
){
super(message);
}
}
Note
Theextendable-errorisapackageofminethathandlesstacktraceandthemessageproperty.YoucandirectlyextendtheErrorclassaswell.
Thus,whenreceivinganexpectederror,wecansafelyoutputitsmessageaspartoftheresponse.Andifit'snotaninstanceofExpectedError,wecanthenoutputpredefinedunknownerrormessagesandhavedetailederrorinformationlogged.
Transformingerrors
Someerrors,suchasthosecausedbyunstablenetworksorremoteservices,areexpected;wemaywanttocatchthoseerrorsandthrowthemoutagainasexpectederrors.Butitisrathertrivialtoactuallydothat.Acentralizederror-transformingprocesscanthenbeappliedtoreducetheeffortsrequiredtomanagethoseerrors.
Thetransformingprocessincludestwoparts:filtering(ormatching)andtransforming.Therearemanyapproachestofiltererrors,suchasthefollowing:
Filterbyerrorclass:Manythird-partylibrariesthrowerrorsofcertainclasses.TakingSequelize(apopularNode.jsORM)asanexample,itthrowsDatabaseError,ConnectionError,ValidationError,andsoon.Byfilteringerrorsbycheckingwhethertheyareinstancesofacertainerrorclass,wemayeasilypickuptargeterrorsfromthepile.Filterbystringorregularexpression:SometimesalibrarymightbethrowingerrorsthatareinstancesofanErrorclassitselfinsteadofitssubclasses;thismakesthoseerrorshardertodistinguishfromothers.Inthissituation,wemayfilterthoseerrorsbytheirmessage,withkeywordsorregularexpressions.Filterbyscope:It'spossiblethatinstancesofthesameerrorclasswiththesameerrormessageshouldresultindifferentresponses.Oneofthereasonsmightbethattheoperationthatthrowsacertainerrorisatalowerlevel,butisbeingusedbyupperstructureswithindifferentscopes.Thus,ascopemarkcouldbeaddedforthoseerrorsandmakethemeasiertobefiltered.
Therecouldbemorewaystofiltererrors,andtheyareusuallyabletocooperateaswell.Byproperlyapplyingthosefiltersandtransformingerrors,wecanreducenoiseforanalyzingwhat'sgoingonwithinasystemandlocateproblemsfasteriftheyshowup.
ModularizingprojectBeforeES6,therewerealotofmodulesolutionsforJavaScriptthatworked.ThetwomostfamousofthemareAMDandcommonjs.AMDisdesignedforasynchronousmoduleloading,whichismostlyappliedinbrowsers,whilecommonjsdoesmoduleloadingsynchronously,andthat'sthewaytheNode.jsmodulesystemworks.
Tomakeitworkasynchronously,writinganAMDmoduletakesmorecharacters.Andduetothepopularityoftoolssuchasbrowserifyandwebpack,commonjsbecomespopularevenforbrowserprojects.
Thepropergranularityofinternalmodulescouldhelpaprojectkeepitsstructurehealthy.Consideraprojectstructurelikethis:
project
├─controllers
├─core
││index.ts
││
│├─product
││index.ts
││order.ts
││shipping.ts
││
│└─user
│index.ts
│account.ts
│statistics.ts
│
├─helpers
├─models
├─utils
└─views
Assumewearewritingacontrollerfilethat'sgoingtoimportamoduledefinedbythecore/product/order.tsfile.Previously,withthecommonjsrequirestyle,wewouldwanttowritethefollowing:
constOrder=require('../core/product/order');
Now,withthenewESimportsyntax,itwouldbeasfollows:
import*asOrderfrom'../core/product/order';
Wait,isn'tthatessentiallythesame?Sortof.Butyoumayhavenoticedseveralindex.tsfilesI'veputintofolders.Now,inthefilecore/product/index.ts,wecanhavethefollowing:
import*asOrderfrom'./order';
import*asShippingfrom'./shipping';
export{Order,Shipping}
Alternatively,wecouldhavethefollowing:
export*from'./order';
export*from'./shipping';
What'sthedifference?Theideasbehindthosetwoapproachesofre-exportingmodulescanvary.ThefirststyleworksbetterwhenwetreatOrderandShippingasnamespaces,underwhichtheentitynamesmaynotbeeasytodistinguishfromonegrouptoanother.Withthisstyle,thefilesarethenaturalboundariesofbuildingthosenamespaces.Thesecondstyleweakensthenamespacepropertyoftwofilesandusesthemastoolstoorganizeobjectsandclassesunderthesamelargercategory.
Agoodthingaboutusingthosefilesasnamespacesisthatmultiple-levelre-exportingisfinewhileweakeningnamespacesmakesithardertounderstanddifferentidentifiernamesasthenumberofre-exportinglevelsgrows.
AsynchronouspatternsWhenwearewritingJavaScriptwithnetworkorfilesystemI/O,thereisa95%chancethatwearedoingitasynchronously.However,anasynchronouscodemaytremendouslydecreasethedeterminabilityatthedimensionoftime.ButwearesoluckythatJavaScriptisusuallysingle-threaded;thismakesitpossibleforustowritepredictablecodewithoutmechanismssuchaslocksmostofthetime.
WritingpredictablecodeThepredictablecodereliesonpredictabletools(ifyouareusingany).Considerahelperlikethis:
typeCallback=()=>void;
letisReady=false;
letcallbacks:Callback[]=[];
setTimeout(()=>{
callbacks.forEach(callback=>callback());
callbacks=undefined;
},100);
exportfunctionready(callback:Callback):void{
if(!callbacks){
callback();
}else{
callbacks.push(callback);
}
}
Thismoduleexportsareadyfunction,whichwillinvokethecallbackspassedinwhen"ready".Itwillassurethatcallbackswillbecalledevenifaddedafterthat.However,youcannotsayforsurewhetherthecallbackwillbecalledinthecurrenteventloop:
import{ready}from'./foo';
leti=0;
ready(()=>{
console.log(i);
});
i++;
Intheprecedingexample,icouldeitherbe0or1whenthecallbackgetscalled.Again,thisisnotwrong,orevenbad,itjustmakesthecodelesspredictable.Whensomeoneelsereadsthispieceofcode,heorshewillneedtoconsidertwopossibilitiesofhowthisprogramwouldrun.Toavoidthisissue,wecansimplywrapupthesynchronousinvocationwithsetImmediate(itmayfallbacktosetTimeoutinolderbrowsers):
exportfunctionready(callback:Callback):void{
if(!callbacks){
setImmediate(()=>callback());
}else{
callbacks.push(callback);
}
}
Writingpredictablecodeisactuallymorethanwritingpredictableasynchronouscode.ThehighlightedlineabovecanalsobewrittenassetImmediate(callback),butthatwouldmake
peoplewhoreadyourcodethinktwice:howwillcallbackgetcalledandwhatarethearguments?
Considerthelineofcodebelow:
letresults=['1','2','3'].map(parseInt);
What'sthevalueofthearrayresults?Certainlynot[1,2,3].Becausethecallbackpassedtothemethodmapreceivesseveralarguments:valueofcurrentitem,indexofcurrentitem,andthewholearray,whilethefunctionparseIntacceptstwoarguments:stringtoparse,andradix.Soresultsareactuallytheresultsofthefollowingsnippet:
[parseInt('1',0),parseInt('2',1),parseInt('3',2)];
However,itisactuallyokaytowritesetImmediate(callback)directly,astheAPIsofthosefunctions(includingsetTimeout,setInterval,process.nextTick,andsoon)aredesignedtobeusedinthisway.Anditisfairtoassumepeoplewhoaregoingtomaintainthisprojectknowthataswell.Butforotherasynchronousfunctionswhosesignaturesarenotwellknown,itisrecommendedtocallthemwithexplicitarguments.
AsynchronouscreationalpatternsWetalkedaboutmanycreationalpatternsinChapter3,CreationalDesignPatterns.Whileaconstructorcannotbeasynchronous,someofthosepatternsmayhaveproblemsapplyingtoasynchronousscenarios.Butothersneedonlyslightmodificationsforasynchronoususe.
InChapter4,StructuralDesignPatternswewalkedthroughtheAdapterPatternwithastorageexamplethatopensthedatabaseandcreatesastorageobjectasynchronously:
classStorage{
privateconstructor(){}
open():Promise<Storage>{
returnopenDatabase()
.then(db=>newStorage(db))
}
}
AndintheProxyPattern,wemadethestorageobjectimmediatelyavailablefromitsconstructor.Whenamethodoftheobjectiscalled,itwaitsfortheinitializationtocompleteandfinishestheoperation:
classStorage{
privatedbPromise:Promise<IDBDatabase>;
getdbReady():Promise<IDBDatabase>{
if(this.dbPromise){
returnthis.dbPromise;
}
//...}
get<T>():Promise<T>{
returnthis
.dbReady
.then(db=>{
//...
});
}
}
Adrawbackofthisapproachisthatallmembersthatrelyoninitializationhavetobeasynchronous,thoughmostofthetimetheyjustareasynchronous.
AsynchronousmiddlewareandhooksTheconceptofmiddlewareiswidelyusedinframeworkssuchasExpress.Middlewareusuallyprocessesitstargetinserial.InExpress,middlewareisappliedroughlyintheorderitisaddedwhiletherearenotdifferentphases.Someotherframeworks,however,providehooksfordifferentphasesintime.Forexample,therearehooksthatwillbetriggeredbeforeinstall,afterinstall,afteruninstall,andsoon.
Note
ThemiddlewaremechanismofExpressisactuallyavariantoftheChainofResponsibilityPattern.Anddependingonthespecificmiddlewaretobeused,itcanactmoreorlesslikehooksinsteadofaresponsibilitychain.
Thereasonstoimplementmiddlewareorhooksvary.Theymayincludethefollowing:
Extensibility:Mostofthetime,theyareappliedduetotherequirementofextensibility.Newrulesandprocessescouldbeeasilyaddedbynewmiddlewareorhooks.Decouplinginteractionswithbusinesslogic:Amodulethatshouldonlycareaboutbusinesslogiccouldneedpotentialinteractionswithaninterface.Forexample,wemightexpecttobeabletoeitherenterorupdatecredentialswhileprocessinganoperation,withoutrestartingeverything.Thuswecancreateamiddlewareorahook,sothatwedon'tneedtohavethemtightlycoupled.
Theimplementationofasynchronousmiddlewarecouldbeinteresting.TakethePromiseversionasanexample:
typeMiddleware=(host:Host)=>Promise<void>;
classHost{
middlewares:Middleware[]=[];
start():Promise<void>{
returnthis
.middlewares
.reduce((promise,middleware)=>{
returnpromise.then(()=>middleware(this));
},Promise.resolve());
}
}
Here,we'reusingreducetodothetrick.WepassedinaPromisefulfilledwithundefinedastheinitialvalue,andchaineditwiththeresultofmiddleware(this).AndthisisactuallyhowthePromise.eachhelperisimplementedinmanyPromiselibraries.
Event-basedstreamparserWhencreatinganapplicationreliesonsocket,weusuallyneedalightweight"protocol"fortheclientandservertocommunicate.UnlikeXHRthatalreadyhandleseverything,byusingsocket,youwillneedtodefinetheboundariessodatawon'tbemixedup.
Datatransferredthroughasocketmightbeconcatenatedorsplit,butTCPconnectionensurestheorderandcorrectnessofbytesgetstransferred.Consideratinyprotocolthatconsistsofonlytwoparts:a4-byteunsignedintegerfollowedbyaJSONstringwithbytelengththatmatchesthe4-byteunsignedinteger.
Forexample,forJSON"{}",thedatapacketwouldbeasfollows:
Buffer<000000027b7d>
Tobuildsuchadatapacket,wejustneedtoconverttheJSONstringtoBuffer(withencodingsuchasutf-8,whichisdefaultencodingforNode.js),andthenprependitslength:
functionbuildPacket(data:any):Buffer{
letjson=JSON.stringify(data);
letjsonBuffer=newBuffer(json);
letpacket=newBuffer(4+jsonBuffer.length);
packet.writeUInt32BE(jsonBuffer.length,0);
jsonBuffer.copy(packet,4,0);
returnpacket;
}
Asocketclientemitsadataeventwhenitreceivesnewbuffers.AssumewearegoingtosendthefollowingJSONstrings:
//000000027b7d
{}
//0000000f7b226b6579223a2276616c7565227d
{"key":"value"}
Wemaybereceivingthemlikethis:
Gettwobuffersseparately;eachofthemisacompletepacketwithlengthandJSONbytesGetonesinglebufferwithtwobuffersconcatenatedGettwo,ormorethantwo,buffers;atleastoneofthepreviouslysentpacketsgetssplitintoseveralones.
Theentireprocessishappeningasynchronously.Butjustlikethesocketclientemitsadataevent,theparsercanjustemititsowndataeventwhenacompletepacketgetsparsed.Theparserforparsingourtinyprotocolmayhaveonlytwostates,correspondingtoheader
(JSONbytelength)andbody(JSONbytes),andtheemittingofthedataeventhappensaftersuccessfullyparsingthebody:
classParserextendsEventEmitter{
privatebuffer=newBuffer(0);
privatestate=State.header;
append(buffer:Buffer):void{
this.buffer=Buffer.concat([this.buffer,buffer]);
this.parse();
}
privateparse():void{}
privateparseHeader():boolean{}
privateparseBody():boolean{}
}
Duetothelimitationoflength,I'mnotgoingtoputthecompleteimplementationoftheparserhere.Forthecompletecode,pleaserefertothefilesrc/event-based-parser.tsinthecodebundleofChapter7,PatternsandArchitecturesinJavaScriptandTypeScript.
Thustheuseofsuchaparsercouldbeasfollows:
import*asNetfrom'net';
letparser=newParser();
letclient=Net.connect(port);
client.on('data',(data:Buffer)=>{
parser.append(data);
});
parser.on('data',(data:any)=>{
console.log('Datareceived:',data);
});
SummaryInthischapter,wediscussedsomeinterestingideasandanarchitectureformedbythoseideas.Mostofthetopicsfocusonasmallscopeanddotheirownjob,buttherearealsoideasaboutputtingawholesystemtogether.
Thecodethatimplementstechniquessuchasexpectederrorandtheapproachtomanagingmodulesinaprojectisnothardtoapply.Butwithproperapplication,itcanbringnotableconveniencetotheentireproject.
However,asIhavealreadymentionedatthebeginningofthischapter,therearetoomanybeautifulthingsinJavaScriptandTypeScripttobecoveredorevenmentionedinasinglechapter.Pleasedon'tstophere,andkeepexploring.
Manypatternsandarchitecturesaretheresultofsomefundamentalprinciplesinsoftwareengineering.Thoseprinciplesmightnotalwaysbeapplicableineveryscenario,buttheymayhelpwhenyoufeelconfused.Inthenextchapter,wearegoingtotalkaboutSOLIDprinciplesinobject-orienteddesignandfindouthowthoseprinciplesmayhelpformausefulpattern.
Chapter8.SOLIDPrinciplesSOLIDPrinciplesarewell-knownObject-OrientedDesign(OOD)principlessummarizedbyUncleBob(RobertC.Martin).ThewordSOLIDcomesfromtheinitialsofthefiveprinciplesitrefersto,includingSingleresponsibilityprinciple,Open-closedprinciple,Liskovsubstitutionprinciple,InterfacesegregationprincipleandDependencyinversionprinciple.Thoseprinciplesarecloselyrelatedtoeachother,andcanbeagreatguidanceinpractice.
HereisawidelyusedsummaryofSOLIDprinciplesfromUncleBob:
Singleresponsibilityprinciple:Aclassshouldhaveone,andonlyone,reasontochangeOpen-closedprinciple:Youshouldbeabletoextendaclassesbehavior,withoutmodifyingitLiskovsubstitutionprinciple:DerivedclassesmustbesubstitutablefortheirbaseclassesInterfacesegregationprinciple:Makefine-grainedinterfacesthatareclientspecificDependencyinversionprinciple:Dependonabstractions,notonconcretions
Inthischapter,wewillwalkthroughthemandfindouthowthoseprinciplescanhelpformadesignthatsmellsnice.
Butbeforeweproceed,Iwanttomentionthatafewofthereasonswhythoseprinciplesexistmightberelatedtotheageinwhichtheywereraised,thelanguagesandtheirbuildingordistributingprocesspeoplewereworkingwith,andevencomputingresources.WhenbeingappliedtoJavaScriptandTypeScriptprojectsnowadays,someofthedetailsmaynotbenecessary.Thinkmoreaboutwhatproblemsthoseprincipleswanttopreventpeoplefromgettinginto,ratherthantheliteraldescriptionsofhowaprincipleshouldbefollowed.
SingleresponsibilityprincipleThesingleresponsibilityprincipledeclaresthataclassshouldhaveone,andonlyonereasontochange.Andthedefinitionoftheworldreasoninthissentenceisimportant.
ExampleConsideraCommandclassthatisdesignedtoworkwithbothcommand-lineinterfaceandgraphicaluserinterface:
classCommand{
environment:Environment;
print(items:ListItem[]){
letstdout=this.environment.stdout;
stdout.write('Items:\n');
for(letitemofitems){
stdout.write(item.text+'\n');
}
}
render(items:ListItem[]){
letelement=<Listitems={items}></List>;
this.environment.render(element);
}
execute(){}
}
Tomakethisactuallywork,executemethodwouldneedtohandleboththecommandexecutionandresultdisplaying:
classCommand{
..
execute(){
letitems=...;
if(this.environment.type==='cli'){
this.print(items);
}else{
this.render(items);
}
}
}
Inthisexample,therearetworeasonsforchanges:
1. Howacommandgetsexecuted.2. Howtheresultofacommandgetsdisplayedindifferentenvironments.
Thosereasonsleadtochangesindifferentdimensionsandviolatethesingleresponsibilityprinciple.Thismightresultinamessysituationovertime.AbettersolutionistohavethosetworesponsibilitiesseparatedandmanagedbytheCommandEnvironment:
Doesthislookfamiliartoyou?BecauseitisavariantoftheVisitorPattern.Nowitistheenvironmentthatexecutesaspecificcommandandhandlesitsresultbasedonaconcreteenvironmentclass.
ChoosinganaxisYoumightbethinking,doesn'tCommandResultviolatethesingleresponsibilityprinciplebyhavingtheabilitiestodisplaycontentinadifferentenvironment?Yes,andno.Whentheaxisofthisreasonissettodisplayingcontent,itdoesnot;butiftheaxisissettodisplayinginaspecificenvironment,itdoes.Buttaketheoverallstructureintoconsideration,theresultofacommandisexpectedtobeanoutputthatcanadapttoadifferentenvironment.Andthusthereasonisone-dimensionalandconfirmstheprinciple.
Open-closedprincipleTheopen-closedprincipledeclaresthatyoushouldbeabletoextendaclass'behavior,withoutmodifyingit.ThisprincipleisraisedbyBertrandMeyerin1988:
Softwareentities(classes,modules,functions,etc.)shouldbeopenforextension,butclosedformodification.
Aprogramdependsonalltheentitiesituses,thatmeanschangingthealready-being-usedpartofthoseentitiesmayjustcrashtheentireprogram.Sotheideaoftheopen-closedprincipleisstraightforward:we'dbetterhaveentitiesthatneverchangeinanywayotherthanextendingitself.
Thatmeansonceatestiswrittenandpassing,ideally,itshouldneverbechangedfornewlyaddedfeatures(anditneedstokeeppassing,ofcourse).Again,ideally.
ExampleConsideranAPIhubthathandlesHTTPrequeststoandresponsesfromtheserver.Wearegoingtohaveseveralfileswrittenasmodules,includinghttp-client.ts,hub.tsandapp.ts(butwewon'tactuallywritehttp-client.tsinthisexample,youwillneedtousesomeimagination).
Savethecodebelowasfilehub.ts.
import{HttpClient,HttpResponse}from'./http-client';
exportfunctionupdate():Promise<HttpResponse>{
letclient=newHttpClient();
returnclient.get('/api/update');
}
Andsavethecodebelowasfileapp.ts.
importHubfrom'./hub';
Hub
.update()
.then(response=>JSON.stringify(response.text))
.then(result=>{
console.log(result);
});
Bravelydone!Nowwehaveapp.tsbadlycoupledwithhttp-client.ts.AndifwewanttoadaptthisAPIhubtosomethinglikeWebSocket,BANG.
Sohowcanwecreateentitiesthatareopenforextension,butclosedformodification?Thekeyisastableabstractionthatadapts.ConsiderthestorageandclientexamplewetookwithAdapterPatterninChapter4,StructuralDesignPatternswehadaStorageinterfacethatisolatesimplementationofdatabaseoperationsfromtheclient.Andassumingthattheinterfaceiswell-designedtomeetupcomingfeaturerequirements,itispossiblethatitwillneverchangeorjustneedtobeextendedduringthelifecycleoftheprogram.
AbstractioninJavaScriptandTypeScriptGuesswhat,ourbelovedJavaScriptdoesnothaveaninterface,anditisdynamicallytyped.Wewerenotevenabletoactuallywriteaninterface.However,wecouldstillwritedowndocumentationabouttheabstractionandcreatenewconcreteimplementationsjustbyobeyingthatdescription.
ButTypeScriptoffersinterface,andwecancertainlytakeadvantageofit.ConsidertheCommandResultclassintheprevioussection.Wewerewritingitasaconcreteclass,butitmayhavesubclassesthatoverridetheprintorrendermethodforcustomizedoutput.However,thetypesysteminTypeScriptcaresonlyabouttheshapeofatype.Thatmeans,whileyouaredeclaringanentitywithtypeCommandResult,theentitydoesnotneedtobeaninstanceofCommandResult:anyobjectwithacompatibletype(namelyhasmethodsprintandrenderwithpropersignaturesinthiscase)willdothejob.
Forexample,thefollowingcodeisvalid:
letenvironment:Environment;
letcommand:Command={
environment,
print(items){},
render(items){},
execute(){}
};
RefactorearlierIdoublestressedthattheopen-closedprinciplecanonlybeperfectlyfollowedunderidealscenarios.Thatcanbearesultoftworeasons:
1. Notallentitiesinasystemcanbeopentoextensionandclosedtomodificationatthesametime.Therewillalwaysbechangesthatneedtobreaktheclosureofexistingentitiestocompletetheirfunctionalities.Whenwearedesigningtheinterfaces,weneeddifferentstrategiesforcreatingstableclosuresfordifferentforeseeablesituations.Butthisrequiresnotableexperienceandnoonecandoitperfectly.
2. Noneofusistoogoodatdesigningaprogramthatlastslongandstayshealthyforever.Evenwiththoroughconsideration,abstractionsdesignedatthebeginningcanbechoppyfacingthechangingrequirements.
Sowhenweareexpectingtheentitiestobeclosedformodification,itdoesnotmeanthatweshouldjuststandthereandwatchitbeingclosed.Instead,whenthingsarestillundercontrol,weshouldrefactorandkeeptheabstractioninthestatusofbeingopentoextensionandclosedtomodificationatthetimepointofrefactoring.
LiskovsubstitutionprincipleTheopen-closedprincipleistheessentialprincipleofkeepingcodemaintainableandreusable.Andthekeytotheopen-closedprincipleisabstractionwithpolymorphism.Behaviorslikeimplementinginterfaces,orextendingclassesmakepolymorphicshapes,butthatmightnotbeenough.
TheLiskovsubstitutionprincipledeclaresthatderivedclassesmustbesubstitutablefortheirbaseclasses.OrinthewordsofBarbaraLiskov,whoraisedthisprinciple:
Whatiswantedhereissomethinglikethefollowingsubstitutionproperty:Ifforeachobjecto1oftypeSthereisanobjecto2oftypeTsuchthatforallprogramsPdefinedintermsofT,thebehaviorofPisunchangedwheno1issubstitutedforo2thenSisasubtypeofT.
Nevermind.Let'stryanotherone:anyforeseeableusageoftheinstanceofaclassshouldbeworkingwiththeinstancesofitsderivedclasses.
ExampleAndherewegowithastraightforwardviolationexample.ConsiderNoodlesandInstantNoodles(asubclassofNoodles)tobecooked:
functioncookNoodles(noodles:Noodles){
if(noodlesinstanceofInstantNoodles){
cookWithBoiledWaterAndBowl(noodles);
}else{
cookWithWaterAndBoiler(noodles);
}
}
Nowifwewanttohavesomefriednoodles...ThecookNoodlesfunctiondoesnotseemtobecapableofhandlingthat.Clearly,thisviolatestheLiskovsubstitutionprinciple,thoughitdoesnotmeanthatit'sabaddesign.
Let'sconsideranotherexamplewrittenbyUncleBobinhisarticletalkingaboutthisprinciple.WearecreatingclassSquarewhichisasubclassofRectangle,butinsteadofaddingnewfeatures,itaddsaconstrainttoRectangle:thewidthandheightofasquareshouldalwaysbeequaltoeachother.AssumewehaveaRectangleclassthatallowsitswidthandheighttobeset:
classRectangle{
constructor(
private_width:number;
private_height:number;
){}
setwidth(value:number){
this._width=value;
}
setheight(value:number){
this._height=value;
}
}
NowwehaveaproblemwithitssubclassSquare,becauseitgetswidthandheightsettersfromRectanglewhileitshouldn't.Wecancertainlyoverridethosesettersandmakebothofthemupdatewidthandheightsimultaneously.Butinsomesituations,theclientmightjustnotwantthat,becausedoingsowillmaketheprogramhardertobepredicted.
TheSquareandRectangleexampleviolatestheLiskovsubstitutionprinciple.Notbecausewedidn'tfindagoodwaytoinherit,butbecauseSquaredoesnotconformthebehaviorofRectangleandshouldnotbeasubclassofitatthebeginning.
TheconstraintsofsubstitutionTypeisanimportantpartinaprogramminglanguage,eveninJavaScript.Buthavingthesameshape,beingonthesamehierarchydoesnotmeantheycanbethesubstitutionofanotherwithoutsomepain.Morethanjusttheshape,thecompletebehavioriswhatreallymattersforimplementationsthatholdtotheLiskovsubstitutionprinciple.
InterfacesegregationprincipleWe'vealreadydiscussedtheimportantroleplayedbyabstractionsinobject-orienteddesign.Theabstractionsandtheirderivedclasseswithoutseparationusuallycomeupwithhierarchicaltreestructures.Thatmeanswhenyouchoosetocreateabranch,youcreateaparallelabstractiontoallofthoseonanotherbranch.
Forafamilyofclasseswithonlyonelevelofinheritance,thisisnotaproblem:becauseitisjustwhatyouwanttohavethoseclassesderivedfrom.Butforahierarchywithgreaterdepth,itcouldbe.
ExampleConsidertheTextReaderexamplewetookwithTemplateMethodPatterninChapter6,BehavioralDesignPatterns:ContinuouswehadFileAsciiTextReaderandHttpAsciiTextReaderderivedfromAsciiTextReader.ButwhatifwewanttohaveotherreadersthatunderstandUTF-8encoding?
Toachievethatgoal,wehavetwocommonoptions:separatetheinterfaceintotwofordifferentobjectsthatcooperate,orseparatetheinterfaceintotwothengetthemimplementedbyasingleclass.
Forthefirstcase,wecanrefactorthecodewithtwoabstractions,BytesReaderandTextReader:
Andforthesecondcase,wecanseparatemethodreadAllBytesanddecodeBytesontotwointerfaces,forexample,BytesReaderandBytesDecoder.Thuswemayimplementthemseparatelyandusetechniqueslikemixintoputthemtogether:
AninterestingpointaboutthisexampleisthatTextReaderaboveitselfisanabstractclass.Tomakethismixinactuallywork,weneedtocreateaconcreteclassofTextReader(withoutactuallyimplementingreadAllBytesanddecodeBytes),andthenmixintwoconcreteclassesofBytesReaderandBytesDecoder.
PropergranularityItissaidthatbycreatingsmallerinterfaces,wecanavoidaclientfromusingbigclasseswithfeaturesthatitneverneeds.Thismaycauseunnecessaryusageofresources,butinpractice,thatusuallywon'tbeaproblem.Themostimportantpartoftheinterfacesegregationprincipleisstillaboutkeepingcodemaintainableandreusable.
Thenthequestioncomesoutagain,howsmallshouldaninterfacebe?Idon'tthinkIhaveasimpleanswerforthat.ButIamsurethatbeingtoosmallmightnothelp.
DependencyinversionprincipleWhenwetalkaboutdependencies,thenaturalsenseisaboutdependenciesfrombottomtotop,justlikehowbuildingsarebuilt.Butunlikeabuildingthatstandsfortensofyearswithlittlechange,softwarekeepschangingduringitslifecycle.Everychangecosts,moreorless.
Thedependencyinversionprincipledeclaresthatentitiesshoulddependonabstractions,notonconcretions.Higherlevelcodeshouldnotdependdirectlyonlow-levelimplementations,instead,itshoulddependonabstractionsthatleadtothoseimplementations.Andthisiswhythingsareinverse.
ExampleStilltakingtheHTTPclientandAPIhubasanexample,whichobviouslyviolatesthedependencyinversionprinciple,takingtheforeseeableapplicationintoconsideration,whattheAPIhubshoulddependonisamessagingmechanismbridgingclientandserver,butnotbareHTTPclient.ThismeansweshouldhaveanabstractionlayerofmessagingbeforetheconcreteimplementationofHTTPclient:
SeparatinglayersComparedtootherprinciplesdiscussedinthischapter,thedependencyinversionprinciplecaresmoreaboutthescopeofmodulesorpackages.Astheabstractionmightusuallybemorestablethanconcreteimplementations,byfollowingdependencyinversionprinciple,wecanminimizetheimpactfromlow-levelchangestohigherlevelbehaviors.
ButforJavaScript(orTypeScript)projectsasthelanguageisdynamicallytyped,thisprincipleismoreaboutanideaofguidancethatleadstoastableabstractionbetweendifferentlayersofcodeimplementation.
Originally,animportantbenefitoffollowingthisprincipleisthat,ifmodulesorpackagesarerelativelylarger,separatingthembyabstractioncouldsavealotoftimeincompilation.ButforJavaScript,wedon'thavetoworryaboutthat;andforTypeScript,wedon'thavetorecompiletheentireprojectformakingchangestoseparatedmoduleseither.
SummaryInthischapter,wewalkedthroughthewell-knownSOLIDprincipleswithsimpleexamples.Sometimes,followingthoseprinciplescouldleadustoausefuldesignpattern.Andwealsofoundthatthoseprinciplesarestronglyboundtoeachother.Usuallyviolatingoneofthemmayindicateotherviolations.
ThoseprinciplescouldbeextremelyhelpfulforOOD,butcouldalsobeoverkilliftheyareappliedwithoutproperadaptions.Awell-designedsystemshouldhavethoseprinciplesconfirmedjustright,oritmightharm.
Inthenextchapter,insteadoftheories,we'llhavemoretimewithacompleteworkflowwithtestingandcontinuousintegrationinvolved.
Chapter9.TheRoadtoEnterpriseApplicationAfterwalkingthroughcommondesignpatterns,wehavenowthebasisofcodedesigning.However,softwareengineeringismoreaboutwritingbeautifulcode.Whilewearetryingtokeepthecodehealthyandrobust,westillhavealottodotokeeptheprojectandtheteamhealthy,robust,andreadytoscale.Inthischapter,we'lltalkaboutpopularelementsintheworkflowofwebapplications,andhowtodesignaworkflowthatfitsyourteam.
Thefirstpartwouldbesettingupthebuildstepsofourdemoproject.We'llquicklywalkthroughhowtobuildfrontendprojectswithwebpack,oneofthemostpopularpackagingtoolsthesedays.Andwe'llconfiguretests,codelinter,andthensetupcontinuousintegration.
Thereareplentyofnicechoiceswhenitcomestoworkflowintegration.Personally,IpreferTeamFoundationServerforprivateprojectsoracombinationofGitHubandTravis-CIforopen-sourceprojects.WhileTeamFoundationServer(orVisualStudioTeamServicesasitscloud-basedversion)providesaone-stopsolutionfortheentireapplicationlifecycle,thecombinationofGitHubandTravis-CIismorepopularintheJavaScriptcommunity.Inthischapter,wearegoingusetheservicesprovidedbyGitHubandTravis-CIforourworkflow.
Herearewhatwearegoingtowalkthrough:
Packagingfrontendassetswithwebpack.Settinguptestsandlinter.GettingourhandsonaGitflowbranchingmodelandotherGit-relatedworkflow.ConnectingaGitHubrepositorywithTravis-CI.Apeekintoautomateddeployment.
CreatinganapplicationWe'vetalkedaboutcreatingTypeScriptapplicationsforbothfrontendandbackendprojectsintheChapter1,ToolsandFrameworks.AndnowwearegoingtocreateanapplicationthatcontainstwoTypeScriptprojectsatthesametime.
DecisionbetweenSPAand"normal"webapplicationsApplicationsfordifferentpurposesresultindifferentchoices.SPA(singlepageapplication)usuallydeliversabetteruserexperienceafterbeingloaded,butitcanalsoleadtotrade-offsonSEOandmayrelyonmorecomplexMV*frameworkslikeAngular.
OnesolutiontobuildSEO-friendlySPAistobuildauniversal(orisomorphic)applicationthatrunsthesamecodeonbothfrontendandbackend,butthatcouldintroduceevenmorecomplexity.OrareverseproxycouldbeconfiguredtorenderautomaticallygeneratedpageswiththehelpoftoolslikePhantom.
Inthisdemoproject,we'llchooseamoretraditionalwebapplicationwithmultiplepagestobuild.Andhere'sthefilestructureoftheclientproject:
TakingteamcollaborationintoconsiderationBeforeweactuallystartcreatingareal-worldapplication,weneedtocomeupwithareasonableapplicationstructure.Aproperapplicationstructureismorethansomethingunderwhichthecodecompilesandruns.Itshouldbearesult,takinghowyourteammembersworktogetherintoconsideration.
Forexample,anamingconventionisinvolvedinthisdemoclientstructureshownearlier:pageassetsarenamedafterpagenamesinsteadoftheirtypes(forexample,style.scss)ornameslikeindex.ts.Andtheconsiderationbehindthisconventionismakingitmorefriendlyforfilenavigationbythekeyboard.
Ofcourse,thisconsiderationisvalidonlyifasignificantnumberofdevelopersinyourteamarecoolwithkeyboardnavigation.Otherthanoperationpreferences,theexperiencesandbackgroundsofateamshouldbeseriouslyconsideredaswell:
Shouldthe"full-stack"modebeenabledforyourteam?Shouldthe"full-stack"modebeenabledforeveryengineerinyourteam?Howshouldyoudivideworkbetweenfrontendandbackend?
Usually,it'snotnecessaryandnotefficienttolimittheaccessofafrontendengineertoclient-sidedevelopment.Ifit'spossible,frontendengineerscouldtakeoverthecontrollerlayerofthebackendandleavehardcorebusinessmodelsandlogictoengineersthatfocusmoreonthebackend.
Wearehavingtheclientandserver-sideprojectsinthesamerepositoryforaneasierintegrationduringdevelopment.Butitdoesnotmeaneverythinginthefrontendorbackendcodebaseshouldbeinthissinglerepository.Instead,multiplemodulescouldbeextractedandmaintainedbydifferentdevelopersinpractice.Forexample,youcanhavedatabasemodelsandbusinesslogicmodelsseparatedfromthecontrollersonthebackend.
BuildingandtestingprojectsWehavealreadytalkedaboutbuildingandtestingTypeScriptprojectsatthebeginningofthisbook.Inthissection,wewillgoalittlebitfurtherforfrontendprojects,includingthebasisofusingWebpacktoloadstaticassetsaswellascodelinting.
StaticassetspackagingwithwebpackModularizinghelpscodekeepahealthystructureandmakesitmaintainable.However,itcouldleadtoperformanceissuesifdevelopment-timecodewritteninsmallmodulesaredirectlydeployedwithoutbundlingforproductionusage.Sostaticassetspackagingbecomesaserioustopicoffrontendengineering.
Backtotheolddays,packagingJavaScriptfileswasjustaboutuglifyingsourcecodeandconcatenatingfilestogether.Theprojectmightbemodularizedaswell,butinaglobalway.ThenwehavelibrarieslikeRequire.js,withmodulesnolongerautomaticallyexposingthemselvestotheglobalscope.
ButasIhavementioned,havingtheclientdownloadmodulefilesseparatelyisnotidealforperformance;soonwehadtoolslikebrowserify,andlater,webpack-oneofthemostpopularfrontendpackagingtoolsthesedays.
Introductiontowebpack
Webpackisanintegratedpackagingtooldedicated(atleastatthebeginning)tofrontendprojects.ItisdesignedtopackagenotonlyJavaScript,butalsootherstaticassetsinafrontendproject.Webpackprovidesbuilt-insupportforbothasynchronousmoduledefinition(AMD)andcommonjs,andcanloadES6orothertypesofresourcesviaplugins.
Note
ES6modulesupportwillgetbuilt-inforwebpack2.0,butbythetimethischapteriswritten,youstillneedpluginslikebabel-loaderorts-loadertomakeitwork.Andofcoursewearegoingtousets-loaderlater.
Toinstallwebpackvianpm,executethefollowingcommand:
$npminstallwebpack-g
BundlingJavaScript
BeforeweactuallyusewebpacktoloadTypeScriptfiles,we'llhaveaquickwalkthroughofbundlingJavaScript.
First,let'screatethefileindex.jsunderthedirectoryclient/src/withthefollowingcodeinside:
varFoo=require('./foo');
Foo.test();
Thencreatethefilefoo.jsinthesamefolderwiththefollowingcontent:
exports.test=functiontest(){
console.log('Hello,Webpack!');
};
Nowwecanhavethembundledasasinglefileusingthewebpackcommand-lineinterface:
$webpack./client/src/index.js./client/out/bundle.js
Byviewingthebundle.jsfilegeneratedbywebpack,youwillseethatthecontentsofbothindex.jsandfoo.jshavebeenwrappedintothatsinglefile,togetherwiththebootstrapcodeofwebpack.Ofcourse,wewouldprefernottotypethosefilepathsinthecommandlineeverytime,buttouseaconfigurationfileinstead.
WebpackprovidesconfigurationfilesupportintheformofJavaScriptfiles,whichmakesitmoreflexibletogeneratenecessarydatalikebundleentriesautomatically.Let'screateasimpleconfigurationfilethatdoeswhatthepreviouscommanddid.
Createfileclient/webpack.config.jswiththefollowinglines:
'usestrict';
constPath=require('path');
module.exports={
entry:'./src/index',
output:{
path:Path.join(__dirname,'out'),
filename:'bundle.js'
}
};
Thesearethetwothingstomention:
1. Thevalueoftheentryfieldisnotthefilename,butthemoduleid(mostofthetimethisisunresolved)instead.Thismeansthatyoucanhavethe.jsextensionomitted,buthavetoprefixitwith./or../bydefaultwhenreferencingafile.
2. Theoutputpathisrequiredtobeabsolute.Buildinganabsolutepathwith__dirnameensuresitworksproperlyifwearenotexecutingwebpackunderthesamedirectoryastheconfigurationfile.
LoadingTypeScript
NowwearegoingtoloadandtranspileourbelovedTypeScriptusingthewebpackplugints-loader.Beforeupdatingtheconfiguration,let'sinstallthenecessarynpmpackages:
$npminstalltypescriptts-loader--save-dev
Ifthingsgowell,youshouldhavetheTypeScriptcompileraswellasthets-loaderplugininstalledlocally.Wemayalsowanttorenameandupdatethefilesindex.jsandfoo.jstoTypeScriptfiles.
Renameindex.jstoindex.tsandupdatethemoduleimportingsyntax:
import*asFoofrom'./foo';
Foo.test();
Renamefoo.jstofoo.tsandupdatethemoduleexportingsyntax:
exportfunctiontest(){
console.log('Hello,Webpack!');
}
Ofcourse,wewouldwanttoaddthetsconfig.jsonfileforthoseTypeScriptfiles(inthefolderclient):
{
"compilerOptions":{
"target":"es5",
"module":"commonjs"
},
"exclude":[
"out",
"node_modules"
]
}
Note
ThecompileroptionoutDirisomittedherebecauseitismanagedinthewebpackconfigurationfile.
TomakewebpackworkwithTypeScriptviats-loader,we'llneedtotellwebpacksomeinformationintheconfigurationfile:
1. Webpackwillneedtoresolvefileswith.tsextensions.Webpackhasadefaultextensionslisttoresolve,including''(emptystring),'.webpack.js','.web.js',and'.js'.Weneedtoadd'.ts'tothislistforittorecognizeTypeScriptfiles.
2. Webpackwillneedtohavets-loaderloading.tsmodulesbecauseitdoesnotcompileTypeScriptitself.
Andhereistheupdatedwebpack.config.js:
'usestrict';
constPath=require('path');
module.exports={
entry:'./src/index',
output:{
path:Path.join(__dirname,'bld'),
filename:'bundle.js'
},
resolve:{
extensions:['','.webpack.js','.web.js','.ts','.js']
},
module:{
loaders:[
{test:/\.ts$/,loader:'ts-loader'}
]
}
};
Nowexecutethecommandwebpackundertheclientfolderagain,weshouldgetthecompiledandbundledoutputasexpected.
Duringdevelopment,wecanenabletranspilemode(correspondingtothecompileroptionisolatedModules)ofTypeScripttohavebetterperformanceoncompilingchangingfiles.Butitmeanswe'llneedtorelyonanIDEoraneditortoprovideerrorhints.Andremembertomakeanothercompilationwithtranspilemodedisabledafterdebuggingtoensurethingsstillwork.
Toenabletranspilemode,addatsfield(definedbythets-loaderplugin)withtranspileOnlysettotrue:
module.exports={
...
ts:{
transpileOnly:true
}
};
Splittingcode
Totaketheadvantageofcodecachingacrosspages,wemightwanttosplitthepackagedmodulesascommonpieces.Thewebpackprovidesabuilt-inplugincalledCommonsChunkPluginthatcanpickoutcommonmodulesandhavethempackedseparately.
Forexample,ifwecreateanotherfilecalledbar.tsthatimportsfoo.tsjustlikeindex.tsdoes,foo.tscanbetreatedasacommonchunkandbepackedseparately:
module.exports={
entry:['./src/index','./src/bar'],
...
plugins:[
newWebpack.optimize.CommonsChunkPlugin({
name:'common',
filename:'common.js'
})
]
};
Formulti-pageapplications,itiscommontohavedifferentpageswithdifferententryscripts.Insteadofmanuallyupdatingtheentryfieldintheconfigurationfile,wecantakeadvantage
ofitbeingJavaScriptandgenerateproperentriesautomatically.Todoso,wemightwantthehelpofthenpmpackageglobformatchingpageentries:
$npminstallglob--saved-dev
Andthenupdatethewebpackconfigurationfile:
constglob=require('glob');
module.exports={
entry:glob
.sync('./src/pages/*/*.ts')
.filter(path=>
Path.basename(path,'.ts')===
Path.basename(Path.dirname(path))
),
...
};
Splittingthecodecanberatheracomplextopicfordeepdive,sowe'llstophereandletyouexplore.
Loadingotherstaticassets
Aswe'vementioned,webpackcanalsobeusedtoloadotherstaticassetslikestylesheetanditsextensions.Forexample,youcanusethecombinationofstyle-loader,css-loaderandsass-loader/less-loadertoload.sass/.lessfiles.
Theconfigurationissimilartots-loadersowe'llnotspendextrapagesfortheirintroductions.Formoreinformation,refertothefollowingURLs:
Embeddedstylesheetsinwebpack:https://webpack.github.io/docs/stylesheets.htmlSASSloaderforwebpack:https://github.com/jtangelder/sass-loaderLESSloaderforwebpack:https://github.com/webpack/less-loader
AddingTSLinttoprojectsAconsistentcodestyleisanimportantfactorofcodequality,andlintersareourbestfriendswhenitcomestocodestyles(andtheyalsohelpswithcommonmistakes).ForTypeScriptlinting,TSLintiscurrentlythesimplestchoice.
TheinstallationandconfigurationofTSLintareeasy.Tobeginwith,let'sinstalltslintasaglobalcommand:
$npminstalltslint-g
Andthenweneedtoinitializeaconfigurationfileusingthefollowingcommandundertheprojectrootdirectory:
$tslint--init
TSLintwillthengenerateadefaultconfigurationfilenamedtslint.json,andyoumaycustomizeitbasedonyourownpreferences.AndnowwecanuseittolintourTypeScriptsourcecode:
$tslint*/src/**/*.ts
IntegratingwebpackandtslintcommandwithnpmscriptsAswe'vementionedbefore,anadvantageofusingnpmscriptsisthattheycanhandlelocalpackageswithexecutablesproperlybyaddingnode_modules/.bintoPATH.Andtomakeourapplicationeasiertobuildandtestforotherdevelopers,wecanhavewebpackandtslintinstalledasdevelopmentdependenciesandaddrelatedscriptstopackage.json:
"scripts":{
"build-client":"cdclient&&webpack",
"build-server":"tsc--projectserver",
"build":"npmrunbuild-client&&npmrunbuild-server",
"lint":"tslint./*/src/**/*.ts",
"test-client":"cdclient&&mocha",
"test-server":"cdserver&&mocha",
"test":"npmrunlint&&npmruntest-client&&npmruntest-server"
}
VersioncontrolThinkingbacktomyseniorhighschooldays,Iknewnothingaboutversioncontroltools.ThebestthingIcoulddowastocreateadailyarchiveofmycodeonaUSBdisk.AndyesIdidloseone!
Nowadays,withtheboomofversioncontroltoolslikeGitandtheavailabilitiesofmultiplefreeserviceslikeGitHubandVisualStudioTeamServices,managingcodewithversioncontroltoolshasbecomeadailybasisforeverydeveloper.
Asthemostpopularversioncontroltool,Githasalreadybeenplayinganimportantroleinyourworkorpersonalprojects.Inthissection,we'lltalkaboutpopularpracticesofusingGitinateam.
Note
NotethatIamassumingthatyoualreadyhavethebasicknowledgeofGit,andknowhowtomakeoperationslikeinit,commit,push,pullandmerge.Ifnot,pleasegethandsonandtrytounderstandthoseoperationsbeforecontinue.
Note
Checkoutthisquicktutorialat:https://try.github.io/.
GitflowVersioncontrolplaysanimportantaroleanditdoesnotonlyinfluencethesourcecodemanagementprocessbutalsoshapestheentireworkflowofproductdevelopmentanddelivery.Thusasuccessfulbranchingmodelbecomesaseriouschoice.
GitflowisacollectionofGitextensionsthatprovideshigh-levelrepositoryoperationsforabranchingmodelraisedbyVincentDriessen.ThenameGitflowusuallyreferstothebranchingmodelaswell.
Inthisbranchingmodel,therearetwomainbranches:masteranddevelop,aswellasthreedifferenttypesofsupportingbranches:feature,hotfix,andrelease.
WiththehelpofGitflowextensions,wecaneasilyapplythisbranchingmodelwithouthavingtorememberandtypedetailedsequencesofcommands.Toinstall,pleasecheckouttheinstallationguideofGitflowat:https://github.com/nvie/gitflow/wiki/Installation.
BeforewecanuseGitflowtocreateandmergebranches,we'llneedtomakeaninitialization:
$gitflowinit-d
Note
Here-dstandsforusingdefaultbranchnamingconventions.Ifyouwouldliketocustomize,youmayomitthe-doptionandanswerthequestionsaboutgitflowinitcommand.
Thiswillcreatemasteranddevelopbranches(ifnotpresent)andsaveGitflow-relatedconfigurationtothelocalrepository.
Mainbranches
Thebranchingmodeldefinestwomainbranches:masteranddevelop.Thosetwobranchesexistinthelifetimeofthecurrentrepository:
Note
Thegraphintheprecedingshowsasimplifiedrelationshipbetweendevelopandmasterbranches.
Branchmaster:TheHEADofmasterbranchshouldalwayscontainproduction-readysourcecode.Itmeansthatnodailydevelopmentisdoneonmasterbranchinthisbranchingmodel,andonlycommitsthatarefullytestedandcanbeperformedwithafast-forwardshouldbemergedintothisbranch.Branchdevelop:TheHEADofdevelopbranchshouldcontaindelivereddevelopmentsourcecode.Changestodevelopbranchwillfinallybemergedintomaster,butusuallynotdirectly.We'llcometothatlaterwhenwetalkaboutreleasebranches.
Supportingbranches
TherearethreetypesofsupportingbranchesinthebranchingmodelofGitflow:feature,hotfix,andrelease.Whattheyroughlydohasalreadybeensuggestedbytheirnames,andwe'llhavemoredetailstofollow.
Featurebranches
Afeaturebranchhasonlydirectinteractionswiththedevelopbranch,whichmeansitchecksoutfromadevelopbranchandmergesbacktoadevelopbranch.Thefeaturebranchesmightbethesimplesttypeofbranchesoutofthethree.
TocreateafeaturebranchwithGitflow,simplyexecutethefollowingcommand:
$gitflowfeaturestart<feature-name>
NowGitflowwillautomaticallycheckoutanewbranchnamedafterfeature/<feature-name>,andyouarereadytostartdevelopmentandcommitchangesoccasionally.
Aftercompletingfeaturedevelopment,Gitflowcanautomaticallymergethingsbacktothedevelopbranchbythefollowingcommand:
$gitflowfeaturefinish<feature-name>
Afeaturebranchisusuallystartedbythedeveloperwhoisassignedtothedevelopmentofthatveryfeatureandismergedbythedeveloperhimorherself,ortheownersofthedevelopbranch(forexample,ifcodereviewisrequired).
Releasebranches
Inasingleiterationofaproduct,afterfinishingthedevelopmentoffeatures,weusuallyneedastageforfullytestingeverything,fixingbugs,andactuallygettingitreadytobereleased.Andworkforthisstagewillbedoneonreleasebranches.
Unlikefeaturebranches,arepositoryusuallyhasonlyoneactivereleasebranchatatime,and
itisusuallycreatedbytheowneroftherepository.Whenthedevelopmentbranchisreachingastateofreleaseandathoroughtestisabouttobegin,wecanthencreateareleasebranchusingthefollowingcommand:
$gitflowreleasestart<version>
Fromnowon,bugfixesthataregoingtobereleasedinthisiterationshouldbemergedorcommittedtobranchrelease/<version>andchangestothecurrentreleasebranchcanbemergedbacktothedevelopbranchanytime.
Ifthetestgoeswellandimportantbugshavebeenfixed,wecanthenfinishthisreleaseandputitonline:
$gitflowreleasefinish<version>
Afterexecutingthiscommand,Gitflowwillmergethecurrentreleasebranchtobothmasteranddevelopbranches.SoinastandardGitflowbranchingmodel,thedevelopbranchwillnotbemergedintothemasterdirectly,thoughafterfinishingarelease,thecontentondevelopandmasterbranchescouldbeidentical(ifnomorechangesaremadetothedevelopbranchduringthereleasingstage).
Note
Finishingthecurrentreleaseusuallymeanstheendoftheiteration,andthedecisionshouldbemadewithseriousconsideration.
Hotfixbranches
Unfortunately,there'saphenomenonintheworldofdevelopers:bugsarealwayshardertofindbeforethecodegoeslive.Afterreleasing,ifseriousbugswerefound,wewouldhavetousehotfixestomakethingsright.
Ahotfixbranchworkskindoflikeareleasebranchbutlastsshorter(becauseyouwouldprobablywantitmergedassoonaspossible).Unlikefeaturebranchesbeingcheckedoutfromdevelopbranch,ahotfixbranchischeckedoutfrommaster.Andaftergettingthingsdone,itshouldbemergedbacktobothmasteranddevelopbranches,justlikeareleasebranchdoes.
Tocreateahotfixbranch,similarlyyoucanexecutethefollowingcommand:
$gitflowhotfixstart<hotfix-name>
Andtofinish,executethefollowingcommand:
$gitflowhotfixfinish<hotfix-name>
SummaryofGitflow
ThemostvaluableideainGitflowbesidethebranchingmodelitselfis,inmyopinion,the
clearoutlineofoneiteration.YoumaynotneedtofolloweverystepmentionedthusfartouseGitflow,butjustmakeitfityourwork.Forexample,forsmallfeaturesthatcanbedoneinasinglecommit,youmightnotactuallyneedafeaturebranch.Butconversely,Gitflowmightnotbringmuchvalueiftheiterationitselfgetschaotic.
PullrequestbasedcodereviewCodereviewcouldbeaveryimportantjointofteamcooperation.Itensuresacceptablequalityofthecodeitselfandhelpsnewcomerscorrecttheirmisunderstandingoftheprojectandaccumulateexperiencesrapidlywithouttakingawrongpath.
Ifyouhavetriedtocontributecodetoopen-sourceprojectsonGitHub,youmustbefamiliarwithpullrequestsorPR.ThereareactuallytoolsorIDEswithcodereviewingworkflowbuilt-in.ButwithGitHubandotherself-hostedserviceslikeGitLab,wecangetitdonesmoothlywithoutrelyingonspecifictools.
Configuringbranchpermissions
Restrictionsonaccessingspecificbrancheslikemasteranddeveloparenottechnicallynecessary.Butwithoutthoserestrictions,developerscaneasilyskipcodereviewingbecausetheyarejustabletodoso.InservicesprovidedbytheVisualStudioTeamFoundationServer,wemayaddacustomcheckinpolicytoforcecodereview.ButinlighterserviceslikeGitHubandGitLab,itmightbehardertohavesimilarfunctionality.
Theeasiestwaymightbetohavedeveloperswhoaremorequalifiedandfamiliarwiththecurrentprojecthavethepermissionsforwritingthedevelopbranch,andrestrictcodereviewinginthisgroupverbally.Forotherdevelopersworkingonthisproject,pullrequestsarenowforcedforgettingchangestheymerged.
Note
GitHubrequiresanorganizationaccounttospecifypushpermissionsforbranches.Besidesthis,GitHubprovidesastatusAPIandcanaddrestrictionstomergingsothatonlybrancheswithavalidstatuscangetmerged.
Commentsandmodificationsbeforemerge
AgreatthingaboutthosepopularGitservicesisthatthereviewerandmaybeothercolleaguesofyoursmaycommentonyourpullrequestsorevenspecificlinesofcodetoraisetheirconcernsorsuggestions.Andaccordingly,youcanmakemodificationstotheactivepullrequestandmakethingsalittlebitclosertoperfect.
Furthermore,referencesbetweenissuesandpullrequestsareshownintheconversation.Thisalongwiththecommentsandmodificationrecordsmakesthecontextofcurrentpullrequestsclearandtraceable.
TestingbeforecommitsIdeally,wewouldexpecteverycommitwemaketopasstestsandcodelinting.Butbecausewearehuman,wecaneasilyforgetaboutrunningtestsbeforecommittingchanges.Andthen,ifwehavealreadysetupcontinuousintegration(we'llcometothatshortly)ofthisproject,pushingthechangeswouldmakeitred.AndifyourcolleaguehassetupaCIlightwithanalarm,youwouldmakeitflashandsoundout.
Toavoidbreakingthebuildconstantly,youmightwanttoaddapre-commithooktoyourlocalrepository.
Githooks
Gitprovidesvarietiesofhookscorrespondingtospecificphasesofanoperationoranevent.AfterinitializingaGitrepository,Gitwillcreatehooksamplesunderthedirectory.git/hooks.
Nowlet'screatethefilepre-commitunderthedirectory.git/hookswiththefollowingcontent:
#!/bin/sh
npmruntest
Note
Thehookfiledoesnothavetobeabashfile,anditcanjustbeanyexecutable.Forexample,ifyouwantliketoworkwithaNode.jshook,youcanupdatetheshebangas#!/usr/bin/envnodeandthenwritethehookinJavaScript.
AndnowGitwillruntestsbeforeeverycommitofchanges.
Addingpre-commithookautomatically
Addinghooksmanuallytothelocalrepositorycouldbetrivial,butluckilywehavenpmpackageslikepre-committhatwilladdpre-commithooksautomaticallywhenit'sinstalled(asyouusuallymightneedtorunnpminstallanyway).
Tousethepre-commitpackage,justinstallitasadevelopmentdependency:
$npminstallpre-commit--save-dev
Itwillreadyourpackage.jsonandexecutenpmscriptslistedwiththefieldpre-commitorprecommit:
{
..
"script":{
"test":"istanbulcover..."
},
"pre-commit":["test"]
}
Note
Atthetimeofwriting,npmpackagepre-commitusessymboliclinkstocreateGithook,whichrequiresadministratorprivilegesonWindows.Butfailingtocreateasymboliclinkwon'tstopthenpminstallcommandfromcompleting.SoifyouareusingWindows,youprobablymightwanttoensurepre-commitisproperlyinstalled.
ContinuousintegrationThecontinuousintegration(CI)referstoapracticeofintegratingmultiplepartsofaprojectorsolutiontogetherregularly.Dependingonthesizeoftheproject,theintegrationcouldbetakenforeverysinglechangeoronatimedschedule.
Themaingoalofcontinuousintegrationistoavoidintegrationissues,anditalsoenforcesthedisciplineoffrequentautomatedtesting,thishelpstofindbugsearlierandpreventsthedegenerationoffunctionalities.
Therearemanysolutionsorserviceswithcontinuousintegrationsupport.Forexample,self-hostedserviceslikeTFSandJenkins,orcloud-basedserviceslikeVisualStudioTeamServices,Travis-CI,andAppVeyor.WearegoingtowalkthroughthebasicconfigurationofTravis-CIwithourdemoproject.
ConnectingGitHubrepositorywithTravis-CIWearegoingtouseGitHubastheGitservicebehindcontinuousintegration.Firstofall,let'sgetourGitHubrepositoryandTravis-CIsettingsready:
1. CreateacorrespondentrepositoryasoriginandpushthelocalrepositorytoGitHub:
$gitremoteaddoriginhttps://github.com/<username>/<repo>.git
$gitpush-uoriginmaster
2. SignintoTravis-CIwithyourGitHubaccountat:https://travis-ci.org/auth.3. Gototheaccountpage,findtheprojectweareworkingwith,andthenflickthe
repositoryswitchon.
NowtheonlythingweneedtomakethecontinuousintegrationsetupworkisaproperTravis-CIconfigurationfile.Travis-CIhasbuilt-insupportformanylanguagesandruntimes.ItprovidesmultipleversionsofNode.jsandmakesitextremelyeasytotestNode.jsprojects.
Createthefile.travis.ymlintherootofprojectwiththefollowingcontent:
language:node_js
node_js:
-"4"
-"6"
before_script:
-npmrunbuild
ThisconfigurationfiletellsTravis-CItotestwithbothNode.jsv4andv6,andexecutethecommandnpmrunbuildbeforetesting(itwillrunthenpmtestcommandautomatically).
Almostready!Nowaddandcommitthenew.travis.ymlfileandpushittoorigin.Ifeverythinggoeswell,weshouldseeTravis-CIstartthebuildofthisprojectshortly.
Note
Youmightbeseeingbuildingstatusbadgeseverywherenowadays,andit'seasytoaddonetotheREADME.mdofyourownproject.IntheprojectpageonTravis-CI,youshouldseeabadgenexttotheprojectname.CopyitsURLandaddittotheREADME.mdasanimage:
![buildingstatus](https://api.travis-ci.org/<username>/<repo>.svg)
DeploymentautomationRatherthanaversioncontroltool,Gitisalsopopularforrelativelysimpledeploymentautomation.Andinthissection,we'llgetourhandsonandconfigureautomateddeploymentbasedonGit.
PassivedeploymentbasedonGitserversidehooksTheideaofpassivedeploymentissimple:whenaclientpushescommitstothebarerepositoryontheserver,apost-receivehookofGitwillbetriggered.Andthuswecanaddscriptscheckingoutchangesandstartdeployment.
TheelementsinvolvedintheGitdeploymentsolutiononboththeclientandserversidesincludes:
Tomakethismechanismwork,weneedtoperformthefollowingsteps:
1. Createabarerepositoryontheserverwiththefollowingcommand:
$mkdirdeployment.git
$cddeployment.git
$gitinit--bare
Note
Abarerepositoryusuallyhastheextension.gitandcanbetreatedasacentralizedplaceforsharingpurposes.Unlikenormalrepositories,abarerepositorydoesnothavetheworkingcopyofsourcefiles,anditsstructureisquitesimilartowhat'sinsidea.gitdirectoryofanormalrepository.
2. Adddeployment.gitasaremoterepositoryofourproject,andtrytopushthemasterbranchtothedeployment.gitrepository:
$cd../demo-project
$gitremoteadddeployment../deployment.git
$gitpush-udeploymentmaster
Note
Weareaddingalocalbarerepositoryastheremoterepositoryinthisexample.Extrastepsmightberequiredtocreaterealremoterepositories.
3. Addapost-receivehookforthedeployment.gitrepository.We'vealreadyworkedwiththeclientsideGithookpre-commit,andtheserversidehooksworkthesameway.
Butwhenitcomestoaseriousproductiondeployment,howtowritethehookcouldbeahardquestiontoanswer.Forexample,howdoweminimizetheimpactofdeployingnewbuilds?
Ifwehavesetupourapplicationwithhighavailabilityloadbalancing,itmightnotbeabigissuetohaveoneofthemofflineforminutes.Butcertainlynotalloftheminthiscase.Soherearesomebasicrequirementsofthedeployscriptsonboththeclientandserversides:
ThedeploymentshouldbeproceededinacertainsequenceThedeploymentshouldstoprunningservicesgently
Andwecandobetterby:
BuildingoutsideofthepreviousdeploymentdirectoryOnlytryingtostoprunningservicesafterthenewlydeployedapplicationisreadytostartimmediately
ProactivedeploymentbasedontimersornotificationsInsteadofusingGithooks,wecanhaveothertoolspullandbuildtheapplicationautomaticallyaswell.Inthisway,wenolongerneedtheclienttopushchangestoserversseparately.Andinstead,theprogramontheserverwillpullchangesfromaremoterepositoryandcompletedeployment.
Anotificationmechanismispreferredtoavoidfrequentfetchingthough,andtherearealreadytoolslikePM2thathaveautomateddeploymentbuilt-in.Youcanalsoconsiderbuildingupyourownusinghooksprovidedbycloud-basedorself-hostedGitservices.
SummaryInthisfinalchapter,webuilttheoutlineofacompleteworkflowstartingwithbuildingandtestingtocontinuousintegrationandautomateddeployment.We'vecoveredsomepopularservicesortoolsandprovideotheroptionsforreaderstodiscoverandexplore.
Amongthevarietiesofchoice,youmightagreethatthemostappropriateworkflowforyourteamistheworkflowthatfitsthebest.Takingpeopleratherthantechnologiesaloneintoconsiderationisanimportantpartofsoftwareengineering,anditisalsothekeytokeepingtheteamefficient(andhappy,perhaps).
Thesadthingaboutateam,oracrowdofpeopleisthatusuallyonlyafewofthemcankeepthepassionburning.We’vetalkedaboutfindingthebalancepoint,butthatiswhatwestillneedtopractice.Andinmostofthecases,expectingeveryoneofyourteamtofindtherightpointisjustunreasonable.Whenitcomestoteamprojects,we'dbetterhaverulesthatcanbevalidatedautomaticallyinsteadofconventionsthatarenottestable.
Afterreadingthisbook,Ihopethereadergetstheoutlinesofthebuildsteps,workflow,andofcourseknowledgeofcommondesignpatterns.Butratherthanthecoldexplanationsofdifferenttermsandpatterns,therearemoreimportantideasIwantedtodeliver:
Weashumansaredull,andshouldalwayskeepourworkdividedascontrollablepieces,insteadofactinglikeagenius.Andthat'salsowhyweneedtodesignsoftwaretomakeourliveseasier.Andwearealsounreliable,especiallyatascaleofsomemass(likeateam).Asalearner,alwaystrytounderstandthereasonbehindaconclusionormechanismbehindaphenomenon.
Part3.Module3TypeScriptBlueprints
Buildexcitingend-to-endapplicationswithTypeScript
Chapter1.TypeScript2.0FundamentalsInChapters2through5,wewilllearnafewframeworkstocreate(web)applicationswithTypeScript.FirstyouneedsomebasicknowledgeofTypeScript2.0.IfyouhaveusedTypeScriptpreviously,thenyoucanskimoverthischapter,oruseitasareferencewhilereadingtheotherchapters.IfyouhavenotusedTypeScriptyet,thenthischapterwillteachyouthefundamentalsofTypeScript.
WhatisTypeScript?TheTypeScriptlanguagelookslikeJavaScript;itisJavaScriptwithtypeannotationsaddedtoit.TheTypeScriptcompilerhastwomainfeatures:itisatranspilerandatypechecker.Atranspilerisaspecialformofcompilerthatoutputssourcecode.IncaseoftheTypeScriptcompiler,TypeScriptsourcecodeiscompiledtoJavaScriptcode.Atypecheckersearchesforcontradictionsinyourcode.Forinstance,ifyouassignastringtoavariable,andthenuseitasanumber,youwillgetatypeerror.
Thecompilercanfigureoutsometypeswithouttypeannotations;forothersyouhavetoaddtypeannotations.Anadditionaladvantageofthesetypesisthattheycanalsobeusedineditors.Aneditorcanprovidecompletionsandrefactoringbasedonthetypeinformation.EditorssuchasVisualStudioCodeandAtom(withaplugin,namelyatom-typescript)providesuchfeatures.
QuickexampleThefollowingexamplecodeshowssomebasicTypeScriptusage.Ifyouunderstandthiscode,youhaveenoughknowledgeforthenextchapters.Thisexamplecodecreatesaninputboxinwhichyoucanenteraname.Whenyouclickonthebutton,youwillseeapersonalizedgreeting:
classHello{
privateelement:HTMLDivElement;
privateelementInput:HTMLInputElement;
privateelementText:HTMLDivElement;
constructor(defaultName:string){
this.element=document.createElement("div");
this.elementInput=document.createElement("input");
this.elementText=document.createElement("div");
constelementButton=document.createElement("button");
elementButton.textContent="Greet";
this.element.appendChild(this.elementInput);
this.element.appendChild(elementButton);
this.element.appendChild(this.elementText);
this.elementInput.value=defaultName;
this.greet();
elementButton.addEventListener("click",
()=>this.greet()
);
}
show(parent:HTMLElement){
parent.appendChild(this.element);
}
greet(){
this.elementText.textContent=`Hello,
${this.elementInput.value}!`;
}
}
consthello=newHello("World");
hello.show(document.body);
Theprecedingcodecreatesaclass,Hello.TheclasshasthreepropertiesthatcontainanHTMLelement.Wecreatetheseelementsintheconstructor.TypeScripthasdifferenttypesforallHTMLelementsanddocument.createElementgivesthecorrespondingelementtype.Ifyoureplacedivwithspan(onthefirstlineoftheconstructor),youwouldgetatypeerrorsayingthattypeHTMLSpanElementisnotassignabletotypeHTMLDivElement.Theclasshastwofunctions:onetoaddtheelementtotheHTMLpageandonetoupdatethegreetingbasedontheenteredname.
Itisnotnecessarytospecifytypesforallvariables.ThetypesofthevariableselementButtonandhellocanbeinferredbythecompiler.
Youcanseethisexampleinactionbycreatinganewdirectoryandsavingthefileasscripts.ts.Inindex.html,youmustaddthefollowingcode:
<!DOCTYPEHTML>
<html>
<head>
<title>HelloWorld</title>
</head>
<body>
<scriptsrc="scripts.js"></script>
</body>
</html>
TheTypeScriptcompilerrunsonNodeJS,whichcanbeinstalledfromhttps://nodejs.org.Afterward,youcaninstalltheTypeScriptcompilerbyrunningnpminstalltypescript-ginaconsole/terminal.Youcancompilethesourcefilebyrunningtscscripts.ts.Thiswillcreatethescripts.jsfile.Openindex.htmlinabrowsertoseetheresult.
ThenextsectionsexplainthebasicsofTypeScriptinmoredetail.Afterreadingthosesections,youshouldunderstandthisexamplefully.
TranspilingThecompilertranspilesTypeScripttoJavaScript.Itdoesthefollowingtransformationsonyoursourcecode:
RemovealltypeannotationsCompilenewJavaScriptfeaturesforoldversionsofJavaScriptCompileTypeScriptfeaturesthatarenotstandardJavaScript
Wecanseetheprecedingthreetransformationsinactioninthenextexample:
enumDirection{
Left,
Right,
Up,
Down
}
letx:Direction=Direction.Left;
TypeScriptcompilesthistothefollowing:
varDirection;
(function(Direction){
Direction[Direction["Left"]=0]="Left";
Direction[Direction["Right"]=1]="Right";
Direction[Direction["Up"]=2]="Up";
Direction[Direction["Down"]=3]="Down";
})(Direction||(Direction={}));
varx=Direction.Left;
Inthelastline,youcanseethatthetypeannotationwasremoved.Youcanalsoseethatletwasreplacedbyvar,sinceletisnotsupportedinolderversionsofJavaScript.Theenumdeclaration,whichisnotstandardJavaScript,wastranspiledtonormalJavaScript.
TypecheckingThemostimportantfeatureofTypeScriptistypechecking.Forinstance,forthefollowingcode,itwillreportthatyoucannotassignanumbertoastring:
letx:string=4;
Inthenextsections,youwilllearnthenewfeaturesofthelatestJavaScriptversions.Afterward,wewilldiscussthebasicsofthetypechecker.
LearningmodernJavaScriptJavaScripthasdifferentversions.SomeoftheseareES3,ES5,ES2015(alsoknownasES6),andES2016.Recentversionsarenamedaftertheyearinwhichtheywereintroduced.Dependingontheenvironmentforwhichyouwritecode,somefeaturesmightbeormightnotbesupported.TypeScriptcancompilenewfeaturesofJavaScripttoanolderversionofJavaScript.Thatisnotpossiblewithallfeatures,however.
RecentwebbrowserssupportES5andtheyareworkingonES2015.
Wewillfirsttakealookattheconstructsthatcanbetranspiledtoolderversions.
letandconstES2015hasintroducedletandconst.Thesekeywordsarealternativestovar.Thesepreventissueswithscoping,asletandconstareblockscopedinsteadoffunctionscoped.Youcanusesuchvariablesonlywithintheblockinwhichtheywerecreated.Itisnotallowedtousesuchvariablesoutsideofthatblockorbeforeitsdefinition.Thefollowingexampleillustratessomedangerousbehaviorthatcouldbepreventedwithletandconst:
alert(x.substring(1,2));
varx="lorem";
for(vari=0;i<10;i++){
setTimeout(function(){
alert(i);
},10*i);
}
Thefirsttwolinesgivenoerror,asavariabledeclaredwithvarcanbeusedbeforeitsdefinition.Withletorconst,youwillgetanerror,asexpected.
Thesecondpartshows10messageboxessaying10.Wewouldexpect10messagessaying0,1,2,andsoonupto9.But,whenthecallbackisexecutedandalertiscalled,iisalready10,soyousee10messagessaying10.
Whenyouchangethevarkeywordstolet,youwillgetanerrorinthefirstlineandthemessagesworkasexpected.Thevariableiisboundtotheloopbody.Foreachiteration,itwillhaveadifferentvalue.Theforloopistranspiledasfollows:
var_loop_1=function(i){
setTimeout(function(){
alert(i);
},10*i);
};
for(vari=0;i<10;i++){
_loop_1(i);
}
Avariabledeclaredwithconstcannotbereassigned,andavariablewithletcanbereassigned.Ifyoureassignaconstvariable,yougetacompileerror.
ClassesAsofES2015,youcancreateclasseseasily.Inolderversions,youcouldsimulateclassestoacertainextent.TypeScripttranspilesaclassdeclarationtotheoldwaytosimulateaclass:
classPerson{
age:number;
constructor(publicname:string){
}
greet(){
console.log("Hello,"+this.name);
}
}
constperson=newPerson("World");
person.age=35;
person.greet();
Thisexampleistranspiledtothefollowing:
varPerson=(function(){
functionPerson(name){
this.name=name;
}
Person.prototype.greet=function(){
console.log("Hello,"+this.name);
};
returnPerson;
}());
varperson=newPerson("World");
person.age=35;
person.greet();
Whenyouprefixanargumentoftheconstructorwithpublicorprivate,itisaddedasapropertyoftheclass.Otherpropertiesmustbedeclaredinthebodyoftheclass.ThisisnotpertheJavaScriptspecification,butneededwithTypeScriptfortypeinformation.
ArrowfunctionsES6introducedanewwaytocreatefunctions.Arrowfunctionsarefunctionexpressionsdefinedusing=>.Suchfunctionlookslikethefollowing:
(x:number,y:boolean):string=>{
statements
}
Thefunctionexpressionstartswithanargumentlist,followedbyanoptionalreturntype,thearrow(=>),andthenablockwithstatements.Ifthefunctionhasonlyoneargumentwithouttypeannotationandnoreturntypeannotation,youmayomittheparenthesis:x=>{...}.Ifthebodycontainsonlyonereturnstatement,withoutanyotherstatements,youcansimplifyitto(x:number,y:number)=>expression.Afunctionwithoneargumentandonlyareturnstatementcanbesimplifiedtox=>expression.
Besidestheshortsyntax,arrowfunctionshaveoneothermajordifferencewithnormalfunctions.Arrowfunctionssharethevalueofthisandthepositionwhereitwasdefined;thisislexicallybound.Previously,youwouldstorethevalueofthisinavariablecalled_thisorself,oryouwouldfixthevalueusing.bind(this).Witharrowfunctions,thatisnotrequiredanymore.
FunctionargumentsItispossibletoaddadefaultvaluetoanargument:
functionsum(a=0,b=0,c=0){
returna+b+c;
}
sum(10,5);
Whenyoucallthisfunctionwithlessthanthreearguments,itwillsettheotherargumentsto0.TypeScriptwillautomaticallyinferthetypesofa,b,andcbasedontheirdefaultvalues,soyoudonothavetoaddatypeannotationthere.
Youcanalsodefineanoptionalargumentwithoutadefaultvalue:functiona(x?:number){}.Theargumentwillthenbeundefinedwhenitisnotprovided.ThisisnotstandardJavaScript,butonlyavailableinTypeScript.
Thesumfunctioncanbedefinedevenbetter,witharestargument.Attheendofafunction,youcanaddarestargument:
functionsum(...xs:number[]){
lettotal=0;
for(leti=0;i<xs.length;i++)total+=xs[i];
returntotal;
}
sum(10,5,2,1);
ArrayspreadItiseasiertocreatearraysinES6.Youcancreateanarrayliteral(withbrackets),inwhichyouuseanotherarray.Inthefollowingexample,youcanseehowyoucanaddanitemtoalistandhowyoucanconcatenatetwolists:
consta=[0,1,2];
constb=[...a,3];
constc=[...a,...b];
AsimilarfeatureforobjectliteralswillprobablybeaddedtoJavaScripttoo.
DestructuringWithdestructuring,youcaneasilycreatevariablesforpropertiesofanobjectorelementsofanarray:
consta={x:1,y:2,z:3};
constb=[4,5,6];
const{x,y,z}=a;
const[u,v,w]=b;
Theprecedingistranspiledtothefollowing:
vara={x:1,y:2,z:3};
varb=[4,5,6];
varx=a.x,y=a.y,z=a.z;
varu=b[0],v=b[1],w=b[2];
Youcanusedestructinginanassignment,variabledeclaration,orargumentofafunctionheader.
TemplatestringsWithtemplatestrings,youcaneasilycreateastringwithexpressionsinit.Ifyouwouldwrite"Hello,"+name+"!",youcannowwriteHello${name}!.
NewclassesES2015hasintroducedsomenewclasses,includingMap,Set,WeakMap,WeakSet,andPromise.Inmodernbrowsers,theseclassesarealreadyavailable.Forotherenvironments,TypeScriptdoesnotautomaticallyaddafallbackfortheseclasses.Instead,youshoulduseapolyfill,suchases6-shim.Mostbrowsersalreadysupporttheseclasses,soinmostcases,youdonotneedapolyfill.Youcanfindinformationonbrowsersupportathttp://caniuse.com.
TypecheckingThecompilerwillcheckthetypesofyourcode.Ithasseveralprimitivetypesandyoucandefinenewtypesyourself.Basedonthesetypes,thecompilerwillwarnwhenavalueofatypeisusedinaninvalidmanner.Thatcouldbeusingastringformultiplicationorusingapropertyofanobjectthatdoesnotexist.Thefollowingcodewouldshowtheseerrors:
letx="foo";
x*2;
x.bar();
TypeScripthasaspecialtype,calledany,thatallowseverything;youcanassigneveryvaluetoitandyouwillnevergettypeerrors.Thetypeanycanbeusedifyoudonothaveanexacttype(yet),forinstance,becauseitisacomplextypeorifitisfromalibrarythatwasnotwritteninTypeScript.Thismeansthatthefollowingcodegivesnocompileerrors:
letx:any="foo";
x*2;
x.bar();
Inthenextsections,wewilldiscoverthesetypesandlearnhowthecompilerfindsthesetypes.
PrimitivetypesTypeScripthasseveralprimitivetypes,whicharelistedinthefollowingtable:
Name Values Example
boolean true,false letx:boolean=true;
string Anystringliteral letx:string="foo";
number Anynumber,includingInfinity,-Infinity,andNaN
letx:number=42;
lety:number=NaN;
Literaltypes Literaltypescanonlycontainonevalue letx:"foo"="foo";
void Onlyusedforafunctionthatdoesnotreturnavalue
functiona():void{}
never Novalues
any Allvaluesletx:any="foo";
lety:any=true;
DefiningtypesYoucandefineyourowntypesinvariousways:
Kind Meaning Example
Objecttype
Representsanobject,withthespecifiedproperties.Propertiesmarkedwith?areoptional.Objectscanalsohaveanindexer(forexample,likeanarray),orcallsignatures.
Objecttypescanbedefinedinline,withaclassorwithaninterfacedeclaration.
letx:{
a:boolean,
b:string,
c?:number,
[i:number]:
string
};
x={
a:true,b:"foo"
};
x[0]="foo";
UniontypeAvalueisassignabletoauniontypeifitisassignabletooneofthespecifiedtypes.Intheexample,itshouldbeastringoranumber.
letx:string|
number;
x="foo";
x=42;
Intersectiontype
Avalueisassignabletoanintersectiontypeifitisassignabletoallspecifiedtypes.
letx:{a:string}
&{b:number}=
{a:"foo",b:42};
EnumtypeAspecialnumbertype,withseveralvaluesdeclared.Thedeclaredmembersgetavalueautomatically,butyoucanalsospecifyavalue.
enumE{
X,
Y=100
}
leta:E=E.X;
Functiontype
Representsafunctionwiththespecifiedargumentsandreturntype.Optionalandrestargumentscanalsobespecified.
letf:(x:string,
y?:boolean)=>
number;
letg:(...xs:
number[])=>
number;
Tupletype Multiplevaluesareplacedinone,asanarray.letx:[string,
number];
X=["foo",42];
UndefinedandnullBydefault,undefinedandnullcanbeassignedtoeverytype.Thus,thecompilercannotgiveyouawarningwhenavaluecanpossiblybeundefinedornull.TypeScript2.0hasintroducedanewmode,calledstrictNullChecks,whichaddstwonewtypes:undefinedandnull.Withthatmode,youdogetwarningsinsuchcases.WewilldiscoverthatmodeinChapter6,AdvancedProgramminginTypeScript.
TypeannotationsTypeScriptcaninfersometypes.ThismeansthattheTypeScriptcompilerknowsthetype,withoutatypeannotation.Ifatypecannotbeinferred,itwilldefaulttoany.Insuchacase,orincasetheinferredtypeisnotcorrect,youhavetospecifythetypesyourself.Thecommondeclarationsthatyoucanannotatearegiveninthefollowingtable:
Location Canitbeinferred? Examples
Variabledeclaration Yes,basedoninitializer
leta:number;
letb=1;
Functionargument
Yes,basedondefaultvalue(secondexample)orwhenpassingthefunctiontoatypedvariableorfunction(thirdexample)
functiona(x:
number){}
functionb(x
=1){}
[1,2].map(
x=>x*2
);
Functionreturntype Yes,basedonreturnstatementsinbody
functiona():
number{}
():number=>
{}
functionc(){
return1;
}
Classmember Yes,basedondefaultvalue
classA{
x:number;
y=0;
}
Interfacemember No
interfaceA{
x:number;
}
YoucansetthecompileroptionnoImplicitAnytogetcompilererrorswhenatypecouldnotbeinferredandfallsbacktoany.Itisadvisedtousethatoptionalways,unlessyouaremigratingaJavaScriptcodebasetoTypeScript.YoucanreadaboutsuchmigrationinChapter10,MigrateJavaScripttoTypeScript.
SummaryInthischapter,youdiscoveredthebasicsofTypeScript.YoushouldnowbefamiliarwiththeprinciplesofTypeScriptandyoushouldunderstandthecodeexampleatthebeginningofthechapter.Younowhavetheknowledgetostartwiththenextchapters,inwhichyouwilllearntwomajorwebframeworks,Angular2andReact.WewillstartwithAngular2inChapter2,AWeatherForecastWidgetwithAngular2.
Chapter2.AWeatherForecastWidgetwithAngular2Inthischapter,we'llcreateasimpleapplicationthatshowsustheweatherforecast.Theframeworkweuse,Angular2,isanewframeworkwrittenbyGoogleinTypeScript.Theapplicationwillshowtheweatherofthecurrentdayandthenext.Inthefollowingscreenshot,youcanseetheresult.WewillexploresomekeyconceptsofAngular,suchasdatabindinganddirectives.
Wewillbuildtheapplicationinthefollowingsteps:
UsingmodulesSettinguptheprojectCreatingthefirstcomponentAddingconditionstothetemplateShowingaforecastCreatingtheforecastcomponentsThemaincomponent
UsingmodulesWewillusemodulesinallapplicationsinthisbook.Modules(alsocalledexternalmodulesandES2015modules)areaconceptofseparatingcodeinmultiplefiles.Everyfileisamodule.Withinthesemodules,youcanusevariables,functions,andclasses(members)exportedbyothermodulesandyoucanmakesomemembersvisibleforothermodules.Touseothermodules,youmustimportthem,andtomakemembersvisible,youneedtoexportthem.Thefollowingexamplewillshowsomebasicusage:
//x.ts
import{one,add,Lorem}from'./y';
console.log(add(one,2));
varlorem=newLorem();
console.log(lorem.name);
//y.ts
exportvarone=1;
exportfunctionadd(a:number,b:number){
returna+b;
}
exportclassLorem{
name="ipsum";
}
Youcanexportdeclarationsbyprefixingthemwiththeexportkeywordorbyprefixingthemwithexportdefault.Adefaultexportshouldbeimporteddifferentlythoughwewillnotusesuchanexportasitcanbeconfusing.Therearevariouswaystoimportafile.Wehaveseenthevariantthatisusedmosttimes,import{a,b,c}from'./d'.Thedotandslashmeanthatthed.tsfileislocatedinthesamedirectory.Youcanuse./x/yand../ztoreferenceafileinasubdirectoryoraparentdirectory.Areferencethatdoesnotstartwithadotcanbeusedtoimportalibrary,suchasAngular.Anotherimportvariantisimport*asefrom'./d'.Thiswillimportallexportsfromd.ts.Theseareavailablease.a,e.b,eisanobjectthatcontainsallexports.
Tokeepcodereadableandmaintainable,itisadvisabletousemultiplesmallfilesinsteadofonebigfile.
SettinguptheprojectWewillquicklysetuptheprojectbeforewecanstartwriting.Wewillusenpmtomanageourdependenciesandgulptobuildourproject.ThesetoolsarebuiltonNodeJS,soitshouldbeinstalledfromnodejs.org.
Firstofall,wemustcreateanewdirectoryinwhichwewillplaceallfiles.Wemustcreateapackage.jsonfileusedbynpm:
{
"name":"weather-widget",
"version":"1.0.0",
"private":true,
"description":""
}
Thepackage.jsonfilecontainsinformationabouttheproject,suchasthename,version,andadescription.ThesefieldsareusedbynpmwhenyoupublishaprojectontheregistryonNPM,whichcontainsalotofopensourceprojects.Wewillnotpublishitthere.Wesettheprivatefieldtotrue,sowecannotaccidentallypublishit.
DirectorystructureWewillseparatetheTypeScriptsourcesfromtheotherfiles.TheTypeScriptfileswillbeaddedinthelibdirectory.Staticfiles,suchasHTMLandCSS,willbelocatedinthestaticdirectory.Thisdirectorycanbeuploadedtoawebserver.Thecompiledsourceswillbewrittentostatic/scripts.WefirstinstallAngularandsomerequirementsofAngularwithnpm.Inaterminal,werunthefollowingcommandintherootdirectoryoftheproject:
npminstallangular2rxjses6-shimreflect-metadatazone.js--save
Theconsolemightshowsomewarningsaboutunmetpeerdependencies.ThesewillprobablybecausedbyaminorversionmismatchbetweenAngularandoneofitsdependencies.Youcanignorethesewarnings.
ConfiguringTypeScriptTypeScriptcanbeconfiguredusingatsconfig.jsonfile.Wewillplacethatfileinthelibdirectory,asallourfilesarelocatedthere.WespecifytheexperimentalDecoratorsandemitDecoratorMetadataoptions,asthesearenecessaryforAngular:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"experimentalDecorators":true,
"emitDecoratorMetadata":true,
"lib":["es2015","dom"]
}
}
ThetargetoptionspecifiestheversionofJavaScriptofthegeneratedcode.Currentbrowserssupportes5.TypeScriptwillcompilenewerJavaScriptfeatures,suchasclasses,toanes5equivalent.Withtheliboption,wecanspecifytheversionoftheJavaScriptlibrary.Weusethelibrariesfromes2015,theversionafteres5.Sincetheselibrariesmightnotbeavailableinallbrowsers,wewilladdapolyfillforthesefeatureslateron.WealsoincludethelibrariesfortheDOM,whichcontainsfunctionssuchasdocument.createElementanddocument.getElementById.
BuildingthesystemWithgulp,itiseasytocompileaprograminmultiplesteps.Formostwebapps,multiplestepsareneeded:compilingTypeScript,bundlingmodules,andfinallyminifyingallcode.Inthisapplication,weneedtodoallofthesesteps.
Gulpstreamssourcefilesthroughaseriesofplugins.Thesepluginscan(justlikegulpitself)beinstalledusingnpm:
npminstallgulp--global
npminstallgulpgulp-typescriptgulp-sourcemapsgulp-uglifysmall--save-dev
Tip
The--globalflagwillinstallthedependencygloballysuchthatyoucancallgulpfromaterminal.The--save-devflagwilladdthedependencytothedevDependencies(developmentdependencies)sectionofthepackage.jsonfile.Use--savetoaddaruntimedependency.
Weusethefollowingpluginsforgulp:
Thegulp-typescriptplugincompilesTypeScripttoJavaScriptThegulp-uglifyplugincanminifyJavaScriptfilesThesmallplugincanbundleexternalmodulesThegulp-sourcemapspluginimprovesthedebuggingexperiencewithsourcemaps
Wewillcreatetwotasks,onethatcompilesthesourcestoadevelopmentbuildandanotherthatcancreateareleasebuild.Thedevelopmentbuildwillhavesourcemapsandwillnotbeminified,whereasthereleasebuildwillbeminifiedwithoutsourcemaps.Minifyingtakessometimesowedonotdothatonthedebugtask.Creatingsourcemapsinthereleasetaskispossibletoo,butgeneratingthesourcemapisslowsowewillnotdothat.
Wewritethesetasksingulpfile.jsintherootoftheproject.Thesecondtaskistheeasiesttowrite,asitonlyusesoneplugin.Thetaskwilllooklikethis:
vargulp=require('gulp');
varuglify=require('gulp-uglify');
gulp.task('release',['compile'],function(){
returngulp.src('static/scripts/scripts.js')
.pipe(uglify())
.pipe(gulp.dest('static/scripts'));
});
Thegulp.taskcallwillregisteratasknamedrelease,whichwilltakestatic/scripts/scripts.js(whichwillbecreatedbythecompiletask),runuglify(atoolthatminifiesJavaScript)onit,andthensaveitinthesamedirectoryagain.Thistaskdependsonthecompiletask,meaningthatthecompiletaskwillberunbeforethisone.
Thefirsttask,compile,ismorecomplicated.ThetaskwilltranspileTypeScript,andbundlethefileswiththeexternallibraries.
First,wemustloadsomeplugins:
vargulp=require('gulp');
vartypescript=require('gulp-typescript');
varsmall=require('small').gulp;
varsourcemaps=require('gulp-sourcemaps');
varuglify=require('gulp-uglify');
WeloadtheconfigurationofTypeScriptinthetsconfig.jsonfile:
vartsProject=typescript.createProject('lib/tsconfig.json');
Now,wecanfinallywritethetask.First,weloadallsourcesandcompilethemusingtheTypeScriptcompiler.Afterthat,webundlethesefiles(includingAngular,storedundernode_modules,usingsmall):
gulp.task('compile',function(){
returngulp.src('lib/**/*.ts')
.pipe(sourcemaps.init())
.pipe(typescript(tsProject))
.pipe(small('index.js',{
externalResolve:['node_modules'],
globalModules:{
"crypto":{
standalone:"undefined"
}
}
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('static/scripts'));
});
gulp.task('release',['compile'],function(){
returngulp.src('static/scripts/scripts.js')
.pipe(uglify())
.pipe(gulp.dest('static/scripts'));
});
gulp.task('default',['compile']);
Thistaskcompilesourprojectandsavestheresultasstatic/scripts/scripts.js.Thesourcemaps.init()andsourcemaps.write('.')functionshandlethecreationofsourcemaps,whichwillimprovethedebuggingexperience.
TheHTMLfileThemainfileofourapplicationistheHTMLfile,static/index.html.Thisfilewillreferenceour(compiled)scriptsandstylesheet:
<!DOCTYPEHTML>
<html>
<head>
<title>Weather</title>
<linkrel="stylesheet"href="style.css"/>
</head>
<body>
<divid="wrapper">
<weather-widget>Loading..</weather-widget>
</div>
<scriptsrc="scripts/index.js"type="text/javascript"></script>
</body>
</html>
Theweather-widgettagwillbeinitializedbyAngular.Wewilladdsomefancystylesinstatic/style.css:
body{
font-family:'SegoeUI',Tahoma,Geneva,Verdana,sans-serif;
font-weight:100;
}
h1,h2,h3{
font-weight:100;
margin:0;
padding:0;
color:#57BEDE;
}
#wrapper{
position:absolute;
left:0;
right:0;
top:0;
width:450px;
margin:10%auto;
}
a:link,a:visited{
color:#57BEDE;
text-decoration:underline;
}
a:hover,a:active{
color:#44A4C2;
}
.clearfix{
clear:both;
}
CreatingthefirstcomponentAngularisbasedoncomponents.ComponentsarebuiltwithothercomponentsandnormalHTMLtags.Ourapplicationwillhavethreecomponents:theforecastpage,theaboutpage,andthewholewidget.Thewidgetitself,whichisreferencedintheHTMLpage,willusetheothertwowidgets.
ThewidgetwillshowtheAboutpageinthethirdtab,asyoucanseeinthefollowingscreenshot:
Theforecastcomponentisshowninthefirsttabofthefollowingscreenshot.Wewillcreatetheforecastandthewidgetlaterinthischapter.
ThetemplateAcomponentisaclassdecoratedwithsomemetadata.Decoratorsarefunctionsthatcanmodifyaclassordecorateitwithsomemetadata.Asimplecomponentthatdoesnothaveanyinteractionwilllooklikethis:
import{Component}from"angular2/core";
@Component({
selector:"about-page",
template:`
<h2>About</h2>
ThiswidgetshowstheweatherforecastofUtrecht.
Thenext24hoursareshownunder'Today'andtheforecastof24-48
hoursaheadunder'Tomorrow'.
`
})
exportclassAbout{
}
Tip
Asaconvention,youcanalwayschooseselectornameswithadash(-).Youcanthenidentifycomponentsbythedash.NormalHTMLtagswillneverhavenameswithadash.
Thiscomponentwillbetheaboutpageselectorofourapplication.Wewillmodifyitinthenextsessions.Wewilluseonefilepercomponent,sowesavethisaslib/about.ts.
TestingWecantestthecomponentbycallingthebootstrapfunction.Wecreateanewfile,lib/index.ts,whichwillstarttheapplication:
import"zone.js";
import"rxjs";
import"reflect-metadata";
import"es6-shim";
import{bootstrap}from"angular2/platform/browser";
import{About}from"./about";
bootstrap(About).catch(err=>console.error(err));
Tip
The.catchsectionwillshowerrorsintheconsole.Ifyoudonotincludethatcall,youwillnotseethoseerrorsandthatcanbeprettyfrustrating.
Wemustchangetheweather-widgettaginstatic/index.htmltoanabout-pagetag.Now,wecanrungulpandopenindex.htmlinabrowsertoseetheresults.
Atthetimeofwritingthis,whenyourunthiscommand,yougetanerrorwhensayingthatthetypedefinitionofzone.jsisincorrect.Youcanignorethiserrorasitisabugofzone.js.
Tip
Testearly
It'salwaysagoodideatotestduringdevelopment.Ifyoutestafterwritingalotofcode,youwilldiscoverissueslate,anditwilltakemoreworktorepairthem.Everytimethatyouwanttotesttheproject,youmustfirstrungulpandthenopenorrefreshindex.html.
InteractionsWecanaddaninteractioninsidetheclassbody.Wemustusebindingstoconnectthetemplatetodefinitionsinthebody.Therearethreedifferentbindings:
One-wayvariablebindingOne-wayeventlistenerTwo-waybinding
Aone-waybindingwillconnecttheclassbodyandtemplateinonedirection.Incaseofavariable,changesofthevariablewillupdatethetemplate,butthetemplatecannotupdatethevariable.Atemplatecanonlysendaneventtotheclass.Incaseofatwo-waybinding,achangeofthevariablechangesthetemplateandachangeinthetemplatewillchangethevariable.Thisisusefulforthevalueofaninputelement,forexample.Wewilltakealookatone-waybindingsinthenextsection.
One-wayvariablebindingInthefirstattemptoftheaboutpage,thelocation(Utrecht)ishardcoded.Inthefinalapplication,wewanttochooseourownlocation.Thefirststepwewilltakeistoaddapropertytotheclassthatcontainsthelocation.Usingaone-waybinding,wewillreferencethatvalueinthetemplate.Aone-wayvariablebindingisdenotedwithbracketsinsideattributesanddoublecurlybracketsinsidetext:
import{Component}from"angular2/core";
@Component({
selector:"about-page",
template:`
<h2>About</h2>
Thiswidgetshowstheweatherforecastof
<a[href]="'https://maps.google.com/?q='+encodedLocation">
{{location}}
</a>
Thenext24hoursareshownunder'Today'andtheforecastof24-48
hoursaheadunder'Tomorrow'.
`
})
exportclassAbout{
location="Utrecht";
getencodedLocation(){
returnencodeURIComponent(this.location);
}
}
Tip
Atthetimeofwritingthis,templatesaren'tcheckedbyTypeScript.Makesurethatyouwritethecorrectnamesofthevariables.Variablesshouldnotbeprefixedbythis.,likeyouwoulddoinclassmethods.
Youcanaddanexpressioninsuchbindings.Inthisexample,thebindingofthehrefattributedoesstringconcatenation.However,thesubsetofexpressionsislimited.Youcanaddmorecomplexcodeinsidegettersintheclass,asdonewithencodedLocation.
Tip
Youcanalsouseadifferentgetter,whichwouldencodethelocationandconcatenateitwiththeGoogleMapsURL.
EventlistenersEventbindingscanconnectaneventemitterofatagorcomponenttoamethodofafunction.Suchbindingisdenotedwithparenthesisinthetemplate.Wewilladdashow-morebuttontoourapplication:
import{Component}from"angular2/core";
@Component({
selector:"about-page",
template:`
<h2>About</h2>
Thiswidgetshowstheweatherforecastof
<a[href]="'https://maps.google.com/?q='+encodedLocation">
{{location}}
</a>.
Thenext24hoursareshownunder'Today'andtheforecastof24-48
hoursaheadunder'Tomorrow'.
<br/>
<ahref="javascript:;"(click)="show()">Showmore</a>
<ahref="javascript:;"(click)="hide()">Showless</a>
`
})
exportclassAbout{
location="Utrecht";
collapsed=true;
show(){
this.collapsed=false;
}
hide()
{
this.collapsed=true;
}
getencodedLocation(){
returnencodeURIComponent(this.location);
}
}
Theshow()orhide()functionwillbecalledwhenoneoftheshoworhidelinksisclickedon.
AddingconditionstothetemplateTheeventhandlerintheprevioussectionsetsthepropertycollapsedtofalsebutthatdoesnotmodifythetemplate.Innormalcode,wewouldhavewrittenif(this.collapsed){...}.Intemplates,wecannotusethat,butwecanusengIf.
DirectivesAdirectiveisanextensiontonormalHTMLtagsandattributes.Itcandefinecustombehavior.Acustomcomponent,suchastheAboutpage,canbeseenasadirectivetoo.ThengIfconditionisabuilt-indirectiveinAngular.Itisacustomattributethatdisplaysthecontentifthespecifiedvalueistrue.
ThetemplatetagIfapieceofacomponentneedstobeshownavariableanamountoftimes,youcanwrapitinatemplatetag.UsingthengIf(orngFor)directive,youcancontrolhowoftenitisshown(incaseofngIf,onceorzerotimes).Thetemplatetagwilllooklikethis:
<template[ngIf]="collapsed">
<div>Content</div>
</template>
Youcanabbreviatethisasfollows:
<div*ngIf="collapsed">Content</div>
Itisadvisedtousetheabbreviatedstyle,butit'sgoodtorememberthatitisshorthandforthetemplatetag.
ModifyingtheabouttemplateSincengIfisabuilt-indirective,itdoesn'thavetobeimported.Customdirectivesneedtobeimported.Wewillseeanexampleofusingcustomcomponentslaterinthischapter.Inthetemplate,wecanuse*ngIfnow.Thetemplatewillthuslooklikethis:
template:`
<h2>About</h2>
Thiswidgetshowstheweatherforecastof
<a[href]="'https://maps.google.com/?q='+encodedLocation">
{{location}}
</a>.
Thenext24hoursareshownunder'Today'andtheforecastof24-48
hoursaheadunder'Tomorrow'.
<br/>
<a*ngIf="collapsed"href="javascript:;"(click)="show()">Show
more</a>
<div*ngIf="!collapsed">
Theforecastusesdatafrom<a
href="http://openweathermap.org">OpenWeatherMap</a>.
<br/>
<ahref="javascript:;"(click)="hide()">Hide</a>
</div>
`
})
Theclassbodydoesnothavetobechanged.Asyoucansee,youcanuseexpressionsinthe*ngIfbindings,whichisnotsurprisingasitisashorthandforone-wayvariablebindings.
UsingthecomponentinothercomponentsWecanusetheabout-pagecomponentinothercomponents,asifitwasanormalHTMLtag.Butthecomponentisstillboring,asitwillalwayssaythatitshowstheweatherbroadcastofUtrecht.Wecanmarkthelocationpropertyasaninput.Afterthat,locationisanattributethatwecansetfromothercomponents.Itisevenpossibletobinditasaone-waybinding.TheInputdecorator,whichweareusinghere,needstobeimportedjustlikeComponent:
import{Component,Input}from"angular2/core";
@Component({
...
})
exportclassAbout{
@Input()
location:string="Utrecht";
collapsed=true;
show(){
this.collapsed=false;
}
hide(){
this.collapsed=true;
}
getencodedLocation(){
returnencodeURIComponent(this.location);
}
}
ShowingaforecastWestillhavenotshownaforecastyet.Wewillusedatafromopenweathermap(http://www.openweathermap.org).Youcancreateanaccountontheirwebsite.Withyouraccount,youcanrequestanAPItoken.Youneedthetokentorequesttheforecast.Afreeaccountislimitedto60requestspersecondand50,000requestsperday.
WesavetheAPItokeninaseparatefile,lib/config.ts:
exportconstopenWeatherMapKey="your-token-here";
exportconstapiURL="http://api.openweathermap.org/data/2.5/";
Tip
Addconstantstoaseparatefile
Whenyouaddconstantsinseparateconfigurationfiles,youcaneasilychangethemandyourcodeismorereadable.Thisgivesyoubettermaintainablecode.
UsingtheAPIWewillcreateanewfile,lib/api.ts,thatwillsimplifydownloadingdatafromopenweathermap.TheAPIusesURLssuchashttp://api.openweathermap.org/data/2.5/forecast?mode=json&q=Utrecht,NL&appid=your-token-here.WewillcreateafunctionthatwillbuildthefullURLoutofforecast?mode=json&q=Utrecht,NL.Thefunctionmustcheckwhetherthepathalreadycontainsaquestionmark.Ifso,itmustadd&appid=,otherwise?appid=:
import{openWeatherMapKey,apiURL}from"./config";
exportfunctiongetUrl(path:string){
leturl=apiURL+path;
if(path.indexOf("?")===-1){
url+="?";
}else{
url+="&";
}
url+="appid="+openWeatherMapKey;
returnurl;
}
Tip
Writesmallfunctions
Smallfunctionsareeasytoreuse.Thisreducestheamountofcodeyouneedtowrite.Thesameappliestocomponents—smallcomponentsareeasytoreuse.
TypingtheAPIYoucanopentheURLintheprevioussectiontogetalookatthedatayouget.WewillwriteaninterfaceforthepartoftheAPIthatwewilluse:
exportinterfaceForecastResponse{
city:{
name:string;
country:string;
};
list:ForecastItem[];
}
exportinterfaceForecastItem{
dt:number;
main:{
temp:number
};
weather:{
main:string,
description:string
};
}
Tip
JSDoccomments
YoucanadddocumentationforinterfacesandtheirpropertiesbyaddingaJSDoccommentbeforeit:
/***Documentationhere*/
CreatingtheforecastcomponentAsaquickrecap,theforecastwidgetwilllooklikethis:
Whatpropertiesdoestheclassneed?Thetemplatewillneedforecastdataofthecurrentdayorthenextday.ThecomponentcanshowtheweatherofTodayandTomorrow,sowewillalsoneedapropertyforthat.Forfetchingtheforecast,wealsoneedthelocation.Toshowtheloadingstateinthetemplate,wewillalsostorethatintheclass.Thiswillresultinthefollowingclass,inlib/forecast.ts:
import{Component,Input}from"angular2/core";
import{ForecastResponse}from"./api";
exportinterfaceForecastData{
date:string;
temperature:number;
main:string;
description:string;
}
enumState{
Loading,
Refreshing,
Loaded,
Error
}
@Component({
selector:"weather-forecast",
template:`...`
})
exportclassForecast{
temperatureUnit="degreesCelsius";
@Input()
tomorrow=false;
@Input()
location="Utrecht";
data:ForecastData[]=[];
state=State.Loading;
}
Tip
Testing
Youcantestthiscomponentbyadjustingthetaginindex.htmlandbootstrappingtherightcomponentinindex.ts.Rungulptocompilethesourcesandopenthewebbrowser.
TemplatesThetemplateusesthengFordirectivetoiterateoverthedataarray:
import{Component,Input}from"angular2/core";
import{ForecastResponse}from"./api";
...
@Component({
selector:"weather-forecast",
template:`
<span*ngIf="loading"class="state">Loading...</span>
<span*ngIf="refreshing"class="state">Refreshing...</span>
<a*ngIf="loaded||error"href="javascript:;"(click)="load()"
class="state">Refresh</a>
<h2>{{tomorrow?'Tomorrow':'Today'}}'sweatherin{{location}}
</h2>
<div*ngIf="error">Failedtoloaddata.</div>
<ul>
<li*ngFor="#itemofdata">
<divclass="item-date">{{item.date}}</div>
<divclass="item-main">{{item.main}}</div>
<divclass="item-description">{{item.description}}</div>
<divclass="item-temperature">
{{item.temperature}}{{temperatureUnit}}
</div>
</li>
</ul>
<divclass="clearfix"></div>
`,
Usingthestylesproperty,wecanaddniceCSSstyles,asshownhere:
styles:[
`.state{
float:right;
margin-top:6px;
}
ul{
margin:0;
padding:0015px;
list-style:none;
width:100%;
overflow-x:scroll;
white-space:nowrap;
}
li{
display:inline-block;
margin-right:15px;
width:170px;
white-space:initial;
}
.item-date{
font-size:15pt;
color:#165366;
margin-right:10px;
display:inline-block;
}
.item-main{
font-size:15pt;
display:inline-block;
}
.item-description{
border-top:1pxsolid#44A4C2;
width:100%;
font-size:11pt;
}
.item-temperature{
font-size:11pt;
}`
]
})
Intheclassbody,weaddthegetterswhichweusedinthetemplate:
exportclassForecast{
...
state=State.Loading;
getloading(){
returnthis.state===State.Loading;
}
getrefreshing(){
returnthis.state===State.Refreshing;
}
getloaded(){
returnthis.state===State.Loaded;
}
geterror(){
returnthis.state===State.Error;
}
...
}
Tip
Enums
Enumsarejustnumberswithnamesattachedtothem.It'smorereadabletowriteState.Loadedthan2,buttheymeanthesameinthiscontext.
Asyoucansee,thesyntaxofngForis*ngFor="#variableofarray".Theenumcannotbereferencedfromthetemplate,soweneedtoaddgettersinthebodyoftheclass.
DownloadingtheforecastTodownloaddatafromtheInternetinAngular,weneedtogettheHTTPservice.WeneedtosettheviewProviderssectionforthat:
import{Component,Input}from"angular2/core";
import{Http,Response,HTTP_PROVIDERS}from"angular2/http";
import{getUrl,ForecastResponse}from"./api";
...
@Component({
selector:"weather-forecast",
viewProviders:[HTTP_PROVIDERS],
template:`...`,
styles:[...]
})
exportclassForecast{
constructor(privatehttp:Http){
}
...
AngularwillinjecttheHttpserviceintotheconstructor.
Tip
Byincludingprivateorpublicbeforeanargumentoftheconstructor,thatargumentwillbecomeapropertyoftheclass,initializedbythevalueoftheargument.
Wewillnowimplementtheloadfunction,whichwilltrytodownloadtheforecastonthespecifiedlocation.Thefunctioncanalsousecoordinatesasalocation,writtenasCoordinateslatlon,wherelatandlonarethecoordinatesasshownhere:
privateload(){
letpath="forecast?mode=json&";
conststart="coordinate";
if(this.location&&
this.location.substring(0,
start.length).toLowerCase()===start){
constcoordinate=this.location.split("");
path+=`lat=${parseFloat(coordinate[1])}&lon=${
parseFloat(coordinate[2])}`;
}else{
path+="q="+this.location;
}
this.state=this.state===State.Loaded?
State.Refreshing:State.Loading;
this.http.get(getUrl(path))
.map(response=>response.json())
.subscribe(res=>
this.update(<ForecastResponse>res),()
=>this.showError());
};
Tip
Threekindsofvariables
Youcandefinevariableswithconst,let,andvar.Avariabledeclaredwithconstcannotbemodified.Variablesdeclaredwithconstorletareblock-scopedandcannotbeusedbeforetheirdefinition.Avariabledeclaredwithvarisfunctionscopedandcanbeusedbeforeitsdefinition.Suchvariablecangiveunexpectedbehavior,soit'sadvisedtouseconstorlet.
ThefunctionwillfirstcalculatetheURL,thensetthestateandfinallyfetchthedataandgetreturnsanobservable.Anobservable,comparabletoapromise,issomethingthatcontainsavaluethatcanchangelateron.Likewitharrays,youcanmapanobservabletoadifferentobservable.Subscriberegistersacallback,whichiscalledwhentheobservableischanged.Thisobservablechangesonlyonce,whenthedataisloaded.Ifsomethinggoeswrong,thesecondcallbackwillbecalled.
Tip
Lambdaexpressions(inlinefunctions)
Thefatarrow(=>)createsanewfunction.It'salmostequaltoafunctiondefinedwiththefunctionkeyword(function(){return...}),butitisscopedlexically,whichmeansthatthisreferstothevalueofthisoutsidethefunction.x=>expressionisashorthandfor(x)=>expression,whichisashorthandfor(x)=>{returnexpression;}.TypeScriptwillautomaticallyinferthetypeoftheargumentbasedonthesignatureofmapandsubscribe.
Asyoucansee,thisfunctionusestheupdateandshowErrorfunctions.TheupdatefunctionstorestheresultsoftheopenweathermapAPI,andshowErrorisasmallfunctionthatsetsthestatetoState.Error.SincetemperaturesoftheAPIareexpressedinKelvin,wemustsubstract273togetthevalueinCelsius:
fullData:ForecastData[]=[];
data:ForecastData[]=[];
privateformatDate(date:Date){
returndate.getHours()+":"
+date.getMinutes()+":"
+date.getSeconds();
}
privateupdate(data:ForecastResponse){
if(!data.list){
this.showError();
return;
}
this.fullData=data.list.map(item=>({
date:this.formatDate(newDate(item.dt*1000)),
temperature:Math.round(item.main.temp-273),
main:item.weather[0].main,
description:item.weather[0].description
}));
this.filterData();
this.state=State.Loaded;
}
privateshowError(){
this.data=[];
this.state=State.Error;
}
privatefilterData(){
conststart=this.tomorrow?8:0;
this.data=this.fullData.slice(start,start+8);
}
ThefilterDatamethodwillfiltertheforecastbasedonwhetherwewanttoseetheforecastoftodayortomorrow.Openweathermaphasoneforecastper3hours,so8perday.Theslicefunctionwillreturnasectionofthearray.fullDatawillcontainthefullforecast,sowecaneasilyshowtheforecastoftomorrow,ifwehavealreadyshowntoday.
Tip
Changedetection
Angularwillautomaticallyreloadthetemplatewhensomepropertyischanged,there'snoneedtoinvalidateanything(asC#developersmightexpect).Thisiscalledchangedetection.
Wealsowanttorefreshdatawhenthelocationischanged.Iftomorrowischanged,wedonotneedtodownloadanydata,becausewecanjustuseadifferentsectionofthefullDataarray.Todothat,wewillusegettersandsetters.Inthesetter,wecandetectchanges:
private_tomorrow=false;
@Input()
settomorrow(value){
if(this._tomorrow===value)return;
this._tomorrow=value;
this.filterData();
}
gettomorrow(){
returnthis._tomorrow;
}
private_location:string;
@Input()
setlocation(value){
if(this._location===value)return;
this._location=value;
this.state=State.Loading;
this.data=[];
this.load();
}
getlocation(){
returnthis._location;
}
Adding@OutputTheresponseofOpenweathermapcontainsthenameofthecity.Wecanusethistosimulatecompletionlateron.Wewillcreateaneventemitter.Othercomponentscanlistentotheeventandupdatethelocationwhentheeventistriggered.Thewholecodewilllooklikethiswithfinalchangeshighlighted:
import{Component,Input,Output,EventEmitter}from"angular2/core";
import{Http,Response,HTTP_PROVIDERS}from"angular2/http";
import{getUrl,ForecastResponse}from"./api";
interfaceForecastData{
date:string;
temperature:number;
main:string;
description:string;
}
enumState{
Loading,
Refreshing,
Loaded,
Error
}
@Component({
selector:"weather-forecast",
viewProviders:[HTTP_PROVIDERS],
template:`
<span*ngIf="loading"class="state">Loading...</span>
<span*ngIf="refreshing"class="state">Refreshing...</span>
<a*ngIf="loaded||error"href="javascript:;"(click)="load()"
class="state">Refresh</a>
<h2>{{tomorrow?'Tomorrow':'Today'}}'sweatherin{{location}}
</h2>
<div*ngIf="error">Failedtoloaddata.</div>
<ul>
<li*ngFor="#itemofdata">
<divclass="item-date">{{item.date}}</div>
<divclass="item-main">{{item.main}}</div>
<divclass="item-description">{{item.description}}</div>
<divclass="item-temperature">
{{item.temperature}}{{temperatureUnit}}
</div>
</li>
</ul>
<divclass="clearfix;"></div>
`,
styles:[
`.state{
float:right;
margin-top:6px;
}
ul{
margin:0;
padding:0015px;
list-style:none;
width:100%;
overflow-x:scroll;
white-space:nowrap;
}
li{
display:inline-block;
margin-right:15px;
width:170px;
white-space:initial;
}
.item-date{
font-size:15pt;
color:#165366;
margin-right:10px;
display:inline-block;
}
.item-main{
font-size:15pt;
display:inline-block;
}
.item-description{
border-top:1pxsolid#44A4C2;
width:100%;
font-size:11pt;
}
.item-temperature{
font-size:11pt;
}`
]
})
exportclassForecast{
constructor(privatehttp:Http){
}
temperatureUnit="degreesCelsius";
private_tomorrow=false;
@Input()
settomorrow(value){
if(this._tomorrow===value)return;
this._tomorrow=value;
this.filterData();
}
gettomorrow(){
returnthis._tomorrow;
}
private_location:string;
@Input()
setlocation(value){
if(this._location===value)return;
this._location=value;
this.state=State.Loading;
this.data=[];
this.load();
}
getlocation(){
returnthis._location;
}
fullData:ForecastData[]=[];
data:ForecastData[]=[];
state=State.Loading;
getloading(){
returnthis.state===State.Loading;
}
getrefreshing(){
returnthis.state===State.Refreshing;
}
getloaded(){
returnthis.state===State.Loaded;
}
geterror(){
returnthis.state===State.Error;
}
@Output()
correctLocation=newEventEmitter<string>(true);
privateformatDate(date:Date){
returndate.getHours()+":"+date.getMinutes()+date.getSeconds();
}
privateupdate(data:ForecastResponse){
if(!data.list){
this.showError();
return;
}
constlocation=data.city.name+","+data.city.country;
if(this._location!==location){
this._location=location;
this.correctLocation.next(location);
}
this.fullData=data.list.map(item=>({
date:this.formatDate(newDate(item.dt*1000)),
temperature:Math.round(item.main.temp-273),
main:item.weather[0].main,
description:item.weather[0].description
}));
this.filterData();
this.state=State.Loaded;
}
privateshowError(){
this.data=[];
this.state=State.Error;
}
privatefilterData(){
conststart=this.tomorrow?8:0;
this.data=this.fullData.slice(start,start+8);
}
privateload(){
letpath="forecast?mode=json&";
conststart="coordinate";
if(this.location&&this.location.substring(0,
start.length).toLowerCase()===start){
constcoordinate=this.location.split("");
path+=`lat=${parseFloat(coordinate[1])}&lon=${
parseFloat(coordinate[2])}`;
}else{
path+="q="+this.location;
}
this.state=this.state===State.Loaded?State.Refreshing:
State.Loading;
this.http.get(getUrl(path))
.map(response=>response.json()))
.subscribe(res=>this.update(<ForecastResponse>res),()=>
this.showError());
}
}
Tip
Thegeneric(typeargument)innewEventEmitter<string>()meansthatthecontentsofaneventwillbeastring.Ifthegenericisnotspecified,itdefaultsto{},anemptyobjecttype,whichmeansthatthereisnocontent.Inthiscase,wewanttosendthenewlocation,whichisastring.
ThemaincomponentAsyoucanseeinthescreenshotintheintroductionofthischapter,thiscomponentshouldhaveatextbox,abutton,andthreetabs.Underthetabs,thesecomponentwillshowtheforecastortheAboutpage.
UsingourothercomponentsWecanuseourcomponentsthatwehavealreadywrittenbyaddingthemtothedirectivessectionandusingtheirtagnamesinthetemplate.
Two-waybindingsTogetthevalueoftheinputbox,weneedtwo-waybindings.WecanusethengModeldirectiveforthat.Thesyntaxcombinesthesyntaxesofthetwoone-waybindings:[(ngModel)]="property".Thedirectiveisagainabuilt-inone,sowedon'thavetoimportit.
Usingthistwo-waybinding,wecanautomaticallyupdatetheweatherwidgetaftereverykeypress.Thatwouldcausealotofrequeststotheserver,andespeciallyonslowconnections,that'snotdesired.
Topreventtheseissues,wewilladdtwoseparateproperties.ThepropertylocationwillcontainthecontentoftheinputandactiveLocationwillcontainthelocation,whichisbeingshown.
ListeningtooureventWecanlistentoourevent,justlikewedidwithotherevents.Wecanaccesstheeventcontentwith$event.Suchalistenerwilllooklike(correct-location)="correctLocation($event).Whentheserverrespondswiththeforecast,italsoprovidesthenameofthelocation.Iftheuserhadasmalltypointhename,theresponsewillcorrectthat.Thiseventwillbefiredinsuchacaseandthenamewillbecorrectedintheinputbox.
GeolocationAPIBecauseourforecastwidgetsupportscoordinates,wecanusethegeolocationAPItosettheinitiallocation.ThatAPIcangivethecoordinateswherethedeviceislocated(roughly).Lateron,wewillusethisAPItosetthewidgettothecurrentlocationwhenthepageloadsasshownhere:
navigator.geolocation.getCurrentPosition(position=>{
constlocation=`Coordinate${position.coords.latitude}${
position.coords.longitude}`;
this.location=location;
this.activeLocation=location;
});
Tip
Templatestring
Templatestrings,nottobeconfusedwithAngulartemplates,arestringswrappedinbackticks(`).Thesestringscanbemultilineandcancontainexpressionsbetween${and}.
ComponentsourcesAsusual,westartbyimportingAngular.Wealsohavetoimportthetwocomponentswehavealreadywritten.Weuseanenumerationagaintostorethestateofthecomponent:
import{Component}from"angular2/core";
import{Forecast}from"./forecast";
import{About}from"./about";
enumState{
Today,
Tomorrow,
About
}
Thetemplatewillusethetwo-waybindingontheinputelement:
@Component({
selector:"weather-widget",
directives:[Forecast,About],
template:`
<input[(ngModel)]="location"(keyup.enter)="clickGo()"
(blur)="clickGo()"/>
<button(click)="clickGo()">Go</button>
<divclass="tabs">
<ahref="javascript:;"[class.selected]="selectedTab===0"
(click)="selectTab(0)">Today</a>
<ahref="javascript:;"[class.selected]="selectedTab===1"
(click)="selectTab(1)">Tomorrow</a>
<ahref="javascript:;"[class.selected]="selectedTab===2"
(click)="selectTab(2)">About</a>
</div>
<divclass="content"[class.is-dirty]="isDirty"*ngIf="selectedTab===
0||selectedTab===1">
<weather-forecast[location]="activeLocation"
[tomorrow]="selectedTab===1"(correctLocation)="correctLocation($event)"
/>
</div>
<divclass="content"*ngIf="selectedTab===2">
<about-page[location]="activeLocation"/>
</div>
`,
Bindingtoclass.selectedmeansthattheelementwillhavetheselectedclassiftheboundvalueistrue.Afterthetemplate,wecanaddsomestylesasshownhere:
styles:[
`.tabs>a{
display:inline-block;
padding:5px;
margin-top:5px;
border:1pxsolid#57BEDE;
border-bottom:0pxnone;
text-decoration:none;
}
.tabs>a.selected{
background-color:#57BEDE;
color:#fff;
}
.content{
border-top:5pxsolid#57BEDE;
}
.is-dirty{
opacity:0.4;
background-color:#ddd;
}`
]
})
Intheconstructor,wecanusethegeolocationAPItogetthecoordinatesofthecurrentposition:
exportclassWidget{
constructor(){
navigator.geolocation.getCurrentPosition(position=>{
constlocation=`Coordinate${position.coords.latitude}${
position.coords.longitude}`;
this.location=location;
this.activeLocation=location;
});
}
location:string="Utrecht,NL";
activeLocation:string="Utrecht,NL";
getisDirty(){
returnthis.location!==this.activeLocation;
}
clickGo(){
this.activeLocation=this.location;
}
correctLocation(location:string){
if(!this.isDirty)this.location=location;
this.activeLocation=location;
}
selectedTab=0;
selectTab(index:number){
this.selectedTab=index;
}
}
SummaryInthischapter,wecreatedanapplicationwithAngular2.WeexploredAngular2anduseditsdirectivesandbindingsinourcomponents.WealsousedanonlineAPI.YoushouldnowbeabletobuildsmallAngularapplications.Inthenextchapter,wewillbuildamorecomplexapplicationinAngular,whichwillalsouseitsownserver.
Chapter3.Note-TakingAppwithaServerInthischapter,wewillcreateaclient-serverapp.TheclientwillbewrittenusingAngular2andtheserverwillbewrittenusingNodeJSandMongoDB.WecanuseTypeScriptonbothsidesandwewillseehowwecanreusecodebetweenthem.
Theapplicationcanbeusedtotakenotes.Wewillimplementaloginpageandbasic,Create,Read,Update,andDelete(CRUD)operationsforthenotes.
Inthischapter,wewillcoverthefollowingtopics:
SettinguptheprojectstructureGettingstartedwithNodeJSUnderstandingthestructuraltypesystemAddingauthenticationTestingtheAPIAddingCRUDoperationsWritingtheclientsideRunningtheapplication
SettinguptheprojectstructureFirst,wehavetosetuptheproject.Thedifferencewiththepreviouschapteristhatwenowhavetobuildtwoapplications—theclientsideandtheserverside.Thiscausessomedifferenceswiththeprevioussetup.
DirectoriesWewillagainplaceourTypeScriptsourcesinthelibdirectory.Inthatdirectory,wewillcreatefoursubdirectories:client,server,shared,andtypings.Thelib/clientdirectorywillcontaintheclient-sideapplicationandthelib/serverdirectorywillcontaintheservercode.Codesthatcanbeusedbyboththeserverandtheclientwillgoinlib/shared.Lastbutnotleast,lib/typingswillcontaintypedefinitionsforsomedependencies,includingNodeJS.
ConfiguringthebuildtoolInlib,wecreateatsconfig.jsonfilethatwillcontainsomeconfigurationforTypeScript.Wewanttocompiletheserver-sidecodetoes2015,sowecanusesomenewfeaturesofTypeScriptandJavaScript.Theclientside,however,mustbecompiledtoes5forbrowsersupport.Inthetsconfigfile,wewillspecifyes2015astargetandoverrideitinthegulpfile.Wecanalsospecifytheversionofthedefaultlibrarythatwewanttouse.Weneedes2015anddom.ThefirstcontainstherecentclassesandfunctionsfromJavaScript,suchasMapandObject.assign:
{
"compilerOptions":{
"target":"es6",
"module":"commonjs",
"experimentalDecorators":true,
"emitDecoratorMetadata":true,
"lib":["es2015","dom"]
}
}
Theliboptionwillonlymakethetypesfornewclassesandfunctionsavailable.Atruntime,thesemightnotbepresent.Weincludeapolyfill,es6-shim,tomakesurethatthesewillalwaysbeavailable.
Thegulpfile,locatedintherootoftheproject,iscomparabletotheconfigurationofthepreviouschapter.Wecaninstallallnecessarydependencies,includingruntimedependencies,usingnpm:
npminit
npminstallgulpgulp-typescriptsmallgulp-sourcemapsmerge2gulp-concat
gulp-uglify--save-dev
npminstallangular2es6-shimrxjsphaethon--save
Youcanagainsettheprivatepropertyinpackage.jsonsothatyoudon'taccidentallyuploadyourprojecttonpm.Ingulpfile.js,wecannowloadalldependencies:
vargulp=require("gulp");
vartypescript=require("gulp-typescript");
varsmall=require("small").gulp;
varsourcemaps=require("gulp-sourcemaps");
varmerge=require("merge2");
varconcat=require("gulp-concat");
varuglify=require("gulp-uglify");
WewillcreatetwoTypeScriptprojects:onefortheserverandonefortheclientside.Inthesecondproject,wewilloverridethetargettoes5:
vartsServer=typescript.createProject("lib/tsconfig.json");
vartsClient=typescript.createProject("lib/tsconfig.json",{
target:"es5"
});
Nowwecanusealmostthesametaskasinthepreviouschapter.Thesourcesmustbeloadedfromlib/clientinsteadoflib,andlib/sharedshouldbeincludedtoo:
gulp.task("compile-client",function(){
returngulp.src(["lib/client/**/*.ts","lib/shared/**/*.ts"],{base:
"lib"})
.pipe(sourcemaps.init())
.pipe(typescript(tsClient))
.pipe(small('client/index.js',{
outputFileName:{standalone:"scripts.js"},
externalResolve:['node_modules'],
globalModules:{
"crypto":{
standalone:"undefined"
}
}
}))
.pipe(sourcemaps.write('.'))
.pipe(gulp.dest('static/scripts'));
});
Thecompilationoftheserver-sidecodeissimpler,asthecodedoesn'thavetobebundled.NodeJShasabuilt-inmoduleloader:
gulp.task("compile-server",function(){
returngulp.src(["lib/server/**/*.ts","lib/shared/**/*.ts"],{base:"lib"})
.pipe(sourcemaps.init())
.pipe(typescript(tsServer))
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("dist"));
});
Weaddthereleaseanddefaulttasksthatcanbuildthereleaseanddebugtasks:
gulp.task("release",["compile-client","compile-server"],function(){
returngulp.src("static/scripts/scripts.js")
.pipe(uglify())
.pipe(gulp.dest("static/scripts"));
});
gulp.task("default",["compile-client","compile-server"]);
Thetaskscanbestartedusinggulporgulprelease.
TypedefinitionsBeforealibrarycanbeusedinTypeScript,youhavetohavetypedefinitionsforit.Thesearestoredin.d.tsfiles.Forsomepackages,theseareautomaticallyinstalled.Forexample,weusedAngularinthepreviouschapterandwedidn'tinstallthedefinitionsmanually.Packagesdistributedonnpmcanincludetheirtypedefinitionsinthesamepackage.Whenyoudownloadsuchapackage,thetypingscomealong.Unfortunately,notallpackagesdothis.AsofTypeScript2.0,itispossibletodownloadtypingsforthesepackagesonnpmtoo.Forinstance,thetypingsformongodbarepublishedinthe@types/mongodbpackage.Youcaninstalltypesforalotofpackagesthisway.TypesforNodeJSitselfareavailablein@types/node.Runthesecommandsintherootdirectory:
npminstall@types/node--save
npminstall@types/mongodb--save
Thecompilerwillautomaticallyfindthetypesformongodbwhenyouimportit.SincewewillnotexplicitlyimportNodeJSinthecode,thecompilerwillnotfindit.Wemustaddittoourtsconfigfile.
{
"compilerOptions":{
"target":"es6",
"module":"commonjs",
"experimentalDecorators":true,
"emitDecoratorMetadata":true,
"lib":["es2015","dom"],
"types":["node"]
}
}
Thecompilercannowusealltypedefinitions.
GettingstartedwithNodeJSInthepreviouschapter,weusedNodeJS,asgulpusesit.Nodecanbeusedforaserverandforacommandlinetool.Inthischapter,wewillbuildaserverandinChapter9,PlayingTic-Tac-ToeagainstanAI,wewillcreateacommandlineapplication.Ifyouhaven'tinstalledNodeyet,youcandownloaditfromnodejs.org.
Wewillfirstcreateasimpleserver.WewillusePhaethon,apackageforNodethatmakesiteasytobuildaserverinNodeJS.Phaethonincludestypedefinitions,sowecanuseitimmediately.Wecreateafilelib/server/index.tsandaddthefollowing:
import{Server}from"phaethon";
constserver=newServer();
server.listener=request=>newphaethon.ServerResponse("Hello");
server.listenHttp(8800);
Wecanrunthisserverusingthefollowingcommand:
gulp&&nodedist/server
Whenyouopenlocalhost:8800inawebbrowser,thelistenercallbackwillbecalledandyouwillseeHellointhebrowser.
AsynchronouscodeAserverdoesn'tdoalltheworkitself.Itwillalsodelegatesometasks.Forinstance,itmightneedtodownloadawebpageorfetchsomethingfromadatabase.Suchataskwillnotgivearesultimmediately.Inthemeantime,theservercoulddosomethingelse.Thisstyleofprogrammingiscalledasynchronousornonblocking,astheorderofexecutionisnotfixedandsuchtaskdoesnotblocktherestoftheapplication.
Imaginewehaveataskthatwilldownloadawebpage.Thesynchronousvariantwouldlooklikethefollowing:
functiondownload(){
return...;
}
functiondemo(){
//Beforedownload
try{
constresult=download();
constresult2=download();
//Downloadcompleted
}catch(error){
//Error
}
}
Callbackapproachforasynchronouscode
Inawebserver,thiswouldpreventtheserverfromhandlingotherrequests.Thetaskblocksthewholeserver.Thatis,ofcourse,notwhatwewant.Thesimplestasynchronousapproachusescallbacks.Thefirstargumentofthecallbackwillcontainanerrorifsomethingwentwrongandthesecondargumentwillcontaintheresultifthereisaresult:
functiondownload(callback:(error:any,result:string)=>void){
...
}
functiondemo(){
//Beforedownload
download((error,result)=>{
if(error){
//Error
}else{
//Downloadcompleted
download((error2,result2)=>{
if(error2){
}else{
//Download2completed
}
});
}
});
}
Disadvantagesofcallbacks
Thedisadvantageofthisisthatwhenyouhavealotofcallbacks,youhavetonestcallbacksincallbacks,whichiscalledcallbackhell.InES6,anewclasswasintroduced,thatactslikeanabstractionofsuchatask.Itiscalledapromise.Suchavaluepromisesthattherewillbearesultnoworlateron.Thepromisecanberesolved,whichmeansthattheresultisready.Thepromisecanalsoberejected,whichmeansthattherewassomeerror:
functiondownload():Promise<string>{
...
}
functiondemo(){
//Beforedownload
download().then(result=>{
//Downloadcompleted
returndownload();
}).then(result2=>{
//Seconddownloadcompleted
});
}
Asyoucansee,theprecedingcodeismorereadablethanthecallbackscode.It'salsoeasiertochaintaskssinceyoucanreturnanotherpromiseinthethensectionofapromise.However,thesynchronouscodeisstillmorereadable.ES7hasintroducedasyncfunctions.Thesefunctionsaresyntacticsugararoundpromises.Insteadofcallingthenonapromise,youcanawaititandwritecodeasifitweresynchronous.
Tip
Atthetimeofwriting,asyncfunctionscanonlybecompiledtoES6.TypeScript2.1willintroducesupportforES5too.
functiondownload():Promise<string>{
...
}
asyncfunctiondemo(){
try{
constresult=awaitdownload();
constresult2=awaitdownload();
}catch(error){
}
}
Asyoucansee,thisisalmostthesameasthecodewestartedwith.Thisgivesthebestofbothworlds:itresultsinreadableandperformantcode.
Tip
Donotforgettheasynckeywordinthefunctionheader.Ifyouwanttoannotatethefunctionwithareturntype,writePromise<T>insteadofT.
ThedatabaseAlotofprogrammersuseMongoDBincombinationwithNodeJS.YoucaninstallMongoDBfromwww.mongodb.org.MongoDBcanbestartedusingthefollowingcommandintheprojectroot:
mongod--dbpath./data
YoucankeeptheprecedingcommandrunninginoneterminalwindowandrunNodeJSinanotherterminalwindowlateron.
Wrappingfunctionsinpromises
Wewillrunthedatabaseonthesamecomputerastheserverandwewillnamethedatabasenotes.ThisyieldstheURLmongodb://localhost/notes,whichweneedtoconnecttothedatabase.Wehavealreadyinstalledthedefinitionswithtsd.MongoDBexposesanAPIbasedoncallbacks.Wewillwraptheseinpromises,aswewilluseasync/awaitlateron.Wrappingafunctioninapromisewilllooklikethefollowing:
functionwrapped(){
returnnewPromise<string>((resolve,reject)=>{
originalFunction((error,result)=>{
if(error){
reject(error);
}else{
resolve(result);
}
});
});
}
ThePromiseconstructortakesacallbackfunction.Thisfunctioncancalltheresolvecallbackifeverythingsucceededorcalltherejectfunctionifsomethingfailed.
Connectingtothedatabase
Weaddthefollowinginlib/server/database.ts.Firstwemustconnecttothedatabase.Insteadofrejectingwhentheconnectionfailed,wewillthrowtheerror.Thiswaytheserverwillquitifitcan'tconnecttothedatabase:
import{MongoClient,Db,Collection}from"mongodb";
constdatabaseUrl="mongodb://localhost:27017/notes";
constdatabase=newPromise<Db>(resolve=>{
MongoClient.connect(databaseUrl,(error,db)=>{
if(error){
throwerror;
}
resolve(db);
})
});
Tip
Usually,youwouldrejectthepromiseincaseofanerror.Here,wethrowtheerrorandcrashtheserver.Inthiscaseitisbettersincetheservercannotdoanythingwithoutadatabaseconnection.
Thedatabasecontainstwocollections(tables):usersandnotes.Sincewecanonlyaccesstheseaftertheconnectiontothedatabasehassucceeded,theseshouldalsobeplacedinaPromise.SincedatabasealreadyisaPromise,wecanuseasync/await:
asyncfunctiongetCollection(name:string){
constdb=awaitdatabase;
returndb.collection(name);
}
exportconstusers=getCollection("users");
exportconstnotes=getCollection("notes");
TheusersandnotesvariableshavethetypePromise<Collection>.
Wecannowwriteafunctionthatwillinsertanitemintoacollectionandreturnapromise.Sincethispromisedoesn'thavearesultingvalue,wewilltypeitasPromise<void>:
exportfunctioninsert(table:Promise<Collection>,item:any){
constcollection=awaittable;
returnnewPromise<void>((resolve,reject)=>{
collection.insertOne(item,(error)=>{
if(error){
reject(error);
}else{
resolve();
}
});
});
}
Queryingthedatabase
Toquerythedatabase,wewillusethefunctionfind.MongoDBreturnsacursorobject,whichallowsyoutostreamallresults.Ifyouhaveabigapplication,andqueriesthatreturnalotofresults,thiscanimprovetheperformanceofyourapplication.Insteadofstreamingtheresults,wecanalsobuffertheminanarraywiththetoArrayfunction:
exportfunctionfind(table:Promise<Collection>,query:any){
constcollection=awaittable;
returnnewPromise<U[]>((resolve,reject)=>{
collection.find(query,(error,cursor)=>{
if(error){
reject(error);
}else{
cursor.toArray((error,results)=>{
if(error){
reject(error);
}else{
resolve(results);
}
});
}
});
});
}
Wewilladdupdateandremovefunctionslateron.
UnderstandingthestructuraltypesystemTypeScriptusesastructuraltypesystem.Whatthatmeanscanbeeasilydemonstratedusingthefollowingexample:
classPerson{
name:string;
}
classCity{
name:string;
}
constx:City=newPerson();
InlanguageslikeC#,thiswouldnotcompile.Theselanguagesuseanominaltypesystem.Basedonthename,aPersonisnotaCity.TypeScriptusesastructuraltypesystem.BasedonthestructureofPersonandCity,thesetypesareequal,astheybothhaveanameproperty.ThisfitswellinthedynamicnatureofJavaScript.Itcan,however,leadtosomeunexpectedbehavior,asthefollowingwouldcompile:
classFoo{
}
constf:Foo=42;
SinceFoodoesnothaveanyproperties,everyvaluewouldbeassignabletoit.Incaseswerethestructuralbehaviorisnotdesired,youcanaddabrand,apropertythataddstypesafetybutdoesnotexistatruntime:
classFoo{
__fooBrand:void;
}
constf:Foo=42;
Nowthelastlinewillgiveanerror,asexpected.
GenericsThetypingsforMongoDBdon'tusegenericsortypearguments.Giventhatwealreadyhavetoaddatinywrapperaroundit,wecanalsoeasilyaddgenericstothatwrapper.Wewillcreateanewtypeforthedatastorethathasgenerics:
exportinterfaceTable<T>extendsCollection{
__tableBrand:T;
}
Ifyoudin'tincludethebrand,Table<A>wouldbestructurallyidenticaltoTable<B>,whichwedonotwant.Wecannowloadthecollectionswiththecorrecttypes.WeusetheUserandNotetypeshere.Wewillcreatetheseinterfaceslateron:
import{User}from"./user";
import{Note}from"./note";
asyncfunctiongetCollection<U>(name:string){
constdb=awaitdatabase;
return<Table<U>>db.collection(name);
}
exportconstusers=getCollection<User>("users");
exportconstnotes=getCollection<Note>("notes");
Withgenerics,theinsertfunctionwilllooklikethefollowing:
exportfunctioninsert<U>(table:Table<U>,item:U){
returnnewPromise<void>((resolve,reject)=>{
table.insertOne(item,(error)=>{
if(error){
reject(error);
}else{
resolve();
}
});
});
}
Forfind,wewantthequerytobeasupertypeofthetablecontent.Inotherwords,youwanttoqueryonsomepropertiesofthecontentofthetable.SupportforthiswasaddedinTypeScript1.8:
exportfunctionfind<UextendsV,V>(table:Table<U>,query:V){
returnnewPromise<U[]>((resolve,reject)=>{
table.find(query,(error,result)=>{
if(error){
reject(error);
}else{
resolve(result);
}
});
});
}
Wewillalsowritewrappersforupdateandremove.TogetherthesefunctionscandotheCRUDoperations:create,read,update,anddelete:
exportfunctionupdate<UextendsV,V>(table:Table<U>,query:V,newItem:U){
returnnewPromise<void>((resolve,reject)=>{
table.update(query,newItem,(error)=>{
if(error){
reject(error);
}else{
resolve();
}
});
});
}
exportfunctionremove<UextendsV,V>(table:Table<U>,query:V){
returnnewPromise<void>((resolve,reject)=>{
table.remove(query,(error)=>{
if(error){
reject(error);
}else{
resolve();
}
});
});
}
Inlib/server/user.ts,wewillcreatetheUsermodel.ForMongoDB,suchtypesshouldhavean_idproperty.Thedatabasewillusethatpropertytoidentifyinstancesofthemodels:
import{ObjectID}from"mongodb";
exportinterfaceUser{
_id:ObjectID;
username:string;
passwordHash:string;
}
Andinlib/server/note.ts,weaddtheNotemodel:
import{ObjectID}from"mongodb";
exportinterfaceNote{
_id:ObjectID;
userId:string;
content:string;
}
TypingtheAPIInlib/shared/api.ts,wewilladdsometypingsfortheAPI.Ontheserverside,wecancheckthattheresponsehastherighttype:
exportinterfaceLoginResult{
ok:boolean;
message?:string;
}
exportinterfaceMenuResult{
items:MenuItem[];
}
exportinterfaceMenuItem{
id:string;
title:string;
}
exportinterfaceItemResult{
id:string;
content:string;
}
Wewillnowimplementthefunctionsthatreturnthesetypes.
AddingauthenticationInlib/server/index.ts,wewillfirstaddsessions.Asessionisaplacetostoredata,whichispersistentforaclientontheserver.Ontheclientside,acookiewillbesaved,whichcontainsanidentifierofthesession.Ifarequestcontainsavalidcookiewithsuchanidentifier,youwillgetthesamesessionobject.Otherwise,anewsessionwillbecreated:
import{Server,ServerRequest,ServerResponse,ServerError,StatusCode,
SessionStore}from"phaethon";
import{ObjectID}from"mongodb";
import{User,login,logout}from"./user";
import*asnotefrom"./note";
Tip
Withimport{...},wecanimportasetofentitiesfromanotherfile.Withimport*as...,weimportthewholefileasanobject.Thefollowingtwosnippetsareequivalent:import*asfoofrom"./foo";foo.bar();import{bar}from"./foo";bar();
Wedefinethetypeofthecontentofthesessionasfollows:
exportinterfaceSession{
userId:ObjectID;
}
constserver=newServer();
ThesessionswillbestoredinaSessionStore.Thelifetimeofasessionis60*60*24secondsoroneday:
constsessionStore=newSessionStore<Session>("session-id",()=>({userId:
undefined}),60*60*24,1024);
server.listener=sessionStore.wrapListener(async(request,session)=>{
constresponse=awaithandleRequest(request,session.data);
if(responseinstanceofServerResponse){
returnresponse;
}else{
constserverResponse=newServerResponse(JSON.stringify(response));
returnserverResponse;
}
});
server.listenHttp(8800);
JSON.stringifywillconvertanobjecttoastring.Suchastringcaneasilybeconvertedbacktoanobjectontheclientside.InChapter2,WeatherForecastWidget,theresponsesoftheweatherAPIwerealsoformattedasJSONstrings.
InhandleRequest,allrequestswillbesenttoahandlerbasedontheirpath:
asyncfunctionhandleRequest(request:ServerRequest,session:Session):
Promise<ServerResponse|Object>{
constpath=request.path.toLowerCase();
if(path==="/api/login")returnlogin(request,session);
if(path==="/api/logout")returnlogout(request,session);
thrownewServerError(StatusCode.ClientErrorNotFound);
}
ImplementingusersinthedatabaseNowwecanimplementauthenticationinuser.ts.Forsafety,wewon'tstoreplainpasswordsinourdatabase.Insteadwehashthem.Ahashisamanipulationofaninput,inawaythatyoucannotfindtheinputbasedonthehash.Whensomeonewantstologin,thepasswordishashedandcomparedwiththehashedpasswordfromthedatabase.Usingthebuilt-inmodulecrypto,thiscaneasilybedone:
import*ascryptofrom"crypto";
functiongetPasswordHash(username:string,password:string):string{
returncrypto.createHash("sha256").update(password.length+"-"+username
+"-"+password).digest("hex");
}
Thelogouthandleriseasytowrite.WemustremovetheuserIdofthesessionasfollows:
exportfunctionlogout(request:ServerRequest,session:Session):LoginResult{
session.userId=undefined;
return{ok:true};
}
Asyoucansee,weareusingtheLoginResultinterfacethatwewrotepreviously.Theloginfunctionwillusetheasync/awaitsyntax.ThefunctionexpectsthattheusernameandpasswordareavailableintheURLquery.Iftheyarenotavailable,validate.expectwillthrowanerror,whichwillbedisplayedasaBadRequesterror:
exportasyncfunctionlogin(request:ServerRequest,session:Session):
Promise<LoginResult>{
constusername=validate.expect(request.query["username"],
validate.isString);
constpassword=validate.expect(request.query["password"],
validate.isString);
constpasswordHash=getPasswordHash(username,password);
constresults=awaitfind(users,{username,passwordHash});
if(results.length===0){
return{ok:false,message:"Usernameorpasswordincorrect"};
}
constuser=results[0];
session.userId=user._id;
return{ok:true};
}
AddinguserstothedatabaseToaddsomeuserstothedatabase,wemustaddsomecodeandruntheserveroncewithit.Inareal-worldapplication,youwouldprobablywanttoaddaregisterform.Thatiscomparabletoaddinganote,whichwewilldolateroninthischapter.
Wewillalsoaddtwohelperfunctionsthatwecanuseinnote.tstocheckwhethertheuserisloggedin:
import*ascryptofrom"crypto";
import{ServerRequest,ServerResponse,ServerError,StatusCode,validate}from
"phaethon";
import{Session}from"./index";
import{LoginResult}from"../shared/api";
import{users,find,insert}from"./database";
exportinterfaceUser{
_id:string;
username:string;
passwordHash:string;
}
functiongetPasswordHash(username:string,password:string):string{
returncrypto.createHash("sha256").update(password.length+"-"+
username+"-"+password).digest("hex");
}
insert(users,{
_id:undefined,
username:"lorem",
passwordHash:getPasswordHash("lorem","ipsum")
});
insert(users,{
_id:undefined,
username:"foo",
passwordHash:getPasswordHash("foo","bar")
});
exportasyncfunctionlogin(request:ServerRequest,session:Session):
Promise<LoginResult>{
constusername=validate.expect(
request.query["username"],validate.isString);
constpassword=validate.expect(
request.query["password"],validate.isString);
constpasswordHash=getPasswordHash(username,password);
constresults=awaitfind(users,{username,passwordHash});
if(results.length===0){
return{ok:false,message:"Usernameorpasswordincorrect"};
}
constuser=results[0];
session.userId=user._id;
return{ok:true};
}
exportfunctionlogout(request:ServerRequest,session:Session):LoginResult
{
session.userId=undefined;
return{ok:true};
}
exportasyncfunctiongetUser(session:Session){
if(session.userId===undefined)returnundefined;
constresults=awaitfind(users,{_id:session.userId});
returnresults[0];
}
exportasyncfunctiongetUserOrError(session:Session){
constuser=awaitgetUser(session);
if(user===undefined){
thrownewServerError(StatusCode.ClientErrorUnauthorized);
}
returnuser;
}
Runtheserveronceandremovethetwoinsertcallsafterward.
TestingtheAPIWecanstarttheserverbyrunningthefollowingcommand:
gulp&&node--harmony_destructuringdist/server
Inawebbrowser,youcanopenlocalhost:8800/api/login?username=lorem&password=ipsumtotestthecode.Youcanchangetheparameterstotesthowawrongusernameorpasswordbehaves.
Fordebugging,youcanaddconsole.log("...");callsinyourcode.
AddingCRUDoperationsMostservershandleCRUDoperationsprimarily.Ourservermusthandlefivedifferentrequests:listallnotesofthecurrentuser,findaspecificnote,insertanewnote,updateanote,andremoveanote.
First,weaddahelperfunctionthatcanbeusedontheserversideandtheclientside.Inlib/shared/note.ts,weaddafunctionthatreturnsthetitleofanote—thefirstline,ifavailable,or"Untitled":
exportfunctiongetTitle(content:string){
constlineEnd=content.indexOf("\n");
if(content===""||lineEnd===0){
return"Untitled";
}
if(lineEnd===-1){
//Notecontainsoneline
returncontent;
}
//Getfirstline
returncontent.substring(0,lineEnd);
}
WewritetheCRUDfunctionsinlib/server/note.ts.WestartwithimportsandtheNotedefinition:
import{ServerRequest,ServerResponse,ServerError,StatusCode,validate}from
"phaethon";
import{ObjectID}from"mongodb";
import{Session}from"./index";
import{getUserOrError}from"./user";
import{Note}from"./note";
import{getTitle}from"../shared/note";
import{MenuResult,ItemResult}from"../shared/api";
import*asdatabasefrom"./database";
exportinterfaceNote{
_id:string;
userId:string;
content:string;
}
ImplementingthehandlersNowwecanimplementthelistfunction.Usingthehelperfunctionswewrotepreviously,wecaneasilywritethefollowingfunction:
exportasyncfunctionlist(request:ServerRequest,session:Session):
Promise<MenuResult>{
constuser=awaitgetUserOrError(session);
constresults=awaitdatabase.find(
database.notes,{userId:user._id});
constitems=results.map(note=>({
id:note._id.toHexString(),
title:getTitle(note.content)
}));
return{items};
}
WithtoHexString,anObjectIDcanbeconvertedtoastring.ItcanbeconvertedbackusingnewObjectID(...).Themapfunctiontransformsanarraywithaspecificcallback.
Inthefindfunction,wemustsearchforanotebasedonaspecificID:
exportasyncfunctionfind(request:ServerRequest,session:Session):
Promise<ItemResult>{
constuser=awaitgetUserOrError(session);
constid=validate.expect(
request.query["id"],validate.isString);
constnotes=awaitdatabase.find(database.notes,
{_id:newObjectID(id),userId:user._id});
if(notes.length===0){
thrownewServerError(StatusCode.ClientErrorNotFound);
}
constnote=notes[0];
return{
id:note._id.toHexString(),
content:note.content
};
}
Tip
DonotforgettoaddtheuserIdinthequery.Otherwise,ahackercouldfindnotesofadifferentuserwithoutknowinghis/herpassword.
Theinsert,update,andremovefunctionscanbeimplementedasfollows.Ininsert,weset_idtoundefined,asMongoDBwilladdauniqueIDitself:
exportasyncfunctioninsert(request:ServerRequest,session:Session):
Promise<ItemResult>{
constuser=awaitgetUserOrError(session);
constcontent=validate.expect(
request.query["content"],validate.isString);
constnote:Note={
_id:undefined,
userId:user._id,
content
};
awaitdatabase.insert(database.notes,note);
return{
id:note._id.toHexString(),
content:note.content
};
}
exportasyncfunctionupdate(request:ServerRequest,session:Session):
Promise<ItemResult>{
constuser=awaitgetUserOrError(session);
constid=validate.expect(
request.query["id"],validate.isString);
constcontent=validate.expect(
request.query["content"],validate.isString);
constnote:Note={
_id:newObjectID(id),
userId:user._id,
content
};
awaitdatabase.update(database.notes,
{_id:newObjectID(id),userId:user._id},note);
return{
id:note._id.toHexString(),
content:note.content
};
}
exportasyncfunctionremove(request:ServerRequest,session:Session){
constuser=awaitgetUserOrError(session);
constid=validate.expect(
request.query["id"],validate.isString);
awaitdatabase.remove(database.notes,
{_id:newObjectID(id),userId:user._id});
return{};
}
RequesthandlingInlib/server/index.ts,wemustaddreferencestothesefunctionsinhandleRequest:
asyncfunctionhandleRequest(request:ServerRequest,session:Session):
Promise<ServerResponse|Object>{
constpath=request.path.toLowerCase();
if(path==="/api/login")
returnlogin(request,session);
if(path==="/api/logout")
returnlogout(request,session);
if(path==="/api/note/list")
returnnote.list(request,session);
if(path==="/api/note/insert")
returnnote.insert(request,session);
if(path==="/api/note/update")
returnnote.update(request,session);
if(path==="/api/note/remove")
returnnote.remove(request,session);
if(path==="/api/note/find")
returnnote.find(request,session);
thrownewServerError(StatusCode.ClientErrorNotFound);
}
WritingtheclientsideJustliketheweatherwidget,wewillwritetheclientsideofthenoteapplicationwithAngular2.Whentheapplicationstarts,itwilltrytodownloadthelistofnotes.Iftheuserisnotloggedin,wewillgetanUnauthorizederror(statuscode401)andshowtheloginform.Otherwise,wecanshowthemenuwithallnotes,alogoutbutton,andabuttontocreateanewnote.Whenclickingonanote,thatnoteisdownloadedandtheusercanedititinthenoteeditor.Iftheuserclicksonthenewbutton,theusercanwritethenewnoteinthe(same)noteeditor.
Theserverusesacookietomanagethesession,sowedonothavetodothatmanuallyontheclientside.
WestartwithalmostthesameHTMLfilesavedasstatic/index.html:
<!DOCTYPEHTML>
<html>
<head>
<title>MyNotes</title>
<linkrel="stylesheet"href="style.css"/>
</head>
<body>
<divid="wrapper">
<note-application>Loading..</note-application>
</div>
<scripttype="text/javascript">
varglobal=window;
</script>
<scriptsrc="scripts/scripts.js"type="text/javascript"></script>
</body>
</html>
Instatic/style.css,weaddsomestylesasfollows:
body{
font-family:'SegoeUI',Tahoma,Geneva,Verdana,sans-serif;
font-weight:100;
}
h1,h2,h3{
margin:00;
padding:00;
color:#C93524;
}
h2{
margin:00;
padding:00;
color:#1C5C91;
}
#wrapper{
position:absolute;
left:0;
right:0;
top:0;
width:450px;
margin:10%auto;
}
a:link,a:visited{
color:#1C5C91;
text-decoration:underline;
}
a:hover,a:active{
color:#3B6282;
}
li>a:link,li>a:visited{
color:#C93524;
text-decoration:underline;
}
li>a:hover,li>a:active{
color:#AD4236;
}
label{
display:block;
}
Inlib/client/api.ts,wecreateafunction,getUrl,thatwillsimplifyAPIaccess.Withthisfunction,wecanwritegetUrl("login",{username:"lorem",password:"ipsum"})insteadof"login?username=lorem&password=ipsum".Thefunctionalsotakestheescapingofcharacters,suchasanampersand,intoaccount:
exportconstbaseUrl="/api/";
exportfunctiongetUrl(method:string,query:{[key:string]:string}){
leturl=baseUrl+method;
letseperator="?";
for(constkeyofObject.keys(query)){
url+=seperator+encodeURIComponent(key)+"="+
encodeURIComponent(query[key]);
seperator="&";
}
returnurl;
}
CreatingtheloginformNowwecancreatetheloginform,asshowninthefollowingscreenshot:
Inlib/client/login.ts,wecreatetheloginform.Westartwiththeimportsandthetemplate:
import{Component,Output,EventEmitter}from"angular2/core";
import{Http,HTTP_PROVIDERS}from"angular2/http";
import{getUrl}from"./api";
import{LoginResult}from"../shared/api";
@Component({
selector:"login-form",
template:`
<h2>Login</h2>
<form(submit)="submit($event)">
<div>{{message}}</div>
<label>Username<br/><input[(ngModel)]="username"/></label>
<label>Password<br/><inputtype="password"
[(ngModel)]="password"/></label>
<buttontype="submit">Login</button>
</form>
`,
viewProviders:[HTTP_PROVIDERS]
})
exportclassLoginForm{
username:string;
password:string;
message:string;
constructor(privatehttp:Http){}
Herewewillsubmittheusernameandpasswordtotheserver.Iftheloginissuccessful,weemitthesuccessevent.Themaincomponentcanthenhidetheloginpageandshowthemenu:
submit(e:Event){
e.preventDefault();
this.http.get(getUrl("login",{username:this.username,password:
this.password}))
.map(response=>response.json())
.subscribe((response:LoginResult)=>{
if(response.ok){
this.success.emit(undefined);
}else{
this.message=response.message;
}
});
}
@Output()
success=newEventEmitter();
}
CreatingamenuInlib/client/menu.ts,wecreatethemenu.Inthemenu,theuserwillseehis/hernotesandcancreateanewnote.Themenuwilllooklikethefollowing:
Thiscomponentcanemittwodifferentevents:createandopen.Thesecondhasanargument,sowehavetoaddstringastypeargument:
import{Component,Input,Output,EventEmitter}from"angular2/core";
import{MenuItem}from"../shared/api";
@Component({
selector:"notes-menu",
template:`
<buttontype="button"(click)="clickCreate()">New</button>
<ul>
<li*ngFor="#itemofitems">
<ahref="javascript:;"(click)="clickItem(item)">{{
item.title}}</a>
</li>
</ul>
`
})
exportclassMenu{
@Input()
items:MenuItem[];
@Output()
create=newEventEmitter();
@Output()
open=newEventEmitter<string>();
clickCreate(){
this.create.emit(undefined);
}
clickItem(item:MenuItem){
this.open.emit(item.id);
}
}
ThenoteeditorThenoteeditorisasimpletextarea.Aboveit,wewillshowthetitleofthenote.Withtwo-waybindings,thetitleisautomaticallyupdatedwhenthecontentofthetextareaischanged.
ThemaincomponentNowwecanwritethemaincomponent.Thiscomponentwillshowoneoftheothercomponents,dependingonthestate.Firstwemustimportrxjs,Angular,andthefunctionsandcomponentswehavealreadywritten:
import"rxjs";
import{Component}from"angular2/core";
import{bootstrap}from"angular2/platform/browser";
import{Http,HTTP_PROVIDERS,Response}from"angular2/http";
import{getUrl}from"./api";
import{MenuItem,MenuResult,ItemResult}from"../shared/api";
import{LoginForm}from"./login";
import{Menu}from"./menu";
import{NoteEditor}from"./note";
Wewilluseanenumtypetostorethestate:
enumState{
Login,
Menu,
Note,
Error
}
Thetemplateshowstherightcomponentbasedonthestate.Thesecomponentshavesomeeventlistenersattached:
@Component({
selector:"note-application",
viewProviders:[HTTP_PROVIDERS],
directives:[LoginForm,Menu,NoteEditor],
template:`
<h1>MyNotes</h1>
<login-form*ngIf="stateLogin"(success)="loadMenu()"></login-form>
<div*ngIf="!stateLogin">
<ahref="javascript:;"(click)="logout()">Logout</a>
</div>
<notes-menu*ngIf="stateMenu"[items]="menu"(create)="createNote()"
(open)="loadNote($event)"></notes-menu>
<note-editor*ngIf="stateNote&¬e"[content]="note.content"
(save)="save($event)"(remove)="remove($event)"></note-editor>
<div*ngIf="stateError">
<h2>Somethingwentwrong</h2>
Reloadthepageandtryagain
</div>
`
})
Inthebodyoftheclass,wehavetoaddsomepropertiesforthestatefirst:
classApplication{
state=State.Menu;
constructor(privatehttp:Http){
this.loadMenu();
}
getstateLogin(){
returnthis.state===State.Login;
}
getstateMenu(){
returnthis.state===State.Menu;
}
getstateNote(){
returnthis.state===State.Note;
}
getstateError(){
returnthis.state===State.Error;
}
menu:MenuItem[]=[];
note:ItemResult=undefined;
Errorhandler
Nowwewillwriteafunctionthatwillloadthemenu.ErrorswillbepassedtohandleError.Iftheuserwasnotauthenticated,wewillfindthestatuscode401hereandshowtheloginform.Forasuccessfulrequest,wecancasttheresponsetotheinterfaceswedefinedinlib/shared/api.ts:
handleError(error:Response){
if(error.status===401){
//Unauthorized
this.state=State.Login;
this.menu=[];
this.note=undefined;
}else{
this.state=State.Error;
}
}
loadMenu(){
this.state=State.Menu;
this.menu=[];
this.http.get(getUrl("note/list",{})).subscribe(response=>{
constbody=<MenuResult>response.json();
this.menu=body.items;
},error=>this.handleError(error));
}
Weimplementtheeventlisteners,createNoteandloadNote,ofthemenu:
createNote(){
this.note={
id:undefined,
content:""
};
this.state=State.Note;
}
loadNote(id:string){
this.note=undefined;
this.http.get(getUrl("note/find",{id:id})).subscribe(response=>
{
this.state=State.Note;
this.note=<ItemResult>response.json();
},error=>this.handleError(error));
}
Insave,wehavetocheckwhetherthenoteisneworbeingupdated:
save(content:string){
leturl:string;
this.note.content=content;
if(this.note.id===undefined){
//Newnote
url=getUrl("note/insert",{content:this.note.content});
}else{
//Existingnote
url=getUrl("note/update",{id:this.note.id,content:
this.note.content});
}
this.state=State.Note;
this.note=undefined;
this.http.get(url).subscribe(response=>{
this.loadMenu();
},error=>this.handleError(error));
}
remove(){
if(this.note.id===undefined){
this.loadMenu();
return;
}
this.http.get(getUrl("note/remove",{id:this.note.id
})).subscribe(response=>{
this.loadMenu();
},error=>this.handleError(error));
}
logout(){
this.http.get(getUrl("logout",{})).subscribe(response=>{
this.state=State.Login;
this.menu=[];
this.note=undefined;
},error=>this.handleError(error));
}
}
bootstrap(Application).catch(err=>console.error(err));
RunningtheapplicationTotesttheapplication,theserverandthestaticfileshavetobeservedfromthesameserver.Todothat,youcanusethehttp-serverpackage.Thatservercanservethestaticfilesandpassthrough(proxy)therequeststotheAPIserver.IfMongoDBisnotrunningyet,openaterminalwindowandrunmongod--dbpath./data.OpenaterminalwindowintherootoftheprojectandrunthefollowingtostarttheAPIserveronlocalhost:8800:
gulp&&node--harmony_destructuringdist/server
Inanewterminalwindow,navigatetothestaticdirectory.Installhttp-serverusingthefollowingcommand:
npminstallhttp-server-g
Nowyoucanstarttheserver:
http-server-Phttp://localhost:8800
Openlocalhost:8080inabrowserandyouwillseetheapplicationthatwehavecreated.
SummaryInthischapter,youcreatedaclient-serverapplication.YouusedNodeJStocreateaserver,withMongoDBandPhaethon.Youalsolearnedmoreaboutasynchronousprogrammingandthestructuraltypesystem.WeusedourknowledgeofAngularfromthefirstchaptertocreatetheclientside.
Inthenextchapter,wewillcreateanotherclient-serverapplication.ThatapplicationisnotaCRUDapplication,butareal-timechatapplication.WewillbeusingReactinsteadofAngular.
Chapter4.Real-TimeChatAfterhavingwrittentwoapplicationswithAngular2,wewillnowcreateonewithReact.Theserverpartwillalsobedifferent.Insteadofaconnectionlessserver,wewillnowcreateaserverwithapersistentconnection.Inthepreviouschapters,theclientsentrequeststotheserverandtheserverrespondedtothem.Nowwewillwriteaserverthatcansendinformationatanytimetotheclient.Thisisneededtosendnewchatmessagesimmediatelytotheclient,asshowninthefollowing:
Inthechatapplication,ausercanfirstchooseausernameandjoinachatroom.Intheroom,he/shecansendmessagesandreceivemessagesfromotherusers.Inthischapter,wewillcoverthefollowingtopics:
SettinguptheprojectGettingstartedwithReactWritingtheserverConnectingtotheserverCreatingthechatroomComparingReactandAngular
SettinguptheprojectBeforewecanstartcoding,wehavetosetuptheproject.ThedirectorystructurewillbethesameasinChapter3,Note-TakingAppwithServer;staticcontainsthestaticfilesforthewebserver,lib/clientcontainstheclient-sidecode,lib/servercontainsthecodefortheserver,lib/sharedcontainsthecodethatcanbeusedonbothsides,andlib/typingscontainsthetypedefinitionsforReact.
Wecaninstallalldependencies,forgulp,theserver,andReact,asfollows:
npminit
npminstallreactreact-domws--save
npminstallgulpgulp-sourcemapsgulp-typescriptgulp-uglifysmall--save-dev
Thetypedefinitionscanbeinstalledusingnpm:
cdlib
npminstall@types/node@types/react@types/react-dom@types/ws--save
Wecreatestatic/index.html,whichwillloadthecompiledJavaScriptfile:
<!DOCTYPEHTML>
<html>
<head>
<title>Chat</title>
<linkhref="style.css"rel="stylesheet"/>
</head>
<body>
<divid="app"></div>
<scripttype="text/javascript">
varprocess={
env:{
NODE_ENV:"DEBUG"//or"PRODUCTION"
}
};
</script>
<scriptsrc="scripts/scripts.js"type="text/javascript"></script>
</body>
</html>
Weaddstylesinstatic/style.css:
body{
font-family:'SegoeUI',Tahoma,Geneva,Verdana,sans-serif;
}
label,input,button{
display:block;
}
ConfiguringgulpWecanusealmostthesamegulpfile.WedonothavetoloadanypolyfillsforReact,sotheresultingfileisevensimpler:
vargulp=require("gulp");
varsourcemaps=require("gulp-sourcemaps");
vartypescript=require("gulp-typescript");
varsmall=require("small").gulp;
varuglify=require("gulp-uglify");
vartsServer=typescript.createProject("lib/tsconfig.json",{
typescript:require("typescript")});
vartsClient=typescript.createProject("lib/tsconfig.json",{typescript:
require("typescript"),target:"es5"});
gulp.task("compile-client",function(){
returngulp.src(["lib/client/**/*.ts","lib/client/**/*.tsx",
"lib/shared/**/*.ts"],{base:"lib"})
.pipe(sourcemaps.init())
.pipe(typescript(tsClient))
.pipe(small("client/index.js",{outputFileName:{
standalone:"scripts.js"},externalResolve:
["node_modules"]}))
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("static/scripts"));
});
gulp.task("compile-server",function(){
returngulp.src(["lib/server/**/*.ts","lib/shared/**/*.ts"],{
base:"lib"})
.pipe(sourcemaps.init())
.pipe(typescript(tsServer))
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("dist"));
});
gulp.task("release",["compile-client","compile-server"],function(){
returngulp.src("static/scripts/**.js")
.pipe(uglify())
.pipe(gulp.dest("static/scripts"));
});
gulp.task("default",["compile-client","compile-server"]);
Inlib/tsconfig.json,weconfigureTypeScript.Wehavetosetthejsxoption.InReact,viewsarewritteninanXML-likelanguage,JSX,insideJavaScript.TousethisinTypeScript,youhavetosetthejsxoptionandusethefileextension.tsxinsteadof.jsx.
{
"compilerOptions":{
"module":"commonjs",
"target":"es6",
"jsx":"react",
"types":["node"]
}
}
GettingstartedwithReactJustlikeAngular,Reactiscomponentbased.Angulariscalledaframework,whereasReactiscalledalibrary.ThismeansthatAngularprovidesalotofdifferentfunctionalitiesandReactprovidesonefunctionality,views.Inthefirsttwochapters,weusedtheHTTPserviceofAngular.Reactdoesnotprovidesuchaservice,butyoucanuseotherlibrariesfromnpminstead.
CreatingacomponentwithJSXAcomponentisaclassthathasarendermethod.ThatmethodwillrendertheviewandisthereplacementofthetemplateinAngular.Asimplecomponentwouldlooklikethefollowing:
exportclassExampleextendsReact.Component<{},{}>{
render(){
constname="World";
return(
<div>
Hello,{name}!
<buttononClick={()=>alert("Hello")}>
Clickme
</button>
</div>
);
}
}
Asyoucansee,youcanembedHTMLinsidetherenderfunction.Expressionscanbewrappedinsidecurlybrackets,bothintextandinpropertiesofothercomponents.Eventhandlerscanbeaddedinthiswaytoo.Insteadofusingbuilt-incomponents,youcanusecustomcomponentsinthesehandlers.Allbuilt-incomponentsstartwithalowercasecharacterandcustomelementsshouldstartwithanuppercasecharacter.Thisisnotjustaconvention,butrequiredbyReact.Wehavetouseadifferentsyntaxfortypecastsin.tsxfiles,asthenormalsyntaxconflictswiththeXMLelements.Insteadof<Type>value,wewillnowwritevalueasType.In.tsfiles,wecanusebothstyles.
AddingpropsandstatetoacomponentIntheexample,thecomponentextendstheReact.Componentclass.Thatclasshastwotypearguments,whichrepresentthepropsandthestate.Thepropscontaintheinputthattheparentcomponentgivestothisone.Youcancomparethattothe@InputdirectiveinAngular.Youcannotmodifythepropsinthecontainingclass.ThestatecontainstheotherpropertiesofacomponentinAngular,whichcanbemodifiedintheclass.Youcanaccessthepropswiththis.propsandthestatewiththis.state.Thestatecannotbemodifieddirectly,asyouhavetoreplacethestatewithanewobject.Imaginethestatecontainstwoproperties,fooandbar.Ifyouwanttomodifyfooandbar,itisnotallowedwiththis.state.foo=42,butyouhavetowritethis.setState({foo:42,bar:true})instead.Inmostcases,youdonothavetochangeallpropertiesofthestate.Insuchcases,youonlyhavetospecifythepropertiesthatyouwanttochange.Forinstance,this.setState({foo:42,bar:true})willchangethevalueoffooandkeeptheoldvalueofbar.Thestateobjectisthenreplacedbyanewobject.Thestateobjectwillneverchange.Suchanobjectiscalledanimmutableobject.WewillreadmoreontheseobjectsinChapter5,NativeQRScannerApp.
Thecomponentwillbere-renderedbyReactaftercallingsetState.
Inotherpartsoftheapplication,wewillalsoneedtomodifyafewpropertiesofanobject.Forbigobjects,thisbecomesannoying.Wecreateahelperfunction,whichrequirestheoldstate,addsmodificationstoit,andreturnsanewstate.Thisfunctiondoesnotchangetheoldstate,butreturnsanewone.Inlib/client/model.ts,wecreatethemodifyfunction:
exportfunctionmodify<UextendsV,V>(old:U,changes:V){
constresult:any=Object.create(Object.getPrototypeOf(old));
for(constkeyofObject.keys(old)){
result[key]=old[key];
}
for(constkeyofObject.keys(changes)){
result[key]=changes[key];
}
return<U>result;
}
CreatingthemenuWewillstartwiththemenuofourapplication.Inthemenu,theusercanchoosethechatroomthathe/shewantstojoin.Themenuwillfirstasktheuserforausername.Afterward,theusercantypethenameofachatroom.Theuserwillgetcompletionsforknownrooms,buthe/shecanalsocreateanewroom.Let'scheckthefollowingscreenshotasanexampleofmenu:
Thecomponentwilldelegatethecompletionstoitsparent,soweneedtoaddthecurrentlistofcompletionstotheprops,suchthattheparentcansetit.Also,weneedtoaddacallbackthatcanbecalledwhenthecompletionsmustbefetched.
Thestatemustcontaintheusernameandtheroomname.Reactdoesnothavetwo-waybindings,sowehavetouseeventlistenerstoupdatetheusernameandroomnameinthestate.
Wewilldisabletherestofthemenuiftheuserhasn'tprovidedtheusername.Whentheuserhasfilledinaroom,weshowalistofcompletionsandabuttontocreateanewroomwiththespecifiedname.
Wewritethecodeinlib/client/menu.tsx.First,wedefinethepropsandstateintwodifferentinterfaces:
import*asReactfrom"react";
import{modify}from"./model";
interfaceMenuProps{
completions:string[];
onRequestCompletions:(room:string)=>void;
onClick:(username:string,room:string)=>void;
}
interfaceMenuState{
username:string;
roomname:string;
}
Second,wecreatetheclass.Wesettheinitialstatewithanemptyusernameandroomname:
exportclassMenuextendsReact.Component<MenuProps,
MenuState>{
state={
username:"",
roomname:""
};
Intherenderfunction,weuseJSXtoshowthecomponent.WecanusenormalTypeScriptconstructs.ThereisnoneedtousesomethinglikeNgIforNgFor,aswedidinAngular:
render(){
constmenuEnabled=this.state.username!=="";
constmenuStyle={
opactity:menuEnabled?1:0.5
};
constshowCreateButton=menuEnabled
&&this.state.roomname!==""
&&this.props.completions
.indexOf(this.state.roomname)===-1;
return(<div>
<labelhtmlFor="username">Username</label>
<inputtype="text"id="username"onChange=
{e=>this.changeUsername(
(e.targetasHTMLInputElement).value)}/>
<divstyle={menuStyle}>
<labelhtmlFor="roomname">Room</label>
<inputtype="text"id="roomname"
disabled={!menuEnabled}
onChange={e=>
this.changeName(
(e.targetasHTMLInputElement).value)
}/>
{showCreateButton
?<buttononClick={
()=>this.submit(this.state.roomname)}>
Createroom{this.state.roomname}</button>
:""}
{this.props.completions.map(
completion=>
<ahref="javascript:;"
key={completion}
style={{display:"block"}}
onClick={()=>
this.submit(completion)}>
{completion}</a>)}
</div>
</div>);
}
Finally,wecanimplementthelisteners:
privatechangeUsername(username:string){
this.setState({username});
}
privatechangeName(roomname:string){
this.setState({roomname});
this.props.onRequestCompletions(roomname);
}
privatesubmit(room:string){
this.props.onClick(this.state.username,room);
}
}
Wecanseethiscomponentinactionbyaddingthefollowinginlib/client/index.tsx:
ReactDOM.render(<Menucompletions={[]}onRequestCompletions={()=>{}}onClick=
{()=>{}}/>);
ThiswillrenderthemenuintheHTMLfile.
TestingtheapplicationToviewtheapplicationinabrowser,youmustfirstbuilditusinggulp.Youcanexecutegulpinaterminal.Afterward,youcanopenstatic/index.htmlinabrowser.
WritingtheserverToaddinteractiontotheapplication,wemustcreatetheserverfirst.Wewillusethewspackagetoeasilycreateawebsocketserver.Onthewebsocket,wecansendmessagesinbothdirections.ThesemessagesareobjectsconvertedtostringswithJSON,justlikeinthepreviouschapters.
ConnectionsInthepreviouschapter,wewroteaconnectionlessserver.Foreveryrequest,anewconnectionwassetup.Wecouldstoreastateusingasession.Suchsessionwasidentifiedwithacookie.Ifyouweretocopythatcookietoadifferentcomputer,youwouldhavethesamesessionthere.
Nowwewillwriteaserverthatusesconnections.Inthisway,theservercaneasilykeeptrackofwhichuserisloggedinandwhere.Theservercanalsosendamessagetotheclientwithoutadirectrequest.Thisautomaticupdatingiscalledpushing.Theopposite,pulling,orpolling,meansthattheclientconstantlyaskstheserverwhetherthereisnewdata.
Withconnections,theorderofarrivalisthesameastheorderofsending.Withaconnectionlessserver,asecondmessagecanuseadifferentrouteandarriveearlier.
TypingtheAPIWewilltypethesemessagesinlib/shared/api.ts.Inthepreviouschapter,theURLidentifiedthefunctiontobecalled.Now,wemustincludethatinformationinthemessageobject.Wetypethemessagesfromtheclienttotheserverandviceversa:
exportenumMessageKind{
FindRooms,
OpenRoom,
SendMessage,
RoomCompletions,
ReceiveMessage,
RoomContent
}
exportinterfaceMessage{
kind:MessageKind;
}
exporttypeClientMessage=OpenRoom|ChatMessage|FindRooms;
exporttypeServerMessage=RoomContent|ChatMessage;
exportinterfaceFindRoomsextendsMessage{
query:string;
}
exportinterfaceOpenRoomextendsMessage{
room:string;
}
exportinterfaceRoomCompletionsextendsMessage{
completions:string[];
}
exportinterfaceRoomContentextendsMessage{
room:string;
messages:ChatContent[];
}
exportinterfaceSendMessageextendsMessage{
text:string;
}
exportinterfaceChatMessageextendsMessage{
content:ChatContent
}
exportinterfaceChatContent{
room:string;
username:string;
content:string;
}
AcceptingconnectionsInlib/server/index.ts,wecreateaserverthatlistensfornewconnections.Wealsokeeptrackofallopenconnections.Whenamessageissentinachatroom,itcanbeforwardedtoallsessionsthathaveopenedthatroom.Weusewstocreateawebsocketserver:
import*asWebSocketfrom"ws";
import*asapifrom"../shared/api";
constserver=newWebSocket.Server({port:8800});
server.on("connection",receiveConnection);
interfaceSession{
sendChatMessage(message:api.ChatContent):void;
}
constsessions:Session[]=[];
Wewillstoretherecentmessagesinanarray.Welimitthesizeofthearray,asanattackercouldotherwisefillthewholememoryofaserverwitha(D)DOSattack:ifausersendsalotofmessages(automatically),thiswillcostalotofservermemory.Ifmultipleusersdothat,thememorycanbefilledentirelyandtheserverwillcrash.
StoringrecentmessagesYoucanimplementthiswithanarraybyremovingthefirstmessageandappendingthenewmessageattheend.However,thiswouldshiftthewholearray,especiallylargearraysthatcantakesometime.Instead,weuseadifferentapproach.Weuseanarraythatcanbeseenasacircle:afterthelastelementcomesthefirstone.Weuseavariablethatpointstotheoldestmessage.Whenanewmessageisadded,theitematthepositionofthepointerisoverwrittenwiththenewmessage.Thepointerisincrementedwithoneandpointsagaintotheoldestmessage.WhenthemessagesA,B,CandDaresentwithanarraysizeof3,thiscanbevisualizedlikethefollowing:
[-,-,-];pointer=0
[A,-,-];pointer=1
[A,B,-];pointer=2
[A,B,C];pointer=0
[D,B,C];pointer=1
IfyouarefamiliarwithanalyzingalgorithmsandBig-Ohnotation,thistakesO(1),whereasthenaiveideatakesO(n).Wecreatethearrayinlib/server/index.ts:
constrecentMessages:api.ChatContent[]=newArray(2048);
letrecentMessagesPointer=0;
Wedonotsavethemessagestodisk.Youcoulddothatanduseacachewithsucharraytoincreasetheperformanceoftheserver.
HandlingasessionForeachconnection,wehavetokeeptrackoftheusernameandroomnameoftheuser.WecandothatwithvariablesinsidethereceiveConnectionfunction:
functionreceiveConnection(ws:WebSocket){
letusername:string;
letroom:string;
Wecanlistentothemessageandcloseevents.Thefirstisemittedwhentheclienthassentamessageinthewebsocket.Thesecondisemittedwhenthewebsockethasbeenclosed.Whenthesocketisclosed,wemustnotsendanymessagestoitandwemustremoveitfromthesessionsarray:
ws.on("message",message);
ws.on("close",close);
constsession:Session={sendChatMessage};
sessions.push(session);
functionmessage(data){
try{
constobject=<api.ClientMessage>JSON.parse(data);
if(typeofobject.kind!=="number")return;
switch(object.kind){
caseapi.MessageKind.FindRooms:
findRooms(<api.FindRooms>object);
caseapi.MessageKind.OpenRoom:
openRoom(<api.OpenRoom>object);
break;
caseapi.MessageKind.SendMessage:
chatMessage(<api.SendMessage>object);
break;
}
}catch(e){
console.error(e);
}
}
functionclose(){
constindex=sessions.indexOf(session);
sessions.splice(index,1);
}
functionsend(data:api.ServerMessage){
ws.send(JSON.stringify(data));
}
Theservershouldalwaysvalidatetheinputthatitgets.ThedatacouldnotbeaJSONstring,whichwouldcauseJSON.parsetothrowanerror.object.kindmightnotbeanumber,asTypeScriptdoesnotdoanyruntimechecks.Wecanvalidatethatwithatypeofcheck.
Tip
Ifyouwouldnothaveaddedatry/catch,theserverwouldcrashiftheclientsendsamessage
thatisnotthecorrectJSON.Topreventthis,wewillcatchthaterror.Fordebugging,wewritetheerrorontheconsole.
ImplementingachatmessagesessionNowwecanimplementthefunctionsthatarecalledwhenamessagecomesin.Westartwiththefunctionthatsendsachatmessagetoallactiveconnectionsinthatroomandstoresitinthearraywithrecentmessages:
functionsendChatMessage(content:api.ChatContent){
if(content.room===room){
send({
kind:api.MessageKind.ReceiveMessage,
content
});
}
}
functionchatMessage(message:api.SendMessage){
if(typeofmessage.content!=="string")return;
constcontent:api.ChatContent={
room,
username,
content:message.content
};
recentMessages[recentMessagesPointer]=content;
recentMessagesPointer++;
if(recentMessagesPointer>=recentMessages.length){
recentMessagesPointer=0;
}
for(constitemofsessions){
if(session!==item)item.sendChatMessage(content);
}
}
Thiswillsendachatmessagetoallothersessionsinthesameroom.WeinsertthemessageattherightlocationinrecentMessagesandadjustthepointer.
Finally,wewillwritethefunctionthatgivescompletionsforroomnames.Wedonothaveanarrayofroomnames,sowehavetogetthatinformationfromtherecentmessages.Theresultingarraycancontainduplicates,sowehavetoremovethese.Anaiveapproachwouldbetocheckforeveryelementifithasoccurredbeforeinthearray.However,thisisaslowoperation.Instead,wesortthearrayfirst.Aftersorting,weonlyhavetocompareeachelementwiththeelementbeforeit.Iftheseareequal,thesecondisaduplicate,otherwiseitisnot.ForthosefamiliarwithBig-Oh,thefirstapproachcostsO(n^2)andthesecondonecostsO(nlog(n)).Thisresultsinthefollowingfunction:
functionfindRooms(message:api.FindRooms){
constquery=message.query;
if(typeofquery!=="string")return;
constrooms=recentMessages
.map(msg=>msg.room)
.filter(room=>room.toLowerCase().indexOf(query.toLowerCase())!==-1)
.sort();
constcompletions:string[]=[];
letprevious:string=undefined;
for(letroomofrooms){
if(previous!==room){
completions.push(room);
previous=room;
}
}
send({
kind:api.MessageKind.RoomCompletions,
completions
});
}
}
Wehavecompletedtheserverandcanfocusontheclientsideagain.
ConnectingtotheserverWecanconnecttotheserverwiththeWebSocketclass:
constsocket=newWebSocket("ws://localhost:8800/");
Sincewe'reusingReact,weaddthefollowingtothestate.Wecreateanewcomponent,App,thatwillshowthemenuorachatroombasedonthestate.Inlib/client/index.tsx,wefirstdefinethestateandpropsofthatcomponent:
import*asReactfrom"react";
import*asReactDOMfrom"react-dom";
import*asapifrom"../shared/api";
import*asmodelfrom"./model";
import{Menu}from"./menu";
import{Room}from"./room";
interfaceProps{
apiUrl:string;
}
interfaceState{
socket:WebSocket;
username:string;
connected:boolean;
completions:string[];
room:model.Room;
}
classAppextendsReact.Component<Props,State>{
state={
socket:undefined,
username:'',
connected:false,
completions:[],
room:undefined
};
AutomaticreconnectingNextup,wewillwriteafunction,connect,thatconnectstotheserverusingaWebSocket.WecallthatfunctionincomponentDidMount,whichiscalledbyReact.Wemustalsocallconnectwhentheconnectiongetsclosedforsomereason(forinstance,networkproblems).Westorethesocketinthestateandwealsokeeptrackofwhethertheclientisconnected:
connect(){
if(this.state.connected)return;
constsocket=newWebSocket(this.props.apiUrl);
this.setState({socket});
socket.onopen=()=>{
this.setState({connected:true});
if(this.state.room){
this.openRoom(this.state.username,this.state.room.name);
}
};
socket.onmessage=e=>this.onMessage(e);
socket.onclose=e=>{
this.setState({connected:false});
setTimeout(()=>this.connect(),400);
};
}
onMessage(e:MessageEvent){
constmessage=JSON.parse(e.data.toString())asapi.ServerMessage;
if(message.kind===api.MessageKind.RoomCompletions){
this.setState({
completions:(messageasapi.RoomCompletions).completions
});
}elseif(message.kind===api.MessageKind.RoomContent){
this.setState({
room:{
name:(messageasapi.RoomContent).room,
messages:(messageasapi.RoomContent).messages.map(msg=>this.mapMessage(msg))
}
});
}elseif(message.kind===api.MessageKind.ReceiveMessage){
this.addMessage(this.mapMessage((messageasapi.ReceiveMessage).content));
}
}
componentDidMount(){
this.connect();
}
socket.onmessageiscalledwhentheclientreceivesamessagefromtheserver.Basedonthekindofmessage,itissenttosomefunctionthatwewillimplementlater.First,wewillwritetherenderfunction.Afterwehavewrittentherenderfunction,weknowwhicheventhandlerswehavetowrite.
Tip
Whenyouwritetopdown,youfirstwritethemainfunctionandafterwardthehelperfunctionsthatthemainfunctionrequires.Withthebottomupapproach,youwritethehelper
functionsbeforeyouwritethemainfunction.Inthissection,wewritethehelperfunctionslast,sowewritetopdown.Youcantrybothstylesandfindoutwhatyoulikemost.
Inrender,werenderthecomponentbasedonthestate—ifthereisnoconnection,weshowConnecting...,iftheuserisinaroom,weshowthatchatroom,otherwiseweshowthemenu:
render(){
if(!this.state.connected){
return<div>Connecting...</div>;
}
if(this.state.room){
return<Roomroom={this.state.room}onPost={content=>
this.post(content)}/>;
}
return<Menu
completions={this.state.completions}
onRequestCompletions={query=>
this.requestCompletions(query)}
onClick={(username,room)=>
this.openRoom(username,room)}
/>;
}
SendingamessagetotheserverBeforewritingtheeventhandlers,wefirstwriteasmallfunctionthatsendsamessagetotheserver.ItconvertsanobjecttoJSON,andTypeScriptwillcheckthatwearesendingacorrectmessagetotheserver:
privatesend(message:api.ClientMessage){
this.state.socket.send(JSON.stringify(message));
}
TherequestCompletionsandopenRoomfunctionssendamessagetotheserver.InopenRoom,wealsohavetostoretheusernameinthestate:
privaterequestCompletions(query:string){
this.send({
kind:api.MessageKind.FindRooms,
query
});
}
privateopenRoom(username,room:string){
this.send({
kind:api.MessageKind.OpenRoom,
username,
room
});
this.setState({username});
}
WritingtheeventhandlerForiterationsinReact,everyelementshouldhaveakeythatcanidentifyit.Thus,weneedtogiveeverymessagesuchakey.Weuseasimplenumerickey,whichwewillincrementforeverymessage:
privatenextMessageId:number=0;
privatepost(content:string){
this.send({
kind:api.MessageKind.SendMessage,
content
});
this.addMessage({
id:this.nextMessageId++,
user:this.state.username,
content,
isAuthor:true
});
}
privateaddMessage(msg:model.Message){
constmessages=[
...this.state.room.messages,
msg
].slice(Math.max(0,this.state.room.messages.length-10));
constroom=model.modify(this.state.room,{
messages
});
this.setState({room});
}
privatemapMessage(msg:api.ChatContent){
return{
id:this.nextMessageId++,
user:msg.username,
content:msg.content,
isAuthor:msg.username===this.state.username
};
}
}
Finally,wecanshowthecomponentintheHTMLfile:
ReactDOM.render(
<AppapiUrl="ws://localhost:8800/"/>,document.getElementById("app")
);
Wehavenowwrittenalleventhandlersandinteractionwiththeserver.Wewritethechatroomcomponentinthenextsection.
CreatingthechatroomWedividethechatroomintotwosubcomponents:amessageandtheinputbox.Whentheusersendsanewmessage,itissenttothemaincomponent.Messageoftheuserwillbeshownontherightandothermessagesontheleft,asshowninthefollowingscreenshot:
Two-waybindingsReactdoesnothavetwo-waybindings.Instead,wecanstorethevalueinthestateandmodifyitwhentheonChangeeventisfired.Fortheinputbox,wewillusethistechnique.Thetextboxshouldbeemptiedwhentheuserhassenthis/hermessage.Withthisbinding,wecaneasilydothatbymodifyingthevalueinthestatetoanemptystring:
classInputBoxextendsReact.Component<{onSubmit(value:string):void;},{
value:string}>{
state={
value:""
};
render(){
return(
<formonSubmit={e=>this.submit(e)}>
<inputonChange={e=>this.changeValue((e.targetas
HTMLInputElement).value)}value={this.state.value}/>
<buttondisabled={this.state.value===""}
type="submit">Submit</button>
</form>
);
}
privatechangeValue(value:string){
this.setState({value});
}
privatesubmit(e:React.FormEvent<{}>){
e.preventDefault();
if(this.state.value){
this.props.onSubmit(this.state.value);
this.state.value="";
}
}
}
StatelessfunctionalcomponentsIfacomponentdoesn'tneedastate,thenitdoesnotneedaclasstostoreandmanagethatstate.Insteadofwritingaclasswithjustarenderfunction,youcanwritethatfunctionwithouttheclass.Thesecomponentsarecalledstatelessfunctionalcomponents.Amessageisclearlystateless,asyoucannotmodifyamessagethathasalreadybeensent:
functionMessage(props:{message:model.Message}){
return(
<div>
<divclassName={props.message.isAuthor?"messagemessage-own":
"message"}>
{props.message.content}
<divclassName="message-user">
{props.message.user}
</div>
</div>
<divstyle={{clear:"both"}}></div>
</div>
);
}
Astatelessfunctionalcomponentcanhaveachildcomponentwithastate.TheinputboxhasastateandcanbeusedinsideRoom,whichisastatelesscomponent.Wehavetosetthekeypropertyinthearrayofmessages.Reactusesthistoidentifycomponentsinsidethearray:
exportfunctionRoom(props:{room:model.Room,onPost:(content:string)=>void
}){
return(
<div>
<h2>{props.room.name}</h2>
{props.room.messages.map(message=><Messagekey={message.id}
message={message}/>)}
<InputonSubmit={content=>props.onPost(content)}/>
</div>
);
}
RunningtheapplicationWecannowrunthewholeapplication.First,wemustcompileitwithgulp.Second,wecanstarttheserverbyrunningnodedist/serverinaterminal.Finally,wecanopenstatic/index.htmlinabrowserandstartchatting.Whenyouopenthispagemultipletimes,youcansimulatemultipleusers.
ComparingReactandAngularInthepreviouschapters,weusedAngularandinthischapterweusedReact.AngularandReactarebothfocusedoncomponents,buttherearedifferences,forinstance,betweenthetemplatesinAngularandtheviewsinReact.Inthissection,youcanreadmoreaboutthesedifferences.
TemplatesandJSXAngularusesatemplatefortheviewofacomponent.Suchatemplateisastringthatisparsedatruntime.TypeScriptcannotcheckthesetemplates.Ifyoumisspellapropertyname,youwillnotgetacompileerror.
ReactusesJSX,whichissyntacticsugararoundfunctioncalls.AJSXelementistransformed,bythecompiler,intoacalltoReact.createElement.Thefirstargumentisthenameoftheelementortheelementclass,thesecondargumentcontainstheprops,andtheotherargumentsarethechildrenofthecomponent.Thefollowingexampledemonstratesthetransform:
<div></div>;
React.createElement("div",null);
<divprop="a"></div>;
React.createElement("div",{prop:"a"});
<div>Foo</div>;
React.createElement(
"div",
null,
"Foo"
);
<div><span>Foo</span></div>;
React.createElement(
"div",
null,
React.createElement(
"span",
null,
"Foo"
)
);
Elementsthatstartwithacapitalletterorcontainadotareconsideredtobecustomcomponents,andotherelementsaretreatedasintrinsicelements,thestandardHTMLelements:
<div></div>;
React.createElement("div",null);
<Foo></Foo>;
React.createElement(Foo,null);
TheseJSXelementsarecheckedandtransformedatcompiletime,soyoudogetanearlyerrorwhenyoumisspellaproperty.ReactisnottheonlyframeworkthatusesJSX,butitisthemostpopularone.
LibrariesorframeworksAngularisaframeworkandReactisalibrary.AlibraryprovidesonefunctionalityinthecaseofReact—theviewsoftheapplication.Aframeworkprovidesalotofdifferentfunctionalities.Forinstance,Angularrenderstheviewsoftheapplication,butalsohas,forinstance,dependencyinjectionandanHttpservice.IfyouwantsuchfeatureswhenyouareusingReact,youcanuseanotherlibrarythatgivesthatfeature.
ReactprogrammersoftenuseaFluxbasedarchitecture.Fluxisanapplicationarchitecturethatisimplementedinvariouslibraries.InChapter5,NativeQRScannerApp,wewilltakealookatthisarchitecture.
SummaryWehavewrittenanapplicationwithwebsockets.WehaveusedReactandJSXfortheviewsofourapplication.WehaveseenmultiplewaystocreatecomponentsandlearnedhowtheJSXtransformworks.InChapter5,NativeQRScannerApp,wewilluseReactagain,butwewillfirsttakealookatmobileappswithNativeScriptinthenextchapter.
Chapter5.NativeQRScannerAppWehavealreadyusedTypeScripttobuildwebappsandaserver.TypeScriptcanalsobeusedtocreatemobileapps.Inthischapter,wewillbuildsuchanapp.TheappcanscanQRcodes.Theappshowsalistofallpreviousscans.IfaQRcodecontainsaURL,theappcanopenthatURLinawebbrowser.VariousframeworksexistformakingmobileappsinTypeScript.WewilluseNativeScript,whichprovidesanativeuserinterfaceandrunsonAndroidandiOS,asshowninthefollowing:
Wewillcreatethisappwiththefollowingsteps:
CreatingtheprojectstructureCreatingaHelloWorldpageCreatingthemainviewAddingadetailsviewScanningQRcodesAddingpersistentstorageStylingtheappComparingNativeScripttoalternatives
GettingstartedwithNativeScriptInstallingNativeScriptrequiresseveralsteps.FordevelopingappsforAndroid,youhavetoinstallJavaDevelopmentKit(JDK)andtheAndroidSDK.AndroidappscanbebuiltonWindows,Linux,andMac.AppsforiOScanonlybebuiltonaMac.YouneedtoinstallXCodetobuildtheseapps.
YoucanfindmoredetailsonhowtoinstalltheAndroidSDKathttps://docs.nativescript.org/start/quick-setup.
AfterinstallingtheAndroidSDKorXCode,youcaninstallNativeScriptusingnpm:
npminstallnativescript-g
Youcanseewhetheryoursystemisconfiguredcorrectlybyrunningthefollowingcommand:
tnsdoctor
IfyouonlywanttodevelopappsforiOS,youcanignoretheerrorsonAndroidandviceversa.
WecantestmostpartsoftheappinasimulatorthatisincludedintheSDKorXCode.ScanningaQRcodeonlyworksonadevice.
Tip
SettingupXCodeforiOSdevelopmentiseasierthaninstallingtheAndroidSDK.IfyoucanchoosebetweeniOSandAndroid,youwanttochooseiOS.
CreatingtheprojectstructureInthepreviouschapters,wewroteourTypeScriptsourcesinthelibdirectory.Thestaticordistdirectorycontainedthecompiledsources.However,inthischapter,wehavetomakeadifferentstructuresinceNativeScripthassomerequirementsonit.NativeScriptrequiresthatthecompiledsourcesarelocatedintheappdirectoryanditusesthelibdirectoryforplugins,sowecannotusethatdirectoryforourTypeScriptsources.Instead,wewillusethesrcdirectory.
NativeScriptcanautomaticallycreateabasicprojectstructure.Byrunningthefollowingcommands,aminimalprojectwillbecreated:
tnsinit
npminstall
Thefirstcommandcreatesthepackage.jsonfileandtheappdirectory.NativeScriptstorestheiconsandsplashscreens(whichyouseewhentheappisloading)inapp.Youcaneditthesefileswhenyouwanttopublishanapp.ThenpminstallcommandinstallsthedependenciesthatNativeScriptneeds.Thesedependencieswereaddedtopackage.jsonbythefirstcommand.
Weneedtomakesomeadjustmentstoit.Wemustcreateanapp/package.jsonfile.NativeScriptusesthisfiletogetthemainfileoftheproject:
{
"main":"app.js"
}
AddingTypeScriptBydefault,NativeScriptappsshouldbewritteninJavaScript.WewillnotusegulptocompileourTypeScriptfilessinceNativeScripthasbuilt-insupportfortranspilerslikeTypeScript.WecanaddTypeScripttoitbyrunningthefollowingcommand:
tnsinstalltypescript
Afterrunningthiscommand,NativeScriptwillautomaticallycompileTypeScripttoJavaScript.Thiscommandhascreatedtwofiles:tsconfig.jsonandreferences.d.ts.ThetsconfigfilecontainstheconfigurationforTypeScript.WewilladdtheoutDiroptiontotsconfig.jsonsothatwedonothavetoplacethesourcefilesinthesamedirectionasthecompiledfiles.NativeScriptrequiresthatJavaScriptfilesareplacedintheappfolder.WewillwriteourTypeScriptsourcesinthesrcfolder,andthecompilerwillwritetheoutputtotheappfolder:
{
"compilerOptions":{
"module":"commonjs",
"target":"es5",
"inlineSourceMap":true,
"experimentalDecorators":true,
"noEmitHelpers":true,
"outDir":"app"
},
"exclude":[
"node_modules",
"platforms"
]
}
Thereferences.d.tsfilecontainsareferencetothedefinitionfiles(.d.tsfiles)ofthecoremodulesofNativeScript.
CreatingaHelloWorldpageTogetstartedwithNativeScript,wewillfirstwriteasimpleapp.Insrc/app.ts,wemustregistermainEntrythatwillcreatetheviewoftheapp.TheentryshouldbeafunctionthatreturnsaPage.APageattributeisoneoftheclassesthatNativeScriptusesfortheuserinterface.Wecancreateabasicpageasfollows:
import*asapplicationfrom"application";
import{Page}from"ui/page";
import{Label}from"ui/label";
application.mainEntry=()=>{
constpage=newPage();
constlabel=newLabel();
label.text="Hello,World";
page.content=label;
returnpage;
};
application.start();
Thiswillcreateasinglelabelandaddittothepage.ThecontentofthepageshouldbeaViewclass,whichisthebaseclassthatallcomponents(includingLabel)inNativeScriptinherit.
YoucanruntheappwithoneofthefollowingcommandsforAndroidandiOS,respectively:
tnsrunandroid--emulator
tnsrunios--emulator
Youcanruntheapponadevicebyremoving--emulator.YourdeviceshouldbeconnectedusingaUSBcable.Youcanseeallconnecteddevicesbyrunningtnsdevice.
Tip
NativeScriptprintsalotontheconsoleaftertheoutputofTypeScript.MakesureyoudonotmissanycompileerrorsofTypeScript.
OniOS,theappwillnowlooklikethefollowing:
Inthenextsections,wewillseehowwecanaddeventlistenersandbuildbiggerviews.
CreatingthemainviewThemainviewwillshowalistofrecentscans.Clickingononeoftherecentscansopensadetailspagethatshowsmoredetailsonthescan.WhenauserclicksontheScanbutton,theusercanscanaQRcodeusingthecamera:
First,wecreatethemodelofascaninsrc/model.ts.Weneedtostorethecontent(astring)andthedateofthescan:
exportinterfaceScan{
content:string;
date:Date;
}
Insrc/view/main.ts,wewillcreatetheview.Theviewshouldexportafunctionthatcreatesthepage,sowecanuseitasthemainEntry.Italsoneedstoexportafunctionthatcanupdatethecontent.Theviewhastwocallbacksorevents:oneiscalledwhenanitemisclickedandtheotheriscalledwhentheuserclicksontheScanbutton.ThiscanbeimplementedbyaddingthetwocallbacksasargumentsofthecreatePagefunctionandreturningsetItems,whichupdatesthecontentofthelist,andcreateView,whichcreatesthePage,asanobject:
import{Page}from"ui/page";
import{ActionBar,ActionItem}from"ui/action-bar";
import{ListView}from"ui/list-view";
exportfunctioncreatePage(itemCallback:(index:number)=>void,scanCallback:
()=>void){
letitems:string[]=[];
letlist:ListView;
return{setItems,createView};
functionsetItems(value:string[]){
items=value;
if(list){
list.items=items;
list.refresh();
}
}
AnActionBaristhebaratthetopofthescreenwiththeappname.WeaddanActionItemattributetoit,whichisabuttoninthebar.WeuseaListViewattributetoshowtherecentscansinalist.Elementshaveanonmethod,whichweusetolistentoevents,similarto
addEventListenerinwebsitesandoninNodeJS.
TheitemLoadingeventisfiredwhenaniteminthelistisbeingrendered.Inthatevent,theviewforanitemofthelistshouldbecreated.Thetapeventisfiredwhentheusertapsonthescanbutton.TheitemCallbackeventwillbeinvokedwiththeindexoftheitemwhenthathappens.
First,wecreatetheactionbar.Weaddittothepageandaddabuttontotheactionbar:
functioncreateView(){
constpage=newPage();
constactionBar=newActionBar();
actionBar.title="QRScanner";
constbuttonScan=newActionItem();
buttonScan.text="Scan";
buttonScan.on("tap",scanCallback);
actionBar.actionItems.addItem(buttonScan);
Next,wecreatethelistasfollows:
list=newListView();
list.items=items;
Finally,weaddeventlistenerstothelist.InitemLoading,wecreateLabel,ifitwasnotcreatedyet,andsetthetextofit.InitemTap,wecallitemCallbackwiththeindexofthetappeditem:
list.on("itemLoading",args=>{
if(!args.view){
args.view=newLabel();
}
(<Label>args.view).text=items[args.index];
});
list.on("itemTap",e=>itemCallback(e.index));
page.actionBar=actionBar;
page.content=list;
returnpage;
}
}
Insrc/app.ts,wecancallthisfunctionandshowtheviewattribute:
import*asapplicationfrom"application";
import{createPage}from"./view/main";
import*asmodelfrom"./model";
letitems:model.Scan[]=[];
constpage=createPage(index=>showDetailsPage(items[index]),scan);
application.mainEntry=page.createView;
application.cssFile="style.css";
application.start();
Wewillimplementscanninglateron.Fornow,wewillalwaysaddafakescan,sowecantesttheotherpartsoftheapplication:
functionscan(){
addItem("Lorem");
}
InaddItem,weaddanewscantothelistofscans.Wecallupdate,whichwillupdatethelistinthemainviewandshowthedetailspagewiththisscan.Welimittheamountofscansinthelistby100:
functionaddItem(content:string){
constitem:model.Scan={
content,
date:newDate()
};
items=[item,...items].slice(0,100);
update();
showDetailsPage(item);
}
Wewillimplementthedetailspageinthenextsection.Fornow,wewillonlyaddaplaceholderfunctionsothatwecantesttheotherfunctions:
functionshowDetailsPage(scan:model.Scan){
}
Inupdate,wechangethevaluesinthelisttothenewitems:
functionupdate(){
page.setItems(items.map(item=>item.content));
}
AddingadetailsviewThedetailsviewisshownwhentheuserscansacodeorclicksonanitemintherecentscanslist.Itshowsthecontentofthescanandthedate,asshowninthefollowingscreenshot:
IfthecontentofthescanisaURL,wewillshowabuttontoopenthatlink,asshowninthefollowingscreenshot:
Attheendofthischapter,wewillstylethispageproperly.
Weaddafunctiontosrc/model.tsthatwillreturntruewhenthescan(probably)containsaURL.Weconsiderascanthatcontainsnospacesandbeginswithhttp://orhttps://tobeaURL:
functionstartsWith(input:string,start:string){
returninput.substring(0,start.length)===start;
}
exportfunctionisUrl({content}:Scan){
if(content.indexOf("")!==-1){
returnfalse;
}
returnstartsWith(content,"http://")||startsWith(content,"https://");
}
Theviewrequiresthescanitselfandoptionallyacallback.Thecallbackwillonlybeprovidedifthescancontainsalinkandthebuttonshouldbeshown.
NativeScripthasvariouswaystoshowmultipleelementsonapage.Apagecanonlycontainasinglecomponent,butNativeScripthascomponentsthatcancontainmultiplecomponents.Thesearecalledlayouts.Thesimplestone,andprobablyalsothemostused,istheStackLayout.Elementswillbeplacedbeloworbesideeachother.TheStackLayouthasapropertyorientationthatindicateswhethertheelementsshouldbeplacedbelow(vertical,default)orbeside(horizontal)eachother.
Otherlayoutsincludethefollowing:
DockLayout:Elementscanbeplacedontheleft,right,top,bottom,orcenterofthecomponent.GridLayout:Elementsareplacedinoneormultiplerowsandcolumnsinagrid.Thisisequaltoa<table>taginHTML.WrapLayout:Arowisfilledwithelements.Whenitisfull,thenextelementsareaddedtoanewrow.
Tip
Youcanfindallcomponentsathttp://docs.nativescript.org/ui/ui-viewsandalllayoutcontainersathttp://docs.nativescript.org/ui/layout-containers.
Insrc/view/details.ts,wewillimplementthispage:
import{EventData}from"data/observable";
import{topmost}from"ui/frame";
import{Page}from"ui/page";
import{ActionBar,ActionItem}from"ui/action-bar";
import{Button}from"ui/button";
import{Label}from"ui/label";
import{StackLayout}from"ui/layouts/stack-layout";
import*asmodelfrom"../model";
exportfunctioncreateDetailsPage(scan:model.Scan,callback?:()=>void){
return{createView};
functioncreateView(){
constpage=newPage();
constlayout=newStackLayout();
page.content=layout;
Inalabel,wewillshowthecontentofthescan.Wecanaddaclassnametoit,justlikeyouwoulddoonanHTMLwebpage.Lateron,wecanstylethispageusingCSS:
constlabel=newLabel();
label.text=scan.content;
label.className="details-content";
layout.addChild(label);
Thedateofthescanwillbeshowninasecondlabel:
constdate=newLabel();
date.text=scan.date.toLocaleString("en");
layout.addChild(date);
Ifacallbackisprovided,weshowabuttonthatwillopenthelinkofthescan:
if(callback){
constbutton=newButton();
button.text="Open";
button.on("tap",callback);
layout.addChild(button);
}
returnpage;
}
}
Insrc/app.ts,wecannowimplementtheshowDetailsPagefunction.Usingtopmost().navigate,wecannavigatetothepage.UserscangobacktothemainpagewiththestandardbackbuttonofAndroidoriOS,whichisautomaticallyshown:
import{topmost}from"ui/frame";
import{openUrl}from"utils/utils";
import{createDetailsPage}from"./view/details";
...
functionshowDetailsPage(scan:model.Scan){
letcallback:()=>void;
if(model.isUrl(scan)){
callback=()=>openUrl(scan.content);
}
topmost().navigate(createDetailsPage(scan,callback).createView);
}
TheopenUrlfunctionopensawebbrowserwiththespecifiedURL.
ScanningQRcodesNativeScripthassupportforplugins.Aplugincanaddextrafunctionality,suchasturningontheflashlightofaphone,vibratingthephone,logginginwithFacebook,orscanningQRcodes.ThesecanbeinstalledusingthecommandlineinterfaceofNativeScript.
WewilluseaNativeScriptplugintoscanQRcodes.ThepluginiscalledNativeScriptBarcodeScanner.ItcanscanQRcodesandotherbarcodeformats.Theplugincanbeinstalledusingthefollowingcommand:
tnspluginaddnativescript-barcodescanner
TypedefinitionsWemustaddadefinitionfiletoimporttheplugin.Theplugindoesnotcontaintypedefinitions,andtypedefinitionsarenotavailableonDefinitelyTypedandTSD.Itisnotnecessarytowritedefinitionsthatarefullycorrect.Weonlyhavetotypethepartsofthelibrarythatweareusing.Weusethescanfunction,whichcantakeanoptionalsettingsobjectandreturnaPromise.Insrc/definitions.d.ts,wewritethefollowingdefinition:
declaremodule"nativescript-barcodescanner"{
functionscan(options?:any):Promise<any>;
}
Tip
Youdonotneedtospecifytheexportkeywordindefinitionfiles.Alldeclarationsinamoduleinadefinitionfileareautomaticallyconsideredtobeexported.
ImplementationThescanfunctioncannowbeimplemented.WeusetheexportedscanfunctionandlistenforPromisetoresolveorreject.WhenPromiseresolves,weaddtheitemtothelistandopenthedetailspage.
Wecanimporttheplugininsrc/app.ts:
import*asbarcodescannerfrom"nativescript-barcodescanner";
Thescanfunctioncannowberewrittenasfollows:
functionscan(){
barcodescanner.scan().then(result=>{
addItem(result.text);
returnfalse;
});
}
Wecanalsoshowamessagewhenthescanfailed.Thisway,theusergetsfeedbackwhenthescanfailed.Wewillshowaquestionaskingwhethertheuserwantstotryagain,asshowninthefollowingscreenshot:
Thiscanbeimplementedbyreplacingthescanfunctionwiththefollowingcode:
import*asdialogsfrom"ui/dialogs";
...
functionscan(){
barcodescanner.scan().then(result=>{
addItem(result.text);
returnfalse;
},()=>{
returndialogs.confirm("Failedtoscanabarcode.Tryagain?")
}).then(tryAgain=>{
if(tryAgain){
scan();
}
});
}
Inthefirstcallback,thescanwassuccessful.Thescanisaddedtotherecentscanlistandthedetailspageshows.Inthesecondcallback,weshowthedialog.Thedialogs.confirmfunctionreturnsapromise,whichwillresolvetoboolean.Inthelastcallback,tryAgainwillbefalseifthescanwassuccessfuloriftheuserclickedontheNobutton.Itwillbetrueiftheuser
clickedontheYesbutton.Inthatcase,wewillshowthebarcodescanneragain.
Tip
Whenyoureturnavalueinthesecondcallback(orcatchcallback),theresultingPromisewillresolvetothatvalue.WhenyoureturnPromise,theresultingPromisewillberesolvedorrejectedwiththevalueorerrorofthatPromise.IfyouwanttorejecttheresultingPromise,youmustusethrow.
TestingonadeviceIntheemulator,wecannottakeapictureofaQRcode;thus,wehavetotesttheapponadevice.WecandothatbyconnectingthedeviceusingaUSBcableandthenrunningtnsrunandroidortnsrunios.YoucantesttheappusingtheseQRcodes,whichcontaintext(leftimage)andaURL(rightimage).YoucanscantheQRcodesseveraltimesandnoticethelistbuildupinthemainview.Whenyourestarttheapp,youwillseethatthelistiscleared.Wewillfixthatinthenextsection.
AddingpersistentstorageWhentheuserclosesandreopenstheapp,theuserseesanemptylistofscans.Wecanmakethelistpersistentbysavingitafterascanandloadingitwhentheappstarts.Wecanusetheapplication-settingsmoduletostorethescans.Thestorageisbasedonkey-value:avalueisassignedtoaspecifickey.
Onlybooleans,numbers,andstringscanbestoredusingthismodule.Anarraycannotbestored.Instead,onecouldstorethelengthunderonekey(forinstance,items-length)andtheitemsunderasetofkeys(items-0,items-1,...).AneasierapproachistoconvertthearraytoastringusingJSON.
Thelistcanbesavedusingthefollowingfunction:
functionsave(){
applicationSettings.setString("items",JSON.stringify(items));
}
TheDateobjectsareconvertedtostringsbyJSON.stringify.Thus,wemustconvertthembacktoaDateobjectmanually:
functionload(){
constdata=applicationSettings.getString("items");
if(data){
try{
items=(<any[]>JSON.parse(data)).map(item=>({
content:item.content,
date:newDate(item.date)
}));
}catch(e){}
}
}
Beforeapplication.start(),wemustcalltheloadandupdatefunctionstoshowthepreviousscans:
constpage=createPage(index=>showDetailsPage(items[index]),scan);
application.mainEntry=page.createView;
load();
update();
application.start();
InaddItem,wemustcallsave:
functionaddItem(content:string){
constitem:model.Scan={
content,
date:newDate()
};
items=[item,...items].slice(0,100);
save();
update();
showDetailsPage(item);
}
StylingtheappTheappcanbestyledusingCSS.NotallCSSpropertiesaresupported,butbasicsettingslikefonts,colors,margin,andpaddingwork.Wecanaddastylesheetintheappaddingthefollowingcodebeforeapplication.start():
application.cssFile="style.css";
Wewillchangethestyleofthefollowingpartsoftheapp:
Inapp/style.css,wewillfirstgivetheActionBarabackgroundcolor:
ActionBar{
background-color:#237691;
color:#fefefe;
}
Tip
Thestylesheetmustbeaddedintheappfolder,insteadofsrc.NativeScriptwillonlyloadfilesinsideapp.TypeScriptfilesarecompiledintothatfolder,butthestylesheetshouldalreadybelocatedthere.
Wewilladdsomemargintothelabelsinthelistanddetailspage:
Label{
margin:10px;
}
Themainpageisnowproperlystyled,asshowninthefollowingscreenshot:
Wecanalsostylethelabelonthedetailpage,whichwegaveaclassname.Wemakethetextinthelabelbiggerandcenterthetext:
.details-content{
font-size:28pt;
text-align:center;
margin:10px;
}
Thisresultsinthefollowingdesign:
ComparingNativeScripttoalternativesVariousframeworksthatcanbuildmobileappsexist.LotsofdevelopersuseCordovaorPhonegap.ThesetoolswrapanHTMLpageintoanapp.Theseappsarecalledhybrid,astheycombineHTMLpageswithmobileapps.Theuserinterfaceisnotnativeandcangiveabaduserexperience.
Othertoolshaveanativeinterface,whichgivesagoodlookandfeel.Titanium,NativeScript,andReactNativedothis.Withthesetools,lesscodecanbesharedbetweenawebappandmobileapp.WithReactNative,appscanbewrittenusingtheReactframework.
InNativeScript,programmershaveaccesstoallnativeAPIs.Thedisadvantageofthisisthattheprogrammerwouldwriteplatform-specificcode.NativeScriptalsoincludeswrappersaroundtheseclasses,whichworkonbothAndroidandiOS.Forinstance,theButtonclass,whichweusedinthischapter,isawrapperaroundandroid.widget.ButtononAndroidandUIButtononiOS.
SummaryInthischapter,wecreatedamobileappusingNativeScript.WeusedaplugintoscanQRcodes.Thescansaresaved,sothelistispersistedafterarestartoftheapp.Finally,weaddedcustomstylestoourapp.
Inthenextchapter,wewillbuildaspreadsheetwebappusingReact.Wewilldiscoversomeprinciplesoffunctionalprogrammingandlearnhowwecanhandlethestateofanapplication.Wewillalsoseehowwecanbuildacross-platformapplication.
Chapter6.AdvancedProgramminginTypeScriptInthepreviouschapters,welearnedthebasicsofTypeScriptandweworkedwithvariousframeworks.WewilldiscovermoreadvancedfeaturesofTypeScriptinthischapter.Thischaptercoversthefollowingaspects:
UsingtypeguardsMoreaccuratetypeguardsCheckingnullandundefinedCreatingtaggeduniontypesComparingperformanceofalgorithms
UsingtypeguardsSometimes,youmustcheckwhetheravalueisofacertaintype.Forinstance,ifyouhaveavalueofaclassBase,youmightwanttocheckifitisofacertainsubclassDerived.InJavaScriptyouwouldwritethiswithaninstanceofcheck.SinceTypeScriptisanextensionofJavaScript,youcanalsouseinstanceofinTypeScript.Inothertypedlanguages,likeC#,youmustthenaddatypecast,whichtellsthecompilerthatavalueisofatype,differentfromwhatthecompileranalyzed.Youcanalsoaddtypecastsintwodifferentways.Theoldsyntaxfortypecastsuses<and>,thenewsyntaxusestheaskeyword.Youcanseethembothinthenextexample:
classBase{
a:string;
}
classDerivedextendsBase{
b:number;
}
constfoo:Base;
if(fooinstanceofDerived){
(<Derived>foo).b;
(fooasDerived).b;
}
Whenyouuseatypeguard,yousaytothecompiler:trustme,thisvaluewillalwaysbeofthistype.Thecompilercannotcheckthatandwillassumethatitistrue.But,weareusingacompilertogetnotifiedabouterrorssowewanttoreducetheamountofcaststhatweneed.
Luckily,thecompilercan,inmostcases,understandtheusagesofinstanceof.Thus,inthepreviousexamplethecompilerknowsthatthetypeoffooisDerivedinsidetheif-block.Thus,wedonotneedtypecaststhere:
constfoo:Base;
if(fooinstanceofDerived){
foo.b;
}
Anexpressionthatcheckswhetheravalueisofacertaintypeiscalledatypeguard.TypeScriptsupportsthreedifferentkindsoftypeguards:
Thetypeofguardchecksforprimitivetypes.Itstartstypeofx===ortypeofx!==,followedbystring,number,boolean,object,function,orsymbol.Theinstanceofguardchecksforclasstypes.Suchatypeguardstartswiththevariablename,followedbyinstanceofandtheclassname.Userdefinedtypeguardsacustomtypeguard.Youcandefineacustomtypeguardasafunctionwithaspecialreturntype:
functionisCat(animal:Animal):animalisCat{
returnanimal.name==="Kitty";
}
YoucanthenuseitasisCat(x).
Youcanusethesetypeguardsintheconditionofif,while,for,anddo-whilestatementsandinthefirstoperandofbinarylogicaloperators(x&&y,x||y)andconditionalexpressions(x?y:z).
NarrowingThetypeofavariablewillchange(locally)afteratypeguard.Thisiscallednarrowing.Thetypewillbemorespecificafternarrowing.Morespecificcanmeanthataclasstypeisreplacedbythetypeofasubclass,orthataunionisreplacedbyoneofitsparts.Thelatterisdemonstratedinthefollowingexample:
letx:string|number;
if(typeofx==="string"){
//x:string
}else{
//x:number
}
Asyoucansee,atypeguardcanalsonarrowavariableintheelseblock.
NarrowinganyNarrowingwillgiveamorespecifictype.Forinstance,stringismorespecificthanany.Thefollowingcodewillnarrowxfromanytostring:
letx:any;
if(typeofx==="string"){
//x:string
}
Ingeneral,amorespecifictypecanbeusedonmoreconstructsthantheinitialtype.Forinstance,youcancall.substringonastring,butnotonastring|number.Whennarrowingfromany,thatisnotthecase.Youmaywritex.abcdifxhasthetypeany,butnotwhenitstypeisstring.Inthiscase,amorespecifictypeallowslessconstructswiththatvalue.Topreventtheseissues,thecompilerwillonlynarrowvaluesoftypeanytoprimitivetypes.Thatmeansthatavaluecanbenarrowedtostring,butnottoaclasstype,forinstance.Thenextexampledemonstratesacasewherethecompilerwouldgiveanundesirederror,ifthiswasnotimplemented:
letx:any;
if(xinstanceofObject){
x.abcd();
}
Intheblockafterthetypeguard,xshouldnotbenarrowedtoObject.
CombiningtypeguardsTypeguardscanbecombinedintwoways.First,youcannestifstatementsandthusapplymultipletypeguardstoavariable:
letx:string|number|boolean;
if(typeofx!=="string"){
if(typeofx!=="number"){
//x:boolean
}
}
Secondly,youcanalsocombinetypeguardswiththelogicaloperators(&&,||).Thepreviousexamplecanalsobewrittenas:
letx:string|number|boolean;
if(typeofx!=="string"&&typeofx!=="number"){
//x:boolean
}
With||,wecancheckthatavaluematchesoneofmultipletypeguards:
letx:string|number|boolean;
if(typeofx==="string"||typeofx==="number"){
//x:string|number
}else{
//x:boolean
}
Morecomplextypeguardscanbecreatedwithuserdefinedtypeguards.
MoreaccuratetypeguardsBeforeTypeScript2.0,thecompilerdidnotusethecontrolflowoftheprogramfortypeguards.Theeasiestwaytoseewhatthatmeans,isbyanexample:
functionf(x:string|number){
if(typeofx==="string"){
return;
}
x;
}
Thetypeguardnarrowsxtostringintheblockaftertheifstatement.Iftheelseblockexisted,itwouldhavenarrowedxtonumberthere.Outsideoftheifstatement,nonarrowinghappens,becausethecompileronlylooksatthestructureorshapeoftheprogram.Thatmeansthatthetypeofxonthelastlinewouldbestring|number,eventhoughthatlinecanonlybeexecutediftheconditionoftheifstatementisfalseandxcanonlybeanumberthere.Withsometerminology,typeguardswereonlysyntaxdirectedandwereonlybasedonthesyntax,notonthecontrolflowoftheprogram.
AsofTypeScript2.0,thecompilercanfollowthecontrolflowoftheprogram.Thisgivesmoreaccuratetypesaftertypeguards.Thecompilerunderstandsthatthelastlineofthefunctioncanonlybereachedifxisnotastring.Thetypeofxonthelastlinewillnowbenumber.Thisanalysisiscalledcontrolflowbasedtypeanalysis.
AssignmentsPreviously,thecompilerdidnotfollowassignmentsofavariable.Ifavariablewasreassignedintheblockafteranifstatement,thenarrowingwouldnotbeapplied.Thus,inthenextexample,thetypeofxisstring|number,bothbeforeandaftertheassignment:
letx:string|number=...;
if(typeofx==="string"){
x=4;
}
Withcontrolflowbasedtypeanalysis,theseassignmentscanbechecked.Thetypeofxwillbestringbeforetheassignmentandnumberafterit.Narrowingafteranassignmentworksonlyforuniontypes.Thepartsoftheuniontypearefilteredbasedontheassignedvalue.Fortypesotherthanuniontypes,thetypeofthevariablewillberesettotheinitialtypeafteranassignment.
Thiscanbeusedtowriteafunctionthateitheracceptsonevalueoralistofvalues,inoneofthefollowingways:
functionf(x:string|string[]){
if(typeofx==="string")x=[x];
//x:string[]
}
functiong(x:string|string[]){
if(xinstanceofArray){
for(constitemofx)g(item);
return;
}
//x:string
}
Withthesameanalysis,thecompilercanalsocheckforvaluesthatarepossiblynullorundefined.Insteadofgettingruntimeerrorssayingundefinedisnotanobject,youwillgetacompiletimewarningthatavariablemightbeundefinedornull.
CheckingnullandundefinedTypeScript2.0introducestwonewtypes:nullandundefined.YouhavetosetthecompileroptionstrictNullCheckstotruetousethesetypes.Inthismode,allothertypescannotcontainundefinedornullanymore.Ifyouwanttodeclareavariablethatcanbeundefinedornull,youhavetoannotateitwithauniontype.Forinstance,ifyouwantavariablethatshouldcontainastringorundefined,youcandeclareitasletx:string|undefined;.
Beforeassignments,thetypeofthevariablewillbeundefined.Assignmentsandtypeguardswillmodifythetypelocally.
GuardagainstnullandundefinedTypeScripthasvariouswaystocheckwhetheravariablecouldbeundefinedornull.Thenextcodeblockdemonstratesthem:
letx:string|null|undefined=...;
if(x!==null){
//x:string|undefined
}
if(x!==undefined){
//x:string|null
}
if(x!=null){
//x:string
}
if(x){
//x:string
}
Thelasttypeguardcanhaveunexpectedbehavior,soitisadvisedtousetheothersinstead.Atruntime,xisconvertedtoaBoolean.nullandundefinedarebothconvertedtofalse,non-emptystringstotrue,butanemptystringisconvertedtofalse.Thelatterisnotalwaysdesired.
Tocheckforastring,youcanalsousetypeofx==="string"asatypeguard.Itisnotalwayspossibletowriteatypeguardforsometypes,butyoucanalwaysusethetypeguardsinthecodeblock.
ThenevertypeTypeScript2.0alsointroducedthenevertype,whichrepresentsanunreachablevalue.Forinstance,ifyouwriteafunctionthatalwaysthrowsanerror,itsreturntypewillbenever.
functionalwaysThrows(){
thrownewError();
}
Inauniontype,neverwilldisappear.Formally,T|neverandnever|TareequaltoT.Youcanusethistocreateanassertionthatacertainpositioninyourcodeisunreachable:
functionunreachable(){
thrownewError("Shouldbeunreachable");
}
functionf(){
switch(...){
case...:
returntrue;
case...:
returnfalse;
default:
returnunreachable();
}
Thecompilertakestheunionofthetypesofallexpressionsinreturnstatements.Thatgivesboolean|neverinthisexample,whichisreducedtoboolean.
WewillusestrictNullChecksinthenextchapters.
CreatingtaggeduniontypesWithTypeScript2.0,youcanaddatagtouniontypesandusetheseastypeguards.Thatfeatureiscalled:discriminateduniontypes.Thissoundsverydifficult,butinpracticeitisveryeasy.Thefollowingexampledemonstratesit:
interfaceCircle{
type:"circle";
radius:number;
}
interfaceSquare{
type:"square";
size:number;
}
typeShape=Circle|Square;
functionarea(shape:Shape){
if(shape.type==="circle"){
returnshape.radius*shape.radius*Math.PI;
}else{
returnshape.size*shape.size;
}
}
Theconditionintheifstatementsworksasatypeguard.Itnarrowsthetypeofshapetocircleinthetruebranchandsquareinthefalsebranch.
Tousethisfeature,youmustcreateauniontypeofwhichallelementshaveapropertywithastringvalue.Youcanthencomparethatpropertywithastringliteralandusethatasatypeguard.Youcanalsodothatcheckinaswitchstatement,likethenextexample.
functionarea(shape:Shape){
switch(shape.type){
case"circle":
returnshape.radius*shape.radius*Math.PI;
case"square":
returnshape.size*shape.size;
}
}
WehavenowseenthenewmajorfeaturesofthetypesystemofTypeScript2.0.Wewillseemostoftheminactioninthenextchapters.Wewillalsowritesomesimplealgorithmsinthesechapters.Wewilllearnsomebackgroundinformationonwritingandanalyzingalgorithmsinthenextsection.
ComparingperformanceofalgorithmsWewillalsowritesomesmallalgorithmsinthenextchapters.Thissectionshowshowtheperformanceofanalgorithmcanbeestimated.Duringsuchanalysis,itisoftenassumedthatonlyalargeinputgivesperformanceproblems.Theanalysiswillshowhowtherunningtimescaleswhentheinputscales.
Thenextsectionrequiressomeknowledgeofbasicmathematics.However,thissectionisnotforeknowledgeforthenextchapters.Ifyoudonotunderstandapieceofthissection,youcanstillfollowtherestofthebook.
Forinstance,ifyouwanttofindtheindexofanelementinalist,youcanuseaforloop:
functionindexOf(list:number[],item:number){
for(leti=0;i<list.length;i++){
if(list[i]===item)returni;
}
return-1;
}
Thisfunctionloopsoverallelementsofthearray.Ifthearrayhassizen,thenthebodyoftheloopwillbeevaluatedntimes.Wedonotknowhowlongthebodyoftheloopruns.Itcouldbehundredsortensofasecond,butthatdependsonthecomputer.Whenyouruntheprogramtwice,thetimewillprobablynotbeexactlythesame.
Luckily,wedonotneedthesenumbersfortheanalysis.Itisimportanttoseethattherunningtimeofthebodydoesnotdependonthesizeofthearray.
Thefunctionfirstsetsito0,andthenexecutesthecodeintheloopatmostntimes.Intheworstcase,thebodyisexecutedntimesandthefinalreturn-1runs.Therunningtimewillthenbesomething+n*something+something,whereallinstancesofsomethingdonotdependonn.Whentheinputisbigenough,wecanneglectthetimeoftheinitializationofiandthefinalreturn-1.So,foralargen,therunningtimeisapproximatelyn*something.
Big-OhnotationMathematicianscreatedanotationtowritethismoresimply,calledBig-Ohnotation.WhenyousaythattherunningtimeisO(n),youmeanthattherunningtimeisatmostn*something,forabigenoughn.Ingeneral,O(f(n)),wheref(n)isaformula,meansthattherunningtimeisatmostamultipleoff(n).Moreformally,ifyousaythattherunningtimeisO(f(n)),youmeanthatforsomenumbersNandcthefollowingholds:ifn>Nthentherunningtimeisatmostc*f(n).Theconditionn>Nisaformalwayofsaying:ifnisbigenough,thevalueofcisthereplacementofsomething.
Fortheoriginalproblem,thiswouldresultinO(n).Whenyouanalyzesomeotheralgorithms,youcancounthowoftenitcanbeexecutedforeachpieceofcode.Fromtheseterms,youmustchoosethehighestone.Wewillanalyzethenextexample:
functionhasDuplicate(items:number[]){
for(leti=0;i<items.length;i++){
for(letj=0;j<items.length;j++){
if(items[i]===items[j]&&i!==j)returntrue;
}
}
returnfalse;
}
Thefirstline,whereiisdeclared,isonlyevaluatedonce.Thelinewherejisdeclaredisexecutedatmostntimes,becauseitisinthefirstforloop.Theifstatementrunsatmostn*n,orn2times.Thelastlineisevaluatedatmostonce.Thehighesttermoftheseisn2.Thus,thisalgorithmrunsinO(n2).
OptimizingalgorithmsForalargearray,thisfunctionmightbetooslow.Ifwewouldwanttooptimizethisalgorithm,wecanmakethesecondforloopshorter.
functionhasDuplicate(items:number[]){
for(leti=0;i<items.length;i++){
for(letj=0;j<i;j++){
if(items[i]===items[j])returntrue;
}
}
returnfalse;
}
Withtheoldversion,wewouldcompareeverytwoitemstwice,butnowwecomparethemonlyonce.Wealsocanremovethechecki!==j.Itrequiressomemoreworktoanalyzethisalgorithm.Thebodyofthesecondforloopisnowevaluated0+1+2+...+(n-1)times.Thisisasumofntermsandtheaverageofthetermsis(n-1)/2.Thisresultsinn*(n-1)/2,orn2/2-n/2.WiththeBig-Ohnotation,youcanwritethisasO(n2).Thisisthehighestterm,sothewholealgorithmstillrunsinO(n2).Asyoucansee,thereisnodifferenceintheBig-Ohbetweentheoriginalandoptimizedversions.Thealgorithmwillbeabouttwiceasfast,butiftheoriginalalgorithmwaswaytooslow,thisoneisprobablytooslow.Realoptimizationisabithardertofind.
BinarysearchWewillfirsttakealookattheindexOfexample,whichrunsinO(n).Whatifweknewthattheinputisalwaysasortedlist?Insuchacase,wecanfindtheindexmuchfaster.Iftheelementatthecenterofthearrayishigherthanthevaluethatwesearch,wedonothavetotakealookatallelementsontherightsideofthearray.Ifthevalueatthecenterislower,thenwecanforgetallelementsontheleftside.Thisiscalledbinarysearch.Wecanimplementthiswithtwovariables,leftandright,whichrepresentthesectionofthearrayinwhichwearesearching:leftisthefirstelementofthatsection,andrightisthefirstelementafterthesection.Soright-1isthelastelementofthesection.Thecodeworksasfollows:itchoosesthecenterofthesection.Ifthatelementistheelementthatwesearch,wecanstop.Otherwise,wecheckwhetherweshouldsearchontheleftorrightside.Whenleftequalsright,thesectionisempty.Wewillthenreturn-1,sincewedidnotfindtheelement.
functionbinarySearch(items:number[],item:number){
letleft=0;
letright=items.length;
while(left<right){
constmid=Math.floor((left+right)/2);
if(item===items[mid]){
returnmid;
}elseif(item<items[mid]){
right=mid;
}else{
left=mid+1;
}
}
return-1;
}
Whatistherunningtimeofthisalgorithm?Tofindthat,wemustknowhowoftenthebodyoftheloopisevaluated.Everytimethatthebodyisexecuted,thefunctionreturns,orthelengthofthesectionisapproximatelydividedbytwo.Intheworstcase,thelengthofthesectionisconstantlydividedbytwo,untilthesectioncontainsoneelement.Thatelementisthesearchedelementandthefunctionreturnsorthelengthbecomeszeroandthefunctionwhileloopstops.So,wecanaskthequestion:howoftencanyoudividenbytwo,untilitbecomeslessthanone?2logngivesusthatnumber.ThisalgorithmrunsinO(2log(n)),whichisalotfasterthanO(n).However,itonlyworksifthearrayissorted.
Tip
InBig-Ohnotation,O(log(n))andO(2log(n))arethesame.Theyonlydifferbysomeconstantnumber,whichdisappearsinBig-Ohnotation.
Built-infunctionsWhenyouuseotherfunctionsinyouralgorithm,youmustbeawareoftheirrunningtime.WecouldforinstanceimplementindexOflikethis:
functionfastIndexOf(items:number[],item:number){
items.sort();
returnbinarySearch(items,item);
}
Bothlinesofthefunctionareonlyexecutedonce,butO(1)isnottherunningtimeofthisalgorithm!ThisfunctioncallsbinarySearch,andweknowthatthebodyofthewhileloopinthatfunctionruns,atmost,approximately2logntimes.Wedonotneedtoknowhowthefunctionisimplemented,weonlyneedtoknowthatittakesO(2log(n)).Wealsocall.sort()onthearray.Wehavenotwrittenthatfunctionourselvesandwecannotanalyzethecodeforit.Forthesefunctions,youmustknow(orlookup)therunningtime.Forsorting,thatisO(n2log(n)).SoourfastIndexOfisnotfasterthantheoriginalversion,asitrunsinO(n2log(n)).
WecanhoweverusesortingtoimprovethehasDuplicatefunction.
functionhasDuplicate(items:number[]){
items.sort();
for(leti=1;i<items.length;i++){
if(items[i]===items[i-1])returntrue;
}
returnfalse;
}
TheloopcostsO(n)andthesortingcostsO(n2log(n)),sothisalgorithmrunsinO(n2log(n)).Thisisfasterthanourinitialimplementation,thattookO(n2).
Withthisbasicknowledge,youcananalyzesimplealgorithmsandcomparetheirspeedsforlargeinputs.Inthenextchapters,wewillanalyzesomeofthealgorithmsthatwewillwrite.
SummaryInthischapter,wehaveseenvariousnewfeaturesofTypeScript2.0.Inthisrelease,lotsofnewfeaturesformoreaccuratetypeanalysiswereadded.Wehaveseencontrolflowbasedtypeanalysis,nullandundefinedchecking,andtaggeduniontypes.Finally,wehavealsotakenalookatanalyzingalgorithms.Wewillusemostofthesetopicsinthenextthreechapters.InChapter7,SpreadsheetApplicationwithFunctionalProgramming,wewillbuildaspreadsheetapplication.Wewillalsodiscoverfunctionalprogrammingthere.
Chapter7.SpreadsheetApplicationswithFunctionalProgrammingInthischapter,wewillexploreadifferentstyleofprogramming:functionalprogramming.Withthisstyle,functionsshouldonlyreturnsomethingandnothaveothersideeffects,suchasassigningaglobalvariable.Wewillexplorethisbybuildingaspreadsheetapplication.
Userscanwritecalculationsinthisapplication.Thespreadsheetcontainsagridandeveryfieldofthegridcancontainanexpressionthatwillbecalculated.Suchexpressionscancontainconstants(numbers),operations(suchasaddition,multiplying),andtheycanreferenceotherfieldsofthespreadsheet.Wewillwriteaparser,thatcanconvertthestringrepresentationofsuchexpressionsintoadatastructure.Afterwards,wecancalculatetheresultsoftheexpressionswiththatdatastructure.Ifnecessary,wewillshowerrorssuchasdivisionbyzerototheuser.
Wewillbuildthisapplicationusingthefollowingsteps:
SettinguptheprojectFunctionalprogrammingUsingdatatypesforexpressionsWritingunittestsParsinganexpressionDefiningthesheetUsingtheFluxarchitectureCreatingactionsWritingtheviewAdvantagesofFlux
SettinguptheprojectWestartbyinstallingthedependenciesthatweneedinthischapterusingNPM:
npminit-y
npminstallreactreact-dom-save
npminstallgulpgulp-typescriptsmall--save-dev
WesetupTypeScriptwithlib/tsconfig.json:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"noImplicitAny":true,
"jsx":"react"
}
}
Weconfiguregulpingulpfile.js:
vargulp=require("gulp");
varts=require("gulp-typescript");
varsmall=require("small").gulp;
vartsProject=ts.createProject("lib/tsconfig.json");
gulp.task("compile",function(){
returngulp.src(["lib/**/*.ts","lib/**/*.tsx"])
.pipe(ts(tsProject))
.pipe(gulp.dest("dist"))
.pipe(small("client/index.js",{externalResolve:["node_modules"],
outputFileName:{standalone:"client.js"}}))
.pipe(gulp.dest("static/scripts/"));
});
WeinstalltypedefinitionsforReact:
npminstall@types/react@types/react-dom--save
Instatic/index.html,wecreatetheHTMLstructureofourapplication:
<!DOCTYPEHTML>
<html>
<head>
<title>Chapter5</title>
<linkhref="style.css"rel="stylesheet"/>
</head>
<body>
<divid="wrapper"></div>
<scripttype="text/javascript">
varprocess={
env:{
NODE_ENV:"DEBUG"//or"PRODUCTION"
}
};
</script>
<scripttype="text/javascript"src="scripts/client.js"></script>
</body>
</html>
Weaddsomebasicstylesinstatic/style.css.Wewilladdmorestyleslateron:
body{
font-family:'TrebuchetMS','LucidaSansUnicode','LucidaGrande','Lucida
Sans',Arial,sans-serif;
}
a:link,a:visited{
color:#5a8bb8;
text-decoration:none;
}
a:hover,a:active{
color:#406486;
}
FunctionalprogrammingWhenyouaskadeveloperwhatthedefinitionofafunctionis,hewouldprobablyanswersomethinglike"somethingthatdoessomethingwithsomearguments".Mathematicianshaveaformaldefinitionforafunction:
Afunctionisarelationwhereaninputhasexactlyoneoutput.
Thismeansthatafunctionshouldalwaysreturnthesameoutputforthesameinput.Functionalprogramming(FP)usesthismathematicaldefinition.Thefollowingcodewouldviolatethisdefinition:
letx=1;
functionf(y:number){
returnx+y;
}
f(1);
x=2;
f(1);
Thefirstcalltofwouldreturn2,butthesecondwouldreturn3.Thisiscausedbytheassignmenttox,whichiscalledasideeffect.Areassignmenttoavariableorapropertyiscalledasideeffect,sincefunctioncallscangivedifferentresultsafterit.
Itwouldbeevenworseifafunctionmodifiedavariablethatwasdefinedoutsideofthefunction:
letx=1;
functiong(y:number){
x=y;
}
Codelikethisishardtoreadortest.Thesemutationsarecalledsideeffects.Whenapieceofcodedoesnothavesideeffects,itiscalledpure.Withfunctionalprogramming,allormostfunctionsshouldbepure.
CalculatingafactorialWewilltakealookatthefactorialfunctiontoseehowwecansurpassthelimitationsoffunctionalprogramming.Thefactorialfunction,writtenasn!isdefinedas1*2*3*...*n.Thiscanbeprogrammedwithasimpleforloop:
exportfunctionfactorial(x:number){
letresult=1;
for(leti=1;i<=x;i++){
result*=i;
}
returnresult;
}
However,thevalueofiisincreasedintheloop,whichisareassignmentandthusasideeffect.Withfunctionalprogramming,recursionshouldbeusedinsteadofaloop.Thefactorialofxcanbecalculatedusingthefactorialofx-1andmultiplyingitwithx,sincex!=x*(x-1)!forx>1.Thefollowingfunctionispureandsmallerthantheiterativefunction.Callingafunctionfromthesamefunctioniscalledrecursion.
exportfunctionfactorial(x:number):number{
if(x<=1)return1;
returnx*factorial(x-1);
}
Tip
Whenyoudefineafunctionwithrecursion,TypeScriptcannotinferthereturntype.Youhavetospecifythereturntypeyourselfinthefunctionheader.
Wewillusethisfunctionlateron,sosavethisaslib/model/utils.ts.
UsingdatatypesforexpressionsFieldsofthespreadsheetcancontainexpressions,thatcanbecalculated.Tocalculatethesevalues,theinputoftheusermustbeconvertedtoadatastructure,whichcanthenbeusedtocalculatetheresultofthatfield.
Theseexpressionscancontainconstants,operations,referencestootherfieldsoraparenthesizedexpression:
Constants:0,42,10.2,4e6,7.5e8Unaryexpression:-expression,expression!Binaryexpression:expression+expression,expression/expressionReferences:3:1(thirdcolumn,firstrow)Parenthesizedexpression:(expression)
Wewillcreatethesetypesinlib/model/expression.ts.Firstweimportfactorial,sincewewillneeditlateron.
import{factorial}from"./utils";
CreatingdatatypesWecandeclaredatatypesfortheseexpressionkinds.Wedefinethemusingaclass.Wecandistinguishthesekindseasilyusinginstanceof.WecandeclareConstantasfollows:
exportclassConstant{
constructor(
publicvalue:number
){}
}
Tip
Addingpublicorprivatebeforeaconstructorargumentissyntacticsugarfordeclaringthepropertyandassigningtoitintheconstructor:
exportclassConstant{
value:number;
constructor(value:number){
this.value=value;
}
}
AUnaryExpressionhasakind(minusorfactorial)andtheoperandonwhichitisworking.Wedefinethekindusinganenum.Fortheexpression,wereferencetheExpressiontypethatwewilldefinelateron:
exportclassUnaryOperation{
constructor(
publicexpression:Expression,
publickind:UnaryOperationKind
){}
}
exportenumUnaryOperationKind{
Minus,
Factorial
}
Abinaryexpressionalsohasakind(Add,Subtract,Multiply,orDivide)andtwooperands.
exportclassBinaryOperation{
constructor(
publicleft:Expression,
publicright:Expression,
publickind:BinaryOperationKind
){}
}
exportenumBinaryOperationKind{
Add,
Subtract,
Multiply,
Divide
}
Wewillcallthereferencetoanotherfield,aVariable.Itcontainsthecolumnandtherowofthereferencedfield:
exportclassVariable{
constructor(
publiccolumn:number,
publicrow:number
){}
}
Aparenthesizedexpressionsimplycontainsanexpression:
exportclassParenthesis{
constructor(
publicexpression:Expression
){}
}
WecannowdefineExpressionastheuniontypeoftheseclasses:
exporttypeExpression=Constant|UnaryOperation|BinaryOperation|Variable
|Parenthesis;
TheprecedingdefinitionmeansthatanExpressionisaConstant,UnaryExpression,BinaryExpression,VariableorParenthesis.
TraversingdatatypesWecandistinguishtheseclassesusinginstanceof.Wewilldemonstratethatbywritingafunctionthatconvertsanexpressiontoastring.TypeScriptwillchangethetypeofavariableafteraninstanceofcheck.Thesechecksarecalledtypeguards.Inthecodebelow,formulainstanceofConstantnarrowsthetypeofformulatoConstantintheblockaftertheif.Intheelseblock,Constantisremovedfromthetypeofformula,resultinginUnaryOperation|BinaryOperation|Variable|Parenthesis.
Usingasequenceofifstatements,wecandistinguishallcases.Foraconstant,wecansimplyconvertthevaluetoastring:
exportfunctionexpressionToString(formula:Expression):string{
if(formulainstanceofConstant){
returnformula.value.toFixed();
ForaUnaryOperation,weshowtheoperatorbeforeoraftertherestoftheexpression.WeconverttheresttoastringusingrecursionandwecallexpressionToStringontheexpression.Becauseofthat,wehadtospecifythereturntypemanually:
}elseif(formulainstanceofUnaryOperation){
const{expression,kind}=formula;
switch(kind){
caseUnaryOperationKind.Factorial:
returnexpressionToString(expression)+"!";
caseUnaryOperationKind.Minus:
return"-"+expressionToString(expression);
}
WeconvertaBinaryOperationtoastringbyinsertingtheoperatorbetweentheconvertedoperands:
}elseif(formulainstanceofBinaryOperation){
const{left,right,kind}=formula;
constleftString=expressionToString(left);
constrightString=expressionToString(right);
switch(kind){
caseBinaryOperationKind.Add:
returnleftString+"+"+rightString;
caseBinaryOperationKind.Subtract:
returnleftString+"-"+rightString;
caseBinaryOperationKind.Multiply:
returnleftString+"*"+rightString;
caseBinaryOperationKind.Divide:
returnleftString+"/"+rightString;
}
Avariableisshownasthecolumn,acolonandtherow:
}elseif(formulainstanceofVariable){
const{column,row}=formula;
returncolumn+":"+row;
Aparenthesizedexpressionisshownasthecontainingexpressionwrappedinparentheses:
}elseif(formulainstanceofParenthesis){
const{expression}=formula;
return"("+expressionToString(expression)+")";
}
}
Thisfunctionisagoodexampleofwalkingthrough(traversing)adatastructurewithrecursion.Suchafunctioncanbewritteninthefollowingsteps:
Distinguishdifferentcases(forinstanceusinginstanceofortypeof)Handlethecontainingnodesrecursively(forinstance,leftandrightofaBinaryOperation)Combinetheresults
Inthenextsession,wewillwriteanotherfunctionthattraversesanexpressiontovalidateit.
ValidatinganexpressionWhenyouarewritingafunctionwithrecursion,youshouldalwaysbesurethatyouarenotcreatinginfiniterecursion,similartoaninfiniteloop.Forinstance,whenyouforgetthebasecasesofthefactorialfunction(x<=1),youwouldgetinfiniterecursion.
Wewouldalsogetrecursionwhenafieldofthespreadsheetreferencesitself(directlyorindirectly).Topreventtheseissues,wewillvalidateanexpressionbeforecalculatingit.Wecreatetherestrictionthatareferenceshouldnotpointtoitselfanditmaynotreferenceahighercolumnorrowindex.
Lateron,wewillalsoshowerrorswhenanumberisdividedbyzero,whenthefactorialofanegativeornon-integeriscalculated,whenareferencedfieldcontainsanerror,andwhenareferencedfieldcontainstextinsteadofanumber.WedefineaclassFailuretorepresentsuchanerror:
exportclassFailure{
constructor(
publickind:FailureKind,
publiclocation:Expression
){}
}
exportenumFailureKind{
ForwardReference,
SelfReference,
TextNotANumber,
DivideByZero,
FactorialNegative,
FactorialNonInteger,
FailedDependentRow
}
Next,wedefineafunctionwhichgivesastringdescriptionoftheerror:
exportfunctionfailureText({kind}:Failure){
switch(kind){
caseFailureKind.ForwardReference:
return"Thisexpressioncontainsaforwardreferencetoanother
variable";
caseFailureKind.SelfReference:
return"Thisexpressionreferencesitself";
caseFailureKind.TextNotANumber:
return"Thisexpressionreferencesafieldthatdoesnotcontaina
number";
caseFailureKind.DivideByZero:
return"Cannotdividebyzero";
caseFailureKind.FactorialNegative:
return"Cannotcomputethefactorialofanegativenumber";
caseFailureKind.FactorialNonInteger:
return"Thefactorialcanonlybecomputedofaninteger";
caseFailureKind.FailedDependentRow:
return"Thisexpressionreferencesafieldthathasoneormore
errors";
}
}
Nowwecandefineavalidatefunction,whichwillgenerateanarrayoferrors.Thefunctionhastwobasecases:constantsandvariables.
Aconstantcanneverhaveerrors.Avariableisanerrorifitisaselforforwardreference.Foraunary,binary,orparenthesizedexpressionwemustvalidatethechildrenrecursively:
exportfunctionvalidate(column:number,row:number,formula:Expression):
Failure[]{
if(formulainstanceofUnaryOperation||formulainstanceofParenthesis){
returnvalidate(column,row,formula.expression);
}elseif(formulainstanceofBinaryOperation){
return[
...validate(column,row,formula.left),
...validate(column,row,formula.right)
];
}elseif(formulainstanceofVariable){
if(formula.column===column&&formula.row===row){
return[newFailure(FailureKind.SelfReference,formula)];
}
if(formula.column>column||formula.row>row){
return[newFailure(FailureKind.ForwardReference,formula)];
}
return[];
}else{
return[];
}
}
Inthefirstifstatement,thetypeofformulaisUnaryOperation|Parenthesis.Sincebothtypeshavethepropertyexpression,wecanaccessit.
CalculatingexpressionsThelasttraversaliscalculatingtheexpression.Thisfunctionwillreturnanumberifthecalculationsucceeded.Otherwise,itwillreturnalistoferrors.Theargumentsofthefunctionaretheexpressionandafunctionthatgivesthevalueofareferencedfield:
exportfunctioncalculateExpression(formula:Expression,resolve:(variable:
Variable)=>number|Failure[]):number|Failure[]{
Foraconstant,wecansimplyreturnitsvalue:
if(formulainstanceofConstant){
returnformula.value;
TocalculatethevalueofaUnaryOperation,wefirstcalculateitsoperand.Ifthatcontainsanerror,wepropagateit.Otherwise,wecalculatethefactorialorthenegativevalueofit.Forafactorialwealsoshowanerrorifitisnotanon-negativeinteger.Becauseofthetypeguard,TypeScriptnarrowsthetypeofvaluetoanumberintheelseblock:
}elseif(formulainstanceofUnaryOperation){
const{expression,kind}=formula;
constvalue=calculateExpression(expression,resolve);
if(valueinstanceofArray){
returnvalue;
}else{
switch(kind){
caseUnaryOperationKind.Factorial:
if(value<0){
return[newFailure(FailureKind.FactorialNegative,
formula)];
}
if(Math.round(value)!==value){
return[newFailure(FailureKind.FactorialNonInteger,
formula)];
}
returnfactorial(Math.round(value));
caseUnaryOperationKind.Minus:
return-value;
}
}
Forabinaryoperation,wecalculatetheleftandrightside.Ifoneofthesecontainserrors,wereturnthose.Otherwiseweapplytheoperatortobothvalues:
}elseif(formulainstanceofBinaryOperation){
const{left,right,kind}=formula;
constleftValue=calculateExpression(left,resolve);
constrightValue=calculateExpression(right,resolve);
if(leftValueinstanceofArray){
if(rightValueinstanceofArray){
return[...leftValue,...rightValue];
}
returnleftValue;
}elseif(rightValueinstanceofArray){
returnrightValue;
}else{
switch(kind){
caseBinaryOperationKind.Add:
returnleftValue+rightValue;
caseBinaryOperationKind.Subtract:
returnleftValue-rightValue;
caseBinaryOperationKind.Multiply:
returnleftValue*rightValue;
caseBinaryOperationKind.Divide:
if(rightValue===0){
return[newFailure(FailureKind.DivideByZero,
formula)];
}
returnleftValue/rightValue;
}
}
Foravariable,wedelegatethecalculationtotheresolvefunction:
}elseif(formulainstanceofVariable){
returnresolve(formula);
}elseif(formulainstanceofParenthesis){
returncalculateExpression(formula.expression,resolve);
}
}
Finally,wecalculatethevalueofaparenthesizedexpressionwiththeexpressionitcontains.
ParsinganexpressionAparsercanconvertastringtosomedatatype.Thefirstguessofthetypeofaparserwouldbe:
typeParser<T>=(source:string)=>T;
Sincewewillalsouseaparsertoparseapartofthesource.Forinstance,whenparsingafactorial,wefirstparsetheoperand(whichhopefullyhasonecharacterremaining,theexclamationmark)andthenparsetheexclamationmark.Thus,aparsershouldreturntheresultingdataandtheremainingsource:
typeParser<T>=(source:string)=>[T,string];
Aconstant(suchas5.2)andavariable(5:2)bothstartwithanumber.Becauseofthat,aparsershouldreturnanarraywithalloptions:
typeParser<T>=(source:string)=>[T,string][];
Todemonstratehowthisworks,imaginethattherearetwoparsers:onethatparsesA,onethatparsesAAandonethatparsesAB.ThestringAAAcouldbeparsedwithasequenceoftheseparsersinthreedifferentways:A-A-A,A-AA,andAA-A.NowimaginethattheparserscanfirstparseAorAA,andthenonlyAB.WewillparseAAB.Thefirstpartwouldresultinthefollowingresult:
[
["A","AB"]
["AA","B"]
]
Theremainingstringofthefirstelement(AB),canthenbeparsedbythesecondparser(AB).Thiswouldhaveanemptystringastheremainingpart.Theremainingstringoftheseconditem(B)cannotbeparsed.Thus,theseparsescanparseAABasA-AB.
CreatingcoreparsersWewillfirstcreatetwocoreparsersinlib/model/parser.ts.Thefunctionparserunsaparserandreturnstheresultifsuccessful,epsilonwillalwayssucceedandtokenwilltrytoparseaspecificstring.Thevaluecanbespecifiedasthelastargumentforbothfunctions:
typeParseResult<T>=[T,string][];
typeParser<T>=(source:string)=>ParseResult<T>;
exportfunctionparse<U>(parser:Parser<U>,source:string):U|undefined{
constresult=parser(source)
.filter(([result,rest])=>rest.length===0)[0];
if(!result)returnundefined;
returnresult[0];
}
constepsilon=<U>(value:U):Parser<U>=>source=>
[[value,source]];
consttoken=<U>(term:string,value:U):Parser<U>=>source=>{
if(source.substring(0,term.length)===term){
return[[value,source.substring(term.length)]];
}else{
return[];
}
};
Wewillcombinethesecoreparsersintomorecomplexandusefulparsers.First,wewillcreateafunctionthattriesdifferentparsers:
constor=<U>(...parsers:Parser<U>[]):Parser<U>=>source=>
(<[U,string][]>[]).concat(...parsers.map(parser=>parser(source)));
Wecanusethistoparseadigit.Wecombinetheparsersthatparsethenumber0t09:
constparseDigit=or(
token("0",0),token("1",1),
token("2",2),token("3",3),
token("4",4),token("5",5),
token("6",6),token("7",7),
token("8",8),token("9",9)
);
Tip
Functionsthathavefunctionsasanargumentorreturntypearecalledhighorderfunctions.Thesefunctionscaneasilybereused.Withfunctionalprogramming,youoftencreatesuchfunctions.
RunningparsersinasequenceAnotherwaytocombineparsersisrunningtheminasequence.Beforewecanwritethesefunctions,wemustdefinetwohelperfunctionsinlib/model/utils.ts.flattenwillconvertanarrayofarraysintoanarray.flatMapwillfirstcallmaponthearrayandsecondlyflatten:
exportfunctionflatten<U>(source:U[][]){
return(<U[]>[]).concat(...source);
}
exportfunctionflatMap<U,V>(source:U[],callback:(value:U)=>V[]):V[]{
returnflatten(source.map(callback));
}
Backinlib/model/parser.ts,wedefineamapfunction,whichcanconvertaParser<U>toaParser<V>:
constmap=<U,V>(parser:Parser<U>,callback:(value:U)=>V):Parser<V>=>
source=>
parser(source).map<[V,string]>(([item,rest])=>[callback(item),rest]);
Wealsodefineabindfunction,whichwillrunaparserafteranotherparser:
constbind=<U,V>(parser:Parser<U>,callback:(value:U)=>Parser<V>):
Parser<V>=>source=>
flatMap(parser(source),([result,rest])=>callback(result)(rest));
Withfunctionalprogramming,thetypeofafunctioncansometimesalreadydescribetheimplementation.Whentheimplementationgivesnotypeerrors,theimplementationisinmostcasescorrect.
Nextup,wecreatetwofunctionsthatcanruntwoorthreeparsersinasequenceandcancombinetheresultsoftheseparsersintoaspecifictype:
constsequence2=<U,V,W>(
left:Parser<U>,
right:Parser<V>,
combine:(x:U,y:V)=>W)=>
bind(left,x=>map(right,y=>combine(x,y)));
constsequence3=<U,V,W,T>(
first:Parser<U>,
second:Parser<V>,
third:Parser<W>,
combine:(x:U,y:V,z:W)=>T)=>
bind(first,x=>sequence2(second,third,(y,z)=>combine(x,y,z)));
Withthesefunctions,wecanwriteafunctionthatcanmatchasequenceofanylength,oralist.Alistiseitheroneelementoroneelementfollowedbyalist.Asyoucansee,thisrequiresrecursion.Weneedtheresultingparserinsidethedefinitionoftheparser,whichisnotpossible.Instead,wecancreateafunctionthatwillevaluatetheparser(source=>parser(source)):
functionlist<U>(parseItem:Parser<U>){
constparser:Parser<U[]>=or(
map(parseItem,item=>[item]),
sequence2(
parseItem,
source=>parser(source),
(item,items)=>[item,...items]
)
);
returnparser;
}
Wecanalsocreateaseparatedlistparser,whichwilleitherparseonlyoneelement,orparsethefirstelementandalistofseparatorsanditems.Wecreateaninterfacetostoretheresultofthefunction:
interfaceSeparatedList<U,V>{
first:U;
items:[V,U][];
}
constseparatedList=<U,V>(parseItem:Parser<U>,parseSeparator:Parser<V>)=>
or(
map(parseItem,first=>({first,items:[]})),
sequence2(
parseItem,
list(sequence2(parseSeparator,parseItem,(sep,item)=><[V,U]>[sep,
item])),
(first,items)=>({first,items})
)
);
Wecannowparsealistofdigits:
constparseDigits=list(parseDigit);
Thiscanparsealistofdigits.Wecanconvertthattoanumberwiththemapfunctionthatwehavedefined.Sinceanintegercanbewrittenas1337=1*10^3+3*10^2+3*10^1+7*10^0.Wecanusethereducefunctionofarraysforthis.reduceworksasfollows:[1,2,3,4].reduce(f,0)===f(f(f(f(0,1),2),3),4)
Wecannowdefinetheconversionfunction:
consttoInteger=(digits:number[])=>digits.reduce(
(previous,current,index)=>
previous+current*Math.pow(10,digits.length-index-1),
0
);
Withmap,wecandefineparseInteger:
constparseInteger=map(parseDigits,toInteger);
Avariablecanbeparsedasasequenceofaninteger(thecolumn),acolon,andanotherinteger
(therow):
constparseVariable=sequence3(parseInteger,token(":",undefined),
parseInteger,
(column,separator,row)=>newVariable(column,row));
ParsinganumberAnumberorconstantcanbewritteninthefollowingways:
8(integer)8.5(withdecimalpart)8e4=80000(withexponent)8.5e4=85000(withdecimalpartandexponent)
Wecreatetwoparsers,thatwillparsethedecimalpartandexponentofanumber.Theyfallbacktoadefaultvalue(0and1)incasethenumberdoesnothaveadecimalpartorexponent:
constparseDecimal=or(
epsilon(0),
sequence2(
token(".",undefined),
parseDigits,
(dot,digits)=>toInteger(digits)/Math.pow(10,digits.length)
)
);
constparseExponent=or(
epsilon(1),
sequence2(
token("e",undefined),
parseDigits,
(e,digits)=>Math.pow(10,toInteger(digits))
)
);
Withthesefunctions,wecaneasilydefinetheparseConstantfunction:
constparseConstant=sequence3(
parseInteger,
parseDecimal,
parseExponent,
(int,decimal,exp)=>newConstant((int+decimal)*exp)
);
WecannowdefineaparsercalledparseConstantVariableOrParenthesis,whichwillparseaconstant,variable,orparenthesizedexpression(asthenamesuggests).parseParenthesiswillbeimplementedlateron:
constparseConstantVariableOrParenthesis=or(parseConstant,parseVariable,
parseParenthesis);
OrderofoperationsWhenevaluatinganexpression,theorderofexecutionisimportant.Forinstance,(3*4)+2equals14,while3*(4+2)equals18.Thecorrectevaluationof3*4+2isthefirstone.Anexpressionshouldbeevaluatedinthisorder:
1. Parenthesis2. Multiplicationanddivision3. Additionandsubtraction4. Unaryexpressions
Multipleinstancesofthesamegroupshouldbeevaluatedfromlefttoright,so10-2+3=(10-2)+3.
Twowaysexisttoimplementthis:parsingthesourceintherightorder,orparsingitlefttorightandcorrectingitduringcalculation.Sincewealreadywrotethecalculationpart,wewillparsethesourceintherightorder.Thatisalsotheeasiestoption.
Basedontheserules,theleftorrightsideofamultiplicationordivisioncanneverbeanadditionorsubtraction.Theoperandofaunaryexpressioncanonlybeaconstant,variable,orparenthesizedexpression.Withtheserestrictions,onecancreatethefollowingabstractrepresentation:
Expression←Term|Expression('+'|'-')Term
Term←Factor|Term('*'|'/')Factor
Factor←ConstantVariableOrParenthesis|'-'ConstantVariableOrParenthesis|
ConstantVariableOrParenthesis'!'
Parenthesis←'('Expression')'
ConstantVariableOrParenthesis←Constant|Variable|Parenthesis
Thismeansthatanexpressioniseitherasingleterm,oranadditionandsubtractionofmultipleterms.Atermisafactororamultiplicationanddivisionoffactors.Afactorcanbeaconstant,variableorparenthesizedexpression,optionallywithaminusoranexclamationmark.Withtheserules,anexpressionwillalwaysbeparsedintherightorder.
Wecaneasilyconvertthisabstractrepresentationtoparsers.WestartwithparseFactor,whichcanbebuiltwithorandsequence2.
constparseFactor=or(
parseConstantVariableOrParenthesis,
sequence2(
token("-",undefined),
parseConstantVariableOrParenthesis,
(t,value)=>newUnaryOperation(value,UnaryOperationKind.Minus)
),
sequence2(
parseConstantVariableOrParenthesis,
token("!",undefined),
(value)=>newUnaryOperation(value,UnaryOperationKind.Factorial)
)
);
WecanimplementparseTermandparseExpressionusingthefunctionseperatedList.WewillusereducetotransformthearrayintoaBinaryOperation,justlikeweusedittoconvertanarrayofnumbersintoasinglenumberintoInteger.First,wecreatethefunctionthattransformsthearrayintoaBinaryOperation.
functionfoldBinaryOperations({first,items}:SeparatedList<Expression,
BinaryOperationKind>){
returnitems.reduce(fold,first);
functionfold(previous:Expression,[kind,next]:[BinaryOperationKind,
Expression]){
returnnewBinaryOperation(previous,next,kind);
}
}
WeusethatfunctioninparseTermandparseExpression.
constparseTerm=map(
separatedList(
parseFactor,
or(
token("*",BinaryOperationKind.Multiply),
token("/",BinaryOperationKind.Divide)
)
),
foldBinaryOperations
);
exportconstparseExpression=map(
separatedList(
parseTerm,
or(
token("+",BinaryOperationKind.Add),
token("-",BinaryOperationKind.Subtract)
)
),
foldBinaryOperations
);
WehavenotdefinedparseParenthesisyet.BecauseitdependsonparseExpression,wemustplaceitbelowitsdefinition.However,ifwewoulddefineitherewithconst,itcannotbereferencedinparseConstantVariableOrParenthesis.Insteadwewilldefineitasafunction.
functionparseParenthesis(source:string):ParseResult<Expression>{
returnsequence3(
token("(",undefined),
parseExpression,
token(")",undefined),
(left,expression,right)=>newParenthesis(expression)
)(source);
}
Functionscanbeusedbeforetheirdefinition.Weaddthesourceasanargument,asdefinedintheParsertype.
DefiningthesheetAspreadsheetwillbeagridoffields.Everyfieldcancontainastringoranexpression,asdemonstratedinthefollowingscreenshot:
Inlib/model/sheet.ts,wewilldefinethesheetandcreatefunctionstoparse,showandcalculateallexpressionsinthefield.
First,wewillimporttypesandfunctionsthatwewilluseinthisfile.
import{Expression,Variable,calculateExpression,Constant,Failure,
FailureKind,validate,expressionToString}from"./expression";
import{parse,parseConstant,parseExpression}from"./parser";
Wecandefineafieldasanexpressionorastring,andasheetasagridoffields:
exporttypeField=Expression|string;
exportclassSheet{
constructor(
publictitle:string,
publicgrid:Field[][]
){}
}
Nowwewillwritefunctionsthatgivetheamountofcolumnsandrowsofthesheet.
exportfunctioncolumns(sheet:Sheet){
returnsheet.grid.length;
}
exportfunctionrows(sheet:Sheet){
constfirstColumn=sheet.grid[0];
if(firstColumn)returnfirstColumn.length;
return0;
}
Theusercanwritetextoranexpressioninthefieldsofthespreadsheet.Whenthecontentofafieldstartswithanequalstoken,itisconsideredanexpression.WewriteafunctionparseFieldthatparsesthecontenttoanexpressionifitstartswiththeequalstoken.Otherwise,itwillreturnthestringas-is.
exportfunctionparseField(content:string):Field{
if(content.charAt(0)==="="){
returnparse(parseExpression,content.substring(1));
}else{
returncontent;
}
}
Wealsocreateafunctionthatchangesafieldtoastring.
exportfunctionfieldToString(field:Field){
if(typeoffield==="string"){
returnfield;
}else{
return"="+expressionToString(field);
}
}
Incaseofanexpression,itconvertsittoastringandaddsanequalstokenbeforeit.Otherwise,itjustreturnsthestring.
CalculatingallfieldsWewillwriteafunctionthatcalculatesallexpressionsinthespreadsheet.Afieldthatcontainsanexpressionisconvertedtoanumber,ifthecalculationsucceeded,oranarrayoferrorsotherwise.Afieldthatcontainstextdoesnotneedcalculation,sothecontentisimmediatelyreturned.
Thisyieldsthistypefortheresultofthecalculation:
exporttypeResult=ResultField[][];
exporttypeResultField=string|number|Failure[];
Wewillusetwonestedloopstoloopovereachfield.Thisisnotpure,butitmakesiteasiertoresolvevariablesinexpressions.Whenavalidexpressionistobecalculated,thereferencedfieldswouldalreadybeevaluated.
exportfunctioncalculateSheet({grid}:Sheet){
constresult:ResultField[][]=[];
for(letcolumn=0;column<grid.length;column++){
constcolumnContent=grid[column];
result[column]=[];
for(letrow=0;row<columnContent.length;row++){
result[column][row]=calculateField(column,row);
}
}
returnresult;
Foreachfield,wefirstcheckwhetheritisastring.Ifso,wecanimmediatelyreturnit.Otherwise,wevalidatetheexpression.Iftheexpressionisinvalid,wereturntheerrorsandotherwiseweruncalculateExpressiononit.
functioncalculateField(column:number,row:number):ResultField{
constfield=grid[column][row];
if(typeoffield==="string"){
returnfield;
}else{
consterrors=validate(column,row,field);
if(errors.length!==0)returnerrors;
returncalculateExpression(field,resolveVariable);
}
}
Whenavariablereferenceneedstoberesolved,wecanaccessthecalculatedvaluefromthearrayresult.Ifitcontainsastring,wetrytoconvertittoanumber.
functionresolveVariable(location:Variable):number|Failure[]{
const{column,row}=location;
constvalue=result[column][row];
if(typeofvalue==="string"){
constnum=parse(parseConstant,value);
if(num===undefined){
return[newFailure(FailureKind.TextNotANumber,location)];
}
returnnum.value;
}elseif(valueinstanceofeArray){
return[newFailure(FailureKind.FailedDependentRow,location)];
}else{
returnvalue;
}
}
}
Wehavealreadywrittenaparserthatcanparseaconstant,sowecanreuseithere.Ifitcontainsanarrayoferrors,wereturnanewerror,whichsaysthatareferencedfieldcontainsanerror.Otherwise,thefieldcontainsanumberandwecansimplyreturnthat.
UsingtheFluxarchitectureInReact,everyclasscomponentcanhaveastate.Maintainingastateisasideeffectandnotpure,sowewillnotusethatinthisapplication.Instead,wewilluseStatelessFunctionalComponents,whicharepure.Westillneedtomaintainthestateoftheapplication.WewillusetheFluxarchitecturetodothat.WithFlux,youneedtowriteasmallpieceofnon-pure,buttheotherpartsoftheapplicationcanbewrittenpure.Thearchitecturecanbedividedintotheseparts:
Store:ContainsthestateoftheapplicationView:ReactcomponentsthatrenderthestatetoHTMLAction:Afunctionthatcanmodifythestate(example:renamethespreadsheet)Dispatcher:Ahubmodifiesthestatebyexecutinganaction
SeveralimplementationsofFluxexist.Wewillbuildourown,sothatwecanunderstandtheideasbetterandwecancreateanimplementationthatcanbeproperlytypedusingTypeScript.
Wewillimplementthesepartsinthefollowingsections.
DefiningthestateInlib/model/state.ts,wecandefineaninterfacethatcontainsthestateoftheapplication.Thestateshouldcontainthisinformation:
ActivespreadsheetCalculatedresultsofallexpressionsSelectedcolumnandrowifapopupisopenedContentofthetextboxofthepopupWhetherornotthetextboxofthepopupcontainsasyntaxerror
Thisyieldsthefollowingdeclaration:
import{Sheet,Result}from"./sheet";
exportinterfaceState{
sheet:Sheet;
result:Result;
selectedColumn:number;
selectedRow:number;
popupInput:string;
popupSyntaxError:boolean;
}
Ifthepopupisnotshown,wewillsetselectedColumnandselectedRowtoundefined.Otherwise,thesepropertieswillcontainthecolumnandrowoftheselectedfield.
constemptyRow=["",""];
constemptyGrid=[
emptyRow,
emptyRow
]
exportconstemptySheet=newSheet("Untitled",emptyGrid)
exportconstempty:State={
sheet:emptySheet,
result:emptyGrid,
selectedColumn:undefined,
selectedRow:undefined,
popupInput:"",
popupSyntaxError:false
}
Weshouldalsoconstructthestateoftheapplicationwhenitstarts.Itshouldcontainanemptysheetandthepopupshouldnotbeopen.
CreatingthestoreanddispatcherWewillcreatethestoreanddispatcherinlib/model/store.ts.Thedispatchershouldtakeanactionandexecuteit.Wefirstdefineanactionasafunctionthatmodifiesastate.Sincewecannotassigntothestate,asthatisnotpure,anactionshouldnotadjusttheoldstate,butcreateanewstateobjectwithacertainmodification.
exporttypeAction<T>=(state:T)=>T;
Thedispatchershouldacceptsuchaction.Wedefinethedispatcherasafunctionwithanactionasanargument.
exporttypeDispatch<T>=(action:Action<T>)=>void;
Wecannowcreatethestore.Thestoreshouldfireacallbackwhenthestatechanges.
exportfunctioncreateStore<U>(state:U,onChange:(newState:U)=>void){
constdispatch:Dispatch<U>=action=>{
state=action(state);
onChange(state);
}
returndispatch;
}
Thestorealsoneedsaninitialstate.WeaddthesetwoasargumentstothecreateStorefunction.Thefunctionwillreturnthedispatcher.
CreatingactionsAnactionshouldmodifythestate.Todothat,wewillfirstcreatethreehelperfunctions.Onetomodifyapartofanobject,onetomodifyapartofanarray,andonetoeasilycreateanewarray.
WeusethesameupdatefunctionaswedidinChapter3,Note-TakingAppwithaServer.Weaddthisfunctiontolib/model/utils.ts.
exportfunctionupdate<UextendsV,V>(old:U,changes:V):U{
constresult=Object.create(Object.getPrototypeOf(old));
for(constkeyofObject.keys(old)){
result[key]=(<any>old)[key];
}
for(constkeyofObject.keys(changes)){
result[key]=(<any>changes)[key];
}
returnresult;
}
Wealsocreateafunctionthatchangestheelementatacertainindexofanarray.Theotherelementswillremainatthesamelocation.Wewillusethisfunctiontochangethecontentofafieldofthespreadsheetlateron.
exportfunctionupdateArray<U>(array:U[],index:number,item:U){
return[...array.slice(0,index),item,...array.slice(index+1)];
}
WedefineafunctionrangeMap,whichcreatesanarray.Thecallbackargumentisusedtocreateeachelementofthearray.
exportfunctionrangeMap<U>(start:number,end:number,callback:(index:
number)=>U):U[]{
constresult:U[]=[];
for(leti=start;i<end;i++){
result[i]=callback(i);
}
returnresult;
}
Tip
ThefunctionsupdateandrangeMaparenotpure,sincethefunctionscontainseveralassignments.Sometimesitisnotpossibleorveryhardtowriteafunctionpure.However,thesefunctionskeepthesideeffectslocalandotherfunctionswillnotperceivethatthefunctionispure.
AddingacolumnorarowInlib/model/action.ts,wewillcreatetheactionsforourapplication.Firstwemustimportthetypesandfunctionthatwehavewrittenbefore.
import{State}from"./state";
import{calculateSheet,Field,rows,fieldToString,parseField}from
"./sheet";
import{update,updateArray,rangeMap}from"./utils";
Nowwecancreateanactionthatcalculatesallexpressions.Wewillnotexportthisaction,butwewilluseitinotheractions.
constmodifyResult=(state:State)=>
update(state,{
result:calculateSheet(state.sheet)
});
Withthisdefinition,modifyResultisafunctionthattakesastateandreturnsanupdatedstatewithamodifiedresultproperty.ThisconformstotheActiontypethatwedefinedearlier.
Wecanusethisfunctiontocreatetheactionsthataddaroworcolumn.Ifanewrowneedstobeadded,everycolumnshouldgetanextrafieldattheend.Thisfieldshouldbeempty;itshouldcontaintheemptystring.Afterwards,weneedtoupdatetheresultpropertyofthestate.WewillusethemodifyResultfunctionforthat.
exportconstaddRow=(state:State)=>
modifyResult(update(state,{
sheet:update(state.sheet,{
grid:state.sheet.grid.map(column=>[...column,""])
})
}));
Toaddanewcolumn,wemustaddanewarraywithemptystrings.WewilluserangeMaptocreatesuchanarray.Wecanuserowstogettheamountofrows,andthusthelengthofthenewarray.
exportconstaddColumn=(state:State)=>
modifyResult(update(state,{
sheet:update(state.sheet,{
grid:[
...state.sheet.grid,
rangeMap(0,rows(state.sheet),()=>"")
]
})
}));
Lateron,theseactionscanbetriggeredbydispatch(addRow)ordispatch(addColumn).Wewillseethatinactionwhenwecreatetheview.
ChangingthetitleAddingaroworacolumnisanactionthatdoesnothavearguments.Changingthetitledoesrequireanargument,namelythenewtitle.Sincethedefinitionofanactiondoesnotallowextraarguments,wecannotwriteitasafunctionthatrequiresthenewtitleandthecurrentstate.Instead,wecancreateafunctionthattakesthenewtitle,andthenreturnsafunctionthatrequiresthecurrentstate.Thatwillgivethisdefinition:
exportconstsetTitle=(title:string)=>(state:State)=>
update(state,{
sheet:update(state.sheet,{title})
});
Thisactioncanbefiredbyrunningdispatch(setTitle("Untitled")).Ifyouforgettheargument,orspecifyawrongargument,TypeScriptwillgiveanerror.OtherimplementationsofFluxmakeithardtotypesuchactions.
ShowingtheinputpopupWeneedtocreateseveralactionsforthepopup:
OpenthepopupCloseitToggleit(closeifitisalreadyopen,closeitotherwise)ChangetheinputofthetextboxSavethenewvalue
Toopenthepopup,weneedthecolumnandtherowofthefieldandsavetheminthestate.Wesettheinputofthepopuptothecontentofthatfield,eitherastringortheexpressionconvertedtoastring.Whenthepopupisopened,itcannothaveanysyntaxerrorssowesetthatpropertytofalse.
exportconstpopupOpen=(selectedColumn:number,selectedRow:number)=>(state:
State)=>
update(state,{
selectedColumn,
selectedRow,
popupInput:fieldToString(state.sheet.grid[selectedColumn][selectedRow]),
popupSyntaxError:false
});
Thepopupcanbeclosedbysettingthecolumnandrowtoundefined.
exportconstpopupClose=(state:State)=>
update(state,{
selectedColumn:undefined,
selectedRow:undefined,
popupInput:""
});
Totogglethepopup,wecheckwhetheritopenedinthespecifiedlocation,andcloseitoropenitafterwards.
exportconstpopupToggle=(column:number,row:number)=>(state:State)=>
(column===state.selectedColumn&&row===state.selectedRow)
?popupClose(state):popupOpen(column,row)(state);
Wecanupdatethecontentoftheinputbox:
exportconstpopupChangeInput=(popupInput:string)=>(state:State)=>
update(state,{
popupInput
});
Finally,wecancreateanactionthatsavestheinputinthepopupandclosesit.However,whenthepopupcontainsasyntaxerror,wewillnotclosethepopup,butwewilltelltheuserthattheinputcontainsanerror.Insuchacase,parseFieldwillreturnundefined.Otherwise,wechangethefieldthatisselectedandrecalculatethewholespreadsheet.
exportconstpopupSave=(state:State)=>{
constinput=state.popupInput;
constvalue=parseField(input);
if(value===undefined){
returnupdate(state,{
popupSyntaxError:true
});
}
returnmodifyResult(update(state,{
sheet:update(state.sheet,{
grid:updateArray(state.sheet.grid,state.selectedColumn,
updateArray(state.sheet.grid[state.selectedColumn],state.selectedRow,
value)
)
}),
selectedColumn:undefined,
selectedRow:undefined,
popupInput:""
}));
};
Theseareallactionsofourapplications.
TestingactionsSinceactionsarepurefunctions,wecaneasilytestthem.Theycanbetestedwithoutthestore,dispatcher,andview.Todemonstratethis,wewillwritetestsforaddColumn,addRowandsetTitle.WestartwithimportingAVA,thesefunctionsandsomehelperfunctions.
import*astestfrom"ava";
import{empty}from"../model/state";
import{addColumn,addRow,setTitle}from"../model/action";
import{columns,rows}from"../model/sheet";
WewillwriteatestforaddColumn.Wevalidatethattheamountofcolumnsisincreasedbyoneandthattheamountofrowshasnotbeenchanged.
test("addColumn",t=>{
conststate=addColumn(empty);
t.is(columns(state.sheet),columns(empty.sheet)+1);
t.is(rows(state.sheet),rows(empty.sheet));
});
WewriteatestforaddRowtoo.Thistime,wevalidatethattheamountofcolumnsstayedthesamebuttheamountofrowsincreased.
test("addRow",t=>{
conststate=addRow(empty);
t.is(columns(state.sheet),columns(empty.sheet));
t.is(rows(state.sheet),rows(empty.sheet)+1);
});
ForsetTitle,wecheckthatthetitlehasindeedbeenchangedandthatthegridhasnotchanged.
test("setTitle",t=>{
conststate=setTitle("foo")(empty);
t.is(state.sheet.title,"foo");
t.is(state.sheet.grid,empty.sheet.grid);
});
Tip
Whenyougetabugreport,trytocreateaunittestthatdemonstratesthaterror.Whenyouhavefixedthebug,youcaneasilyvalidateitbyrunningthetestsandyoupreventthebugfromreturninginthefeature.
WritingtheviewTheapplicationwillshowaninputboxatthetopofthescreen,whichisusedtotypethetitleofthespreadsheet.Belowthetitle,atableisshownwhichcontainsallfieldsofthespreadsheet.Whentheuserclicksonafield,apopupiscreatedwhichallowstheusertochangethecontentofthatfield.Ifthefieldcontainserrors,theseerrorsareshowninthepopup:
WewilluseReacttocreatetheviewofourapplication.WithStatelessFunctionalComponents,wecanwritepurefunctionsthatrenderthestate.
RenderingthegridInlib/client/sheet.tsx,wewillimportReactandfunctionsandtypesthatwecreatedbefore:
import*asReactfrom"react";
import{Dispatch}from"../model/store";
import{Expression,expressionToString,failureText}from
"../model/expression"
import{State}from"../model/state";
import{Sheet,Field,Result,ResultField,columns,rows,parseField,
fieldToString}from"../model/sheet";
import{update,rangeMap}from"../model/utils";
import*asactionfrom"../model/action";
WewillrenderthespreadsheetinRenderSheet.Thatfunctionrequiresthestateandthedispatcher.
exportfunctionRenderSheet({state,dispatch}:{state:State,dispatch:
Dispatch<State>}){
const{sheet,result}=state;
constcolumnCount=columns(sheet);
constrowCount=rows(sheet);
Atthetopofthescreen,weshowtheinputbox.Whentheuserchangesthetitle,weadjustthestatetoitwiththesetTitleaction.
return(
<divclassName="sheet">
<inputclassName="sheet-title"value={sheet.title}
onChange={e=>dispatch(action.setTitle((e.targetas
HTMLInputElement).value))}/>
Weshowthetablebelowthetitle.Inthistable,weshowthecalculatedvaluesofallfields.Wealsoshowtwobuttonstoaddanewroworcolumn.Thesebuttonsdispatchtheactionsthatwedefinedearlier.
<table>
<tbody>
<tr>
<th></th>
{rangeMap(0,columnCount,index=><thkey={index}>{
index}</th>)}
<throwSpan={rowCount+1}className="sheet-add-
column">
<ahref="javascript:;"
onClick={()=>dispatch(action.addColumn)}>Add
column</a>
</th>
</tr>
{rangeMap(0,rowCount,renderRow)}
<tr><thcolSpan={columnCount+2}>
<ahref="javascript:;"
onClick={()=>dispatch(action.addRow)}>Addrow</a>
</th></tr>
</tbody>
</table>
</div>
);
WerenderarowintherenderRowfunction.WeuserangeMaptocallthisfunction,andtocallrenderColumn.Reactrequiresthatweusethekeypropertyinaloop.Weassigntherowandcolumnindextoit,sincethesewillbeunique.
functionrenderRow(row:number){
return(
<trkey={row}>
<th>{row}</th>
{rangeMap(0,columnCount,renderColumn)}
</tr>
);
functionrenderColumn(column:number){
return(
<RenderFieldkey={column}column={column}row={row}state={state}
dispatch={dispatch}/>
);
}
}
}
Reactcomponentsshouldstartwithacapitalletter.Normalfunctionsshouldbenamedwithalowerletterasaconvention,butforcomponentswehavetobreakthatrule.
RenderingafieldTorenderafield,wewillfirstquerythecontentofthefieldandcheckwhetherthepopupisopenonthisfield.
functionRenderField({column,row,state,dispatch}:{column:number,row:
number,state:State,dispatch:Dispatch<State>}){
constfield=state.sheet.grid[column][row];
constresult=state.result[column][row];
constopen=state.selectedColumn===column
&&state.selectedRow===row;
Nowwecheckwhetherthefieldcontainstextoranexpression.Incaseofanexpression,itcaneitherbeasuccessfulcalculationorafailedone.Ifitfailed,wewillshowtheamountoferrors.Inthepopup,theusercanreadallerrors.Wegenerateaclassnamebasedonthis,andonwhetherthepopupisopenedinthisfield.
lettext:string;
letclassName:string;
if(typeofresult==="string"){
text=result;
className="field-string";
}elseif(typeofresult==="number"){
text=result.toString();
className="field-value";
}else{
text=result.length===1?"1error":result.length+"errors";
className="field-error";
}
className+="field";
if(open){
className+="field-open";
}
Withthesevariables,wecanrenderthefield.Ifwemustshowthepopup,wewilldothatwithRenderPopup.
return(
<tdclassName={className}>
<spanonClick={()=>dispatch(action.popupToggle(column,row))}>
{text}
</span>
{open?
<RenderPopup
field={field}
content={result}
syntaxError={state.popupSyntaxError}
input={state.popupInput}
dispatch={dispatch}/>
:undefined
}
</td>
);
}
Wedefinethatfunctioninthenextsection.Weattachaneventlistenertothefieldwhichwillopenorclosethefieldwhentheuserclicksonit.
ShowingthepopupWewillshowthepopupinRenderPopup.Thepopupcontainsaninputbox,asave,andcancelbutton:
Ifthefieldcontainsanerror,weshowitbelowthetwobuttons:
Forerrorsotherthansyntaxerrors,weshowthelocationwhereithappened:
Wewillfirststoreallerrorsinavariable.Incaseofasyntaxerror,wecannotgivedetails.Forothererrors,weshowadescriptionandthelocationoftheerror.
functionRenderPopup({field,content,syntaxError,input,dispatch}:{field:
Field,content:ResultField,syntaxError:boolean,input:string,dispatch:
Dispatch<State>}){
leterrors:JSX.Element|JSX.Element[];
if(syntaxError){
errors=<divclassName="failure">
Couldnotparsethisexpression.
</div>;
}elseif(contentinstanceofArray){
errors=content.map((failure,index)=><divclassName="failure"key=
{index.toString()}>
<spanclassName="failure-text">{failureText(failure)}</span>
<spanclassName="failure-source">{expressionToString(failure.location)}
</span>
</div>);
}
Nowwecanbuildthefullview.Weattacheventlistenerstotheinputbox,save,andclosebutton.Wealsowraptheinputboxinaform,suchthattheusercanpressEnter(insteadof
clickingSave)toacceptthechanges.
return(
<divclassName="field-popup">
<formonSubmit={(e)=>{e.preventDefault();dispatch(action.popupSave);}}>
<inputvalue={input}autoFocus
onChange={e=>dispatch(action.popupChangeInput((e.targetas
HTMLInputElement).value))}/>
</form>
<ahref="javascript:;"onClick={()=>dispatch(action.popupSave)}>Save</a>
<ahref="javascript:;"onClick={()=>
dispatch(action.popupClose)}>Cancel</a>
<br/>
{errors}
</div>
);
}
AddingstylesInstatic/style.css,wewilladdsomemorestyles.Wewillmakethetextoftheinputboxforthetitlebigger.
.sheet-title{
font-size:24pt;
margin:0010px;
border:1pxsolid#ccc;
width:200px;
}
Wewilladdabordertothetableandstylethebuttontoaddacolumn.
.sheet>table,.sheettr,.sheetth,.sheettd{
border:1pxsolid#ccc;
border-collapse:collapse;
}
.sheet-add-column>a{
width:70px;
display:block;
}
Wewillstylethefieldssotheyshowtheirvalueproperlyandcancontainapopup.
.field{
position:relative;
}
.field>span{
display:block;
min-width:42px;
font-size:10pt;
height:18px;
padding:3px;
}
.field-value>span{
font-family:Cambria,Cochin,Georgia,Times,TimesNewRoman,serif;
text-align:right;
}
.field-error>span{
color:#aa2222;
}
.field-open{
background-color:#eee;
}
Weaddsomestylestothepopup:
.field-popup{
position:absolute;
left:0px;
top:20px;
z-index:10;
background-color:#eee;
border-bottom:4pxsolid#5a8bb8;
border-right:1pxsolid#ddd;
padding:8px;
width:300px;
}
.field-popup>input{
margin-right:10px;
}
.field-popup>a{
margin-left:10px;
}
Finally,wechangethelooksoferrormessagesinthepopup.
.failure-text{
font-style:italic;
}
.failure-source{
margin-left:10px;
color:#555;
font-family:Cambria,Cochin,Georgia,Times,TimesNewRoman,serif;
}
GluingeverythingtogetherInlib/client/index.tsx,wewillcombineallpartsofourapplication.Wewillcreateacomponentthatcontainsthestateandrenderstheview.Whenthestateisupdatedinthestore,wewillpropagatethattothiscomponentandrendertheviewagain.
import*asReactfrom"react";
import{render}from"react-dom";
import{createStore,Dispatch}from"../model/store";
import{State,empty}from"../model/state";
import{RenderSheet}from"./sheet";
classAppextendsReact.Component<{},State>{
dispatch:Dispatch<State>;
state=empty;
constructor(props:{}){
super(props);
this.dispatch=createStore(this.state,state=>this.setState(state));
}
render(){
return(
<divclassName="sheet">
<RenderSheet
state={this.state}
dispatch={this.dispatch}/>
</div>
);
}
}
Finally,wecanrenderthiscomponentintheHTMLfile.
render(<App/>,document.getElementById("wrapper"));
Wecanviewtheresultbyrunninggulpcompileandopeningstatic/index.htmlinyourbrowser.
AdvantagesofFluxInthissectionyoucanfindsomeoftheadvantagesofusingFlux,thearchitecturethatweusedinthischapter.
Fluxisbasedontheunidirectionalflowofdata.Angularsupportstwowaybindings,whichallowdatatoflowintwodirections.Withthisdataflow,alotofpropertiesmightgetchangedafterasinglechangeismade.Thiscanleadtounpredictablebehaviorinbigapplications.FlowandReactdonothavesuchbindings,butinsteadthereisacleanflowofdata(store|view|action|dispatch|store).
ThepartsofFluxarenotstrictlyboundtoeachother.Thismakesiteasytotestspecificpartsoftheapplicationwithunittests.Wealreadysawthattheactionsdonotdependontheview.
Goingcross-platformSincethepartsofFluxarenotbound,wecan,relatively,replacetheHTMLviewsoftheapplicationwithviewsofadifferentplatform.Theuserinterfacedoesnotstorethestateoftheapplication,butitismanagedinthestore.TheotherpartsneednomodificationwhentheHTMLviewsarereplaced.Thiswaywecanporttheapplicationtoadifferentplatformandgocross-platform.
SummaryWehavebuiltaspreadsheetapplicationwithfunctionalprogramming,React,andFluxinthischapter.Wehavediscoveredthelimitationsoffunctionalprogrammingandlearnedhowwecantakeadvantageofit.Wehavewrittenautomatedunittestsforpartsofthecodethatwehavewritten.Wealsosawhowwecantraversedatastructuresandwriteaparserwithfunctionalprogramming.WiththeFluxarchitecture,welearnthowwecanwritethebiggestpartoftheapplicationwithpurefunctions.
Inthenextchapter,wewillseemoreoffunctionalprogramming.WewillrebuildPac-ManwiththeHTML5canvas.
Chapter8.PacManinHTML5Inthischapter,wewillrecreatePacManwiththeHTML5canvas.Justlikethepreviouschapter,wewillbeusingfunctionalprogramming.WiththeHTML5canvasandJavaScript,youcanplaygamesinthebrowser.
PacManisaclassicgamewheretheplayer(PacMan,theyellowcircle)musteatallofthedots.TheghostsaretheenemiesofPacMan:whenyougetcaughtbyaghost,youlose.Ifyoueatallofthedotswithoutbeingcaughtbyaghost,youwinthegame.
Drawingonacanvasis,justlikemodifyingtheHTMLelementsofapage,asideeffectandthusnotpure.Sincewewillbeusingfunctionalprogramming,wewillcreatesomeabstractionaroundit,similartowhatReactdoes.Wewillbuildasmallnon-pureframeworksowecanusethattobuildtherestofthegamewithpurefunctions.WewillalsousestrictNullChecksinthischapter.Thecompilerwillcheckwhichvaluescanbeundefinedornull.
Wewillbuildthegameinthesesteps:
SettinguptheprojectUsingtheHTML5canvasDesigningtheframeworkDrawingonthecanvasAddingutilityfunctionsCreatingthemodelsDrawingtheviewHandlingeventsCreatingthetimehandlerRunningthegameAddingamenu
SettinguptheprojectTheprojectstructurewillbesimilartothepreviousprojects.Inlib,wewillplaceoursources.Weseparatethefilesfortheframeworkandthegameinlib/frameworkandlib/game.Inlib/tsconfig.json,weconfigureTypeScript:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"strictNullChecks":true
}
}
Intherootdirectory,wesetupgulpingulpfile.js:
vargulp=require("gulp");
varts=require("gulp-typescript");
varsmall=require("small").gulp;
vartsProject=ts.createProject("lib/tsconfig.json");
gulp.task("compile",function(){
returngulp.src("lib/**/*.ts")
.pipe(ts(tsProject))
.pipe(small("game/index.js",{outputFileName:{standalone:"scripts.js"
}}))
.pipe(gulp.dest("static/scripts/"));
});
gulp.task("default",["compile"]);
WecaninstallourdependencieswithNPM.
npminit-y
npminstallgulpgulp-typescriptsmall--save-dev
Finally,wecreateasimpleHTMLfileinstatic/index.html.
<!DOCTYPEHTML>
<html>
<head>
<title>PacMan</title>
</head>
<bodystyle="background-color:black;">
<canvasid="game"width="800"height="600"></canvas>
<scriptsrc="scripts/scripts.js"></script>
</body>
</html>
Beforewestartwritingtheframework,wewillhaveaquicklookathowtheHTML5canvasworks.
UsingtheHTML5canvasTheHTML5canvasisanHTMLelement,justlike<div>.However,thecanvasdoesnotcontainotherHTMLelements,butitcancontainadrawinggeneratedbyJavaScriptcode.Inlib/game/index.tswewillquicklyexperimentwithit.
Wecangetareferencetothecanvasusingdocument.getElementByIdthesamewaywegotareferencetoa<div>element:
constcanvas=<HTMLCanvasElement>document.getElementById("game");
Wecannotdirectlydrawonthecanvas;wehavetogetarenderingcontextfirst.Currently,twokindsofrenderingcontextsexist:atwodimensionalcontextandawebglcontext,usedfor3Drendering.Thewebglcontextisalothardertouse.Luckily,PacManis2D,sowecanusethe2Dcontext:
constcontext=canvas.getContext("2d");
Inaneditorwithcompletions,youcancheckwhichfunctionsexistonthecontext.Forinstance,youcanusecontext.fillRect(10,10,100,100)todrawafilledrectanglefrom10,10to110,110.Thex-axisstartsattheleftsideandgoestotheright,andthey-axisstartsatthetopofthecanvasandgoesdown.
Beforeyoucandrawanythingonthecanvas,youmustsetthedrawingcolor.Thecanvasdistinguishestwodifferentcolorsettings:thefillcolorandthestrokecolor.Thefillcolorisusedtopaintafilledshape.Thestrokecolorisusedtodrawashapethatonlyconsistsofanoutline.
Wecansetthesecolorsusingcontext.fillStyleandcontext.strokeStyle:
context.fillStyle="#ff0000";
context.strokeStyle="#0000ff";
Wecanalsosettheweightofalinewithasimilarproperty.
context.lineWidth=5;
Wecandrawrectangleswiththesestyles.
context.fillRect(10,10,100,100);
context.strokeRect(20,20,100,100);
Thisresultsinthefollowingimage:
SavingandrestoringthestateThecontextalsohasthefunctionssave()andrestore().Withthesefunctions,youcanrestorethecurrentdrawstyles,suchasfillStyle,andlineWidth.restore()resetsthestatetothelasttimethatsave()wascalled,basedonaLIFOstack(LastIn,FirstOut).
Inthefollowingexample,therestoreonposition3resetsthestatetothestatesavedonposition2,andrestoreonposition4resetsittoposition1:
context.save();//1
context.fillStyle="#ff0000";
context.save();//2
context.strokeStyle="#0000ff";
context.restore();//3
context.restore();//4
Wewillusethesefunctionsintheframeworkastheycaneasilybeusedwithrecursion.
DesigningtheframeworkWewilldesigntheframeworkbasedonfunctionalprogramming.Theframeworkwilldoallnon-purework,sothattherestoftheapplicationcanbebuiltwithpurefunctions(exceptforMath.random).
Tip
Strictlyspeaking,Math.randomisnotapurefunction.GiventhatMath.random()isnotalwaysequaltoMath.random(),thatfunctionwillupdatesomeinternalstate.Inpurefunctionallanguages,suchafunctioncanstillexist.Thatfunctiontakesastateandreturnsarandomnumberandanewstate.Sinceeverycalltorandomwillgetadifferentstate,itcanreturndifferentrandomvalues.
Agameconsistsofaneventloop.TheamountofiterationsthatthisloopdoespersecondiscalledFPSorframespersecond.Everystepoftheloop,thegamestateneedstobeupdated.Forinstance,enemiesandtheplayercanmove,andtheplayercaneatdotsinPacMan.Attheendofeachstep,thegamestatemustberedrawn.
Thegamemustalsohandleuserinput.Whentheuserpressestheleftbutton,theplayershouldstartmovingtotheleft.
Wewillsplittheeventloopintothefollowingcomponents:
Theview,whichwilldrawthegameeverystepAtimehandler,whichwillbecalledonceineverystepAneventhandler,whichwillbecalledforeveryeventthatoccurs
Withfunctionalprogrammingitcanoftenbeusefultothinkaboutthetypesoffunctionsbeforeyouwritethem.Wewilltakeaquicklookatthetypesofthesethreecomponents.ImaginethestateisstoredinsomeinterfaceState.Theviewwilltransformthisstateintoapicture.Theviewmightneedthewidthandheightofthecanvas,soweaddtheseasarguments.WewillcreatethedefinitionofaPicturelateron:
functiondraw(state:State,width:number,height:number):Picture
Thetimehandlershouldtransformthestateintoanewstate.Itshouldnothaveanyotherarguments:
functiontimeHandler(state:State):State
Theeventhandleralsotransformsthestate,butitcanuseanextraargument,whichcontainstheeventthathasoccurred:
functioneventHandler(state:State,event:Event):State
Inthenextsections,wewillcreateaframeworkthatmanagesthesethreecomponents.
CreatingpicturesWewillstartbycreatingdatatypesforpictures.Someexamplesofapictureareacircle,aline,text,oracombinationofthose.Suchpicturescanalsobescaled,repositioned(translated),orrotated.Anemptypictureisalsoapicture.
Wedefineapictureastheunionofthesedifferentkinds:
exporttypePicture
=Empty
|Rectangle
|RectangleOutline
|Circle
|CircleOutline
|Line
|Text
|Color
|Translate
|Rotate
|Scale
|Pictures;
Westartbycreatingsomebasictypes.TheEmptypicturecanbedefinedasfollows:
exportclassEmpty{
__emptyBrand:void;
}
Thisclassdoesnotneedanyproperties.However,ifyoudonotaddanypropertiestoaclass,valuesofeverytypewillbeassignabletoit.ThisisbecauseTypeScripthasastructuraltypesystem,andforinstanceastringhas,attheleast,allpropertiesofanemptyclass(thatis,noproperties).Forinstance,astringoranumberisassignabletothatclass.Topreventthat,weaddabrandtotheclass.Abrandisapropertythatdoesnotexistatruntime,butisusedtopreventissueswithstructuraltyping.
Forrectanglesandcircles,wecreatedifferenttypes.Oneisfilled,onehasonlytheoutline.Forsuchoutlines,wecansetthethickness:
exportclassRectangle{
__rectangleBrand:void;
constructor(
publicx=0,
publicy=0,
publicwidth=1,
publicheight=width
){}
}
exportclassRectangleOutline{
__rectangleOutlineBrand:void;
constructor(
publicx=0,
publicy=0,
publicwidth=1,
publicheight=width,
publicthickness=1
){}
}
Ifwedonotaddabrandtothesedefinitions,aRectanglewillbeassignabletoaRectangleOutline.Thesebrandsarealsonecessarytodifferentiatearectangleandacircle:
exportclassCircle{
__circleBrand:void;
constructor(
publicx=0,
publicy=0,
publicwidth=1,
publicheight=width
){}
}
exportclassCircleOutline{
__circleOutlineBrand:void;
constructor(
publicx=0,
publicy=0,
publicwidth=1,
publicheight=width,
publicthickness=1
){}
}
Wedefinealineasalistofpointsandathickness:
exporttypePoint=[number,number];
exporttypePath=Point[];
exportclassLine{
__lineBrand:void;
constructor(
publicpath:Path,
publicthickness:number
){}
}
Next,wedefinethetypefortext:
exportclassText{
__textBrand:void;
constructor(
publictext:string,
publicfont:string
){}
}
WrappingotherpicturesWecanwrapotherpicturesandcreatenewones.Forinstance,wewillchangethecolorofapicturewithColor.WiththisdefinitionwecanwritenewColor("#ff0000",newCircle(0,0,2,2))togetaredcircle:
exportclassColor{
__colorBrand:void;
constructor(
publiccolor:string,
publicpicture:Picture
){}
}
Wecouldalsorepositionapicture.Thisisusuallycalledtranslating.newTranslate(100,100,newCircle(0,0,2,2))drawsacirclearound(100,100)insteadof(0,0):
exportclassTranslate{
__translateBrand:void;
constructor(
publicx:number,
publicy:number,
publicpicture:Picture
){}
}
Asthenamesuggests,Rotaterotatessomeotherpicture:
exportclassRotate{
__rotateBrand:void;
constructor(
publicangle:number,
publicpicture:Picture
){}
}
WecanresizeapicturewithScale.newScale(5,5,newCircle(0,0,2,2))woulddrawacircleof10x10insteadof2x2:
exportclassScale{
__scaleBrand:void;
constructor(
publicx:number,
publicy:number,
publicpicture:Picture
){}
}
Thelastclassprovidesawaytoshowmultiplepicturesasonepicture:
exportclassPictures{
__picturesBrand:void;
constructor(
publicpictures:Picture[]
){}
}
Inlib/framework/draw.ts,wewilldrawthesepicturesonacanvas.Wewillimplementthatfunctionlater;wewillnowonlydefineitsheader:
import{Picture,Rectangle,RectangleOutline,Circle,CircleOutline,Line,
Text,Color,Translate,Rotate,Scale,Pictures,Path}from"./picture";
exportfunctiondrawPicture(context:CanvasRenderingContext2D,item:Picture){
}
Wehavenowdefinedallthedatatypesneededtodrawapicture.WewillcreateeventsbeforeweimplementthedrawPicturefunction.
CreatingeventsTheapplicationcanacceptkeyboardevents.Wewilldistinguishbetweentwokindsofevent:akeypressandakeyrelease.Wewillnotaddmouseevents,butyoucanaddtheseyourself.Wedefinetheseeventsinlib/framework/event.ts.
Everykeyhasacertainkeycode,anumberthatidentifiesakey.Forinstance,theleftarrowkeyhascode37.Wewilladdthekeycodetotheeventclass:
exportconstenumKeyEventKind{
Press,
Release
}
exportclassKeyEvent{
constructor(
publickind:KeyEventKind,
publickeyCode:number
){}
}
Wedefinetheeventsourceasafunctionthatwillbeinvokedeverystep.Itwillreturnalistofeventsthatoccurredinthatstep.
exportfunctioncreateEventSource(element:HTMLElement){
letqueue:KeyEvent[]=[];
consthandleKeyEvent=(kind:KeyEventKind)=>(e:KeyboardEvent)=>{
e.preventDefault();
queue.push(newKeyEvent(
kind,
e.keyCode
));
};
constkeypress=handleKeyEvent(KeyEventKind.Press);
constkeyup=handleKeyEvent(KeyEventKind.Release);
element.addEventListener("keydown",keypress);
element.addEventListener("keyup",keyup);
functionevents(){
constresult=queue;
queue=[];
returnresult;
}
returnevents;
}
Wewillcallthisfunctionineverysteptocheckfornewevents.Inthenextsection,wewillpasstheseeventstotheeventhandler,whichwillupdatethegamestate.
BindingeverythingtogetherInlib/framework/game.ts,wewillbindthesecomponentstogether.Wewillcreateafunctionthatstartstheeventloopandupdatesthestateeverystep.Thefunctionhasthesearguments:
Thecanvaselementonwhichthegamewillbedrawn.Theeventelement.Eventsonthiselementwillbesenttotheeventhandler.Thisdoesnothavetobethesameelementasthecanvas.Anelementneedsfocustogetkeyboardevents.Sincethecanvasdoesnotalwayshavefocus,itcanbebettertolistenforeventsonthebodyelement,ifthereisonlyonegameonthewebpage.Theamountofframespersecond.Theinitialstateofthegame.Afunctionthatdrawsthestate.Thetimehandler.Theeventhandler.
Weregisterthetypeofthestateasagenericortypeargument.Usersofthisfunctioncanprovidetheirowntype.TypeScriptwillautomaticallyinferthistypebasedonthevalueofthestateargument:
import{Picture}from"./picture";
import{drawPicture}from"./draw";
import{createEventSource,KeyEvent}from"./event";
exportfunctiongame<UState>(
canvas:HTMLCanvasElement,
eventElement:HTMLElement,
fps:number,
state:UState,
drawState:(state:UState,width:number,height:number)=>Picture,
timeHandler:(state:UState)=>UState=x=>x,
eventHandler:(state:UState,event:KeyEvent)=>UState){
WithcreateEventSource,whichwehavewrittenbefore,wecangetaneventsourceforthespecifiedelement:
consteventSource=createEventSource(eventElement);
Tosetupdrawing,wemustacquiretherenderingcontext:
constcontext=canvas.getContext("2d")!;
ThefunctiongetContextmayreturnnullwhenthecontexttypeisnotsupported.Thetype2dissupportedinallbrowsersthatsupportacanvas,sowecansafelycastitwithanexclamationmark.Thiscastwillremovethenullabilityfromthetype.Wecreateaninterval,suchthatthestepfunctionwillbecalledmultipletimespersecond,basedonthefpsparameter:
setInterval(step,1000/fps);
WewillusethefunctionrequestAnimationFrametorendertheview.Thisfunctiontakesacallbackthatwillbecalledwhenthebrowserwantstoredrawthepage.Ifthebrowserdoesnotneedtoredraw,orithasnotimeforit,itwillnottrytoredrawit.Ifthedrawfunctionispure,thisdoesnotaffectthegame:
letdrawAnimationFrame=-1;
draw();
functionstep(){
letprevious=state;
for(consteventofeventSource()){
state=eventHandler(state,event);
}
state=timeHandler(state);
if(previous!==state&&drawAnimationFrame===-1){
drawAnimationFrame=requestAnimationFrame(draw);
}
}
Finally,wecreatethedrawfunction.Thisfunctionrendersthepictureinthecenterofthescreen.Acanvashasanx-axisthatgoestotheleftanday-axisthatgoesdown.Inmathematics,however,they-axisgoestothetop.Wewillchoosethelatterandflipthewholepicture.context.restore();willrestorethestatetothestateatcontext.save().Thetransformationsdonotinfluenceanydrawingsafterthedrawfunction,forinstanceinthenextstep:
functiondraw(){
drawAnimationFrame=-1;
const{width,height}=canvas;
context.clearRect(0,0,width,height);
context.save();
context.translate(Math.round(width/2),Math.round(height/2));
context.scale(1,-1);
drawPicture(context,drawState(state,width,height));
context.restore();
}
}
Wewillusethesaveandrestorefunctioninthenextsectiontoo.Wewillthendrawallkindsofpictureonthecanvas.
DrawingonthecanvasInlib/framework/draw.ts,wewillimplementthedrawPicturefunctionthatwecreatedbefore.Usinginstanceofwecancheckwhichkindofpicturewemustdraw.
Wewillinterpretthelocationofanobjectasthecenterofit.Thus,newRectangle(10,10,100,100)willdrawarectanglearound10,10.WecandrawtheoutlineofarectangleorthewholerectanglewithstrokeRectandfillRect:
import{Picture,Rectangle,RectangleOutline,Circle,CircleOutline,Line,
Text,Color,Translate,Rotate,Scale,Pictures,Path}from"./picture";
exportfunctiondrawPicture(context:CanvasRenderingContext2D,item:Picture){
context.save();
if(iteminstanceofRectangleOutline){
const{x,y,width,height,thickness}=item;
context.strokeRect(x-width/2,y-height/2,width,height);
}elseif(iteminstanceofRectangle){
const{x,y,width,height}=item;
context.fillRect(x-width/2,y-height/2,width,height);
Todrawacircle,weusethearcfunction.Thatfunctiondoesnotdrawthecircleitself,butonlyregistersitspath.Wecandrawthelineorfillitusingstrokeorfill.WemustwraparcwithbeginPathandclosePathtodothat:
}elseif(iteminstanceofCircleOutline||iteminstanceofCircle){
const{x,y,width,height}=item;
if(width!==height){
context.scale(1,height/width);
}
context.beginPath();
context.arc(x,y,width/2,0,Math.PI*2);
context.closePath();
if(iteminstanceofCircleOutline){
context.lineWidth=item.thickness;
context.stroke();
}else{
context.fill();
}
Foraline,wemustdosomethingsimilar.WithlineTo,wecandrawonesectionoftheline.Alinedoesnothavetobeclosed;itdoesnothavetoendatthelocationitstarted.Thus,wedonotcallclosePath:
}elseif(iteminstanceofLine){
const{path,thickness}=item;
context.lineWidth=thickness;
context.beginPath();
if(path.length===0)return;
const[head,...tail]=path;
const[headX,headY]=head;
context.moveTo(headX,headY);
for(const[x,y]oftail){
context.lineTo(x,y);
}
context.stroke();
WithfillText,wecandrawtextonthecanvas.Wewillcenterthetext.Wemustalsoscalethetext,sincewehaveflippedthewholecanvasingame.ts.Ifyouforgetthis,thetextwouldbeupsidedown:
}elseif(iteminstanceofText){
const{text,font}=item;
context.scale(1,-1);
context.font=font;
context.textAlign="center";
context.textBaseline="middle";
context.fillText(text,0,0);
Wewilldrawpicturesthatcontainotherpictures,suchasColororPictures,withrecursion.ForColor,wecansimplysetthecoloronthecontext:
}elseif(iteminstanceofColor){
const{color,picture}=item;
context.fillStyle=color;
context.strokeStyle=color;
drawPicture(context,picture);
ForTranslate,Rotate,andScale,wecanusethetranslate,rotate,andscalefunctionsthatexistontherenderingcontext:
}elseif(iteminstanceofTranslate){
const{x,y,picture}=item;
context.translate(x,y);
drawPicture(context,picture);
}elseif(iteminstanceofRotate){
const{angle,picture}=item;
context.rotate(angle);
drawPicture(context,picture);
}elseif(iteminstanceofScale){
const{x,y,picture}=item;
context.scale(x,y);
drawPicture(context,picture);
ForPictures,wecanusealooptorenderallpictures:
}elseif(iteminstanceofPictures){
const{pictures}=item;
for(constpictureofpictures){
drawPicture(context,picture);
}
}
Finally,werestorethestateofthecontext:
context.restore();
}
Wehavenowfinishedtheworkontheframework.Wewilldevelopthegameinthenextsection.
AddingutilityfunctionsWewillwriteseveralutilityfunctionsinlib/game/utils.ts.Withflatten,wewilltransformanarrayofarraysintoonearray.
exportfunctionflatten<U>(source:U[][]):U[]{
return(<U[]>[]).concat(...source);
}
Withupdate,wecanmodifysomepropertiesofanobject.Thisisthesamefunctionasinpreviouschapters.
exportfunctionupdate<UextendsV,V>(old:U,changes:V):U{
constresult=Object.create(Object.getPrototypeOf(old));
for(constkeyofObject.keys(old)){
result[key]=(<any>old)[key];
}
for(constkeyofObject.keys(changes)){
result[key]=(<any>changes)[key];
}
returnresult;
}
Next,wewillcreateafunctionforworkingwithMath.random.randomIntwillreturnarandomintegerinacertainrangeandchancehasachancetoreturntrue:
exportfunctionrandomInt(min:number,max:number){
returnmin+Math.floor(Math.round(
Math.random()*(max-min+1)
));
}
exportfunctionchance(x:number){
returnMath.random()<x;
}
WecancalculatethedifferencebetweentwopointswiththePythagoreantheorem:
exportfunctionsquare(x:number){
returnx*x;
}
exportfunctiondistance(x1:number,y1:number,x2:number,y2:number){
returnMath.sqrt(square(x1-x2)+square(y1-y2));
}
Finally,wewriteafunctionthatcheckswhetheranumberisaninteger:
exportfunctionisInt(x:number){
returnMath.abs(Math.round(x)-x)<0.001;
}
Duetoroundingerrors,wemustcheckthatthevalueisnearaninteger.
CreatingthemodelsInlib/game/model.ts,wewillcreatethemodelsforthegame.Thesemodelswillcontainthestateofthegame,suchasthelocationoftheenemies,walls,anddots.Thestatemustalsocontainthecurrentmovementoftheplayerandthedifficultylevel,asthegamewillhavemultipledifficulties.
UsingenumsWestartwithseveralenums.Wecanstorethedifficultywithsuchanenum:
exportenumDifficulty{
Easy,
Hard,
Extreme
}
Thevaluesofanenumareconvertedtonumbersduringcompilation.TypeScriptgivesthefirstelementzeroasthevalue,thenextitemone,andsoon.Inthisexample,Easyis0,Hardis1,andExtremeis2.However,youcanalsoprovideothervalues.Forsomeapplications,thiscanbeuseful.WewillusecustomvaluestodefineMovement.Thisenumcontainsthefourdirectionsinwhichtheplayercanmove.Incasetheuserdoesnotmove,weuseNone.Wegivethemembersavalue:
exportenumMovement{
None=0,
Left=1,
Right=-1,
Top=2,
Bottom=-2
}
Withthesevalues,wecaneasilycreateafunctionthatcheckswhethertwomovementsareintheoppositedirection:theirsumshouldequalzero:
exportfunctionisOppositeMovement(a:Movement,b:Movement){
returna+b===0;
}
Otherusefulpatternsthatareoftenusedarebitwisevalues.Anumberisstoredinacomputerasmultiplebits.Forinstance,00000011(binary)equals3(decimal).Youcancalculatethedecimalvalueofabinarynumberasfollows.Thefirstpositionfromtherighthasvalue1.Thenexthasvalue2,then4,8,andsoon.Summingthevaluesofthepositionswithaoneresultsinthedecimalvalue.
YoucanusethisbinaryrepresentationtostoremultipleBooleansinanumber.00000011wouldthenmeanthatthefirsttwovaluesaretrue,andtheothervaluesare0.Weuseanenumtodefinethenamesoftheseproperties.<<isthebitwiseshiftoperator.1<<xmeansthat00000001isshiftedxbitstotheleft.Forinstance,1<<4resultsin00010000:
exportenumSide{
Left=1<<0,
Right=1<<1,
Top=1<<2,
Bottom=1<<3,
LeftTop=1<<4,
RightTop=1<<5,
LeftBottom=1<<6,
RightBottom=1<<7
}
Wecancombinemultiplevaluesusingthebitwiseoroperator,|.Abitoftheoutputis1ifatleastoneoftheinputbitsonthatpositionis1.Thus,Side.Left|Side.Rightequals00000011.Wecancheckwhethersomebitistruewiththebitwiseandoperator,&.Abitoftheoutputofthisoperatoris1ifbothinputbitsonthatpositionare1.Forinstance,00000011andSide.Rightresultsin00000010.Thisisnotzero,sotheBooleanvalueofSide.Rightinthatnumberistrue.
Wewillusethislaterontodrawtheedgesofthewalls.Asyoucanseeinthefollowingscreenshot,theedgesofallwallsaredrawn.
StoringthelevelNow,wewilldefineamodelthatcanstorethestateofalevel.Alevelcontainsseveralobjectsthatareplacedinacertainlocation:
exportinterfaceObject{
x:number;
y:number;
}
Anenemyshouldalsocontainthelocationatwhichitistargeted.Anenemymightnotalwaysknowwheretheplayeris,soitcannotalwaysruntowardtheplayer:
exportinterfaceEnemyextendsObject{
toX:number;
toY:number;
}
Forawall,westorethesidesonwhichwallsexist.Theseneighborsareusedtodrawthewalls:
exportinterfaceWallextendsObject{
neighbours:Side;
}
Wecanstoretheseobjectsinthelevel.Wealsostorethesizeofthegrid,thecurrentmovementandthemovementbasedonthekeyboardinputinthelevel:
exportinterfaceLevel{
walls:Wall[];
dots:Object[];
enemies:Enemy[];
player:Object;
width:number;
height:number;
inputMovement:Movement;
currentMovement:Movement;
difficulty:Difficulty;
}
CreatingthedefaultlevelWewillwriteafunctionthatcanparsealevel,basedonastring.Thisallowsustocreatethelevelasfollows:
constdefaultLevel=parseLevel([
"WWWWWWWWWWWWWWW",
"W....E........W",
"W.WWWWW.WWWWW.W",
"W.W...W.W...W.W",
"WE..W.....W...W",
"W.W...W.W...W.W",
"W.WWWWW.WWWWW.W",
"W.............W",
"WWWW.WWWW.WWWW",
"W....WW....W",
"W.WW.WPW.WW.W",
"W.WW.WWWW.WWEW",
"W.............W",
"WWWWWWWWWWWWWWW"
]);
AWmeansthatthereshouldbeawallinthatlocation,Estandsforanenemy,Pfortheplayer,andadotforadotthatPacMancaneat.
Toparsealevel,wewillfirstsplitthesestringsintoanarrayofarrays,ourgrid:
functionparseLevel(data:string[]):Level{
constgrid=data.map(row=>row.split(""));
WewillcreateafunctionmapBoard,whichwilltransformthisgridintoanarrayofobjects.toObjectcreatesanobjectifthegridcontainsthespecifiedcharacterinthatlocation:
return{
walls:mapBoard(toWall),
dots:mapBoard(toObject(".")),
enemies:mapBoard(toEnemy),
player:mapBoard(toObject("P"))[0],
width:grid[0].length,
height:grid.length,
inputMovement:Movement.None,
currentMovement:Movement.None,
difficulty:Difficulty.Easy
};
InmapBoard,wefirstapplythecallbacktoeachelementofthegrid.Wethenflattenthegridtoaonedimensionalarray.Wefilterelementsthatareundefinedoutofthisarray,asthecallbackshouldreturnundefinedwhentheelementinthegridatthatlocationisnottheexpectedkind:
functionmapBoard<U>(callback:(field:string,x:number,y:number)=>U|
undefined):U[]{
constmapped=grid.map((row,y)=>row.map((field,x)=>
callback(field,x,y)));
returnflatten(mapped).filter(item=>item!==undefined)
asU[];
}
Wehavetocastthevalueinthereturn-statement.TheTypeScriptcompilercannotfollowthatthecalltofilteronlypassesthroughvaluesthatarenotundefined.
IntoObject,wecreateafunctionthatwillcreateanobjectifthegridcontainsthespecifiedcharacteratthatposition.Afunctionthatreturnsafunctioncanbeusedtocurry.Curryingmeansthatyoufirstprovidesomearguments,andlaterontheotherarguments.Inthiscase,weprovidethefirstargument,thekindwithinthereturnstatement,someprecedinglines.TheotherargumentsareprovidedbythemapBoardfunction:
functiontoObject(kind:string){
return(value:string,x:number,y:number)=>{
if(value!==kind)returnundefined;
return{x,y};
}Wewillreturnthecontentofafieldofthegridinget.Iftheindex
isoutofbounds,we
}
Wewillreturnthecontentofafieldofthegridinget.Iftheindexisoutofbounds,wereturnundefined:
functionget(x:number,y:number){
constrow=grid[y];
if(!row)returnundefined;
returnrow[x];
}
Weusethisfunctiontocheckfortheneighborsofawall.Usingthebitwisedefinedenum,wecanregisterallsidesonwhichthewallhasaneighbor:
functiontoWall(kind:string,x:number,y:number):Wall|undefined{
if(kind!=="W")returnundefined;
letneighbours:Side=0;
if(get(x-1,y)==="W")neighbours|=Side.Left;
if(get(x+1,y)==="W")neighbours|=Side.Right;
if(get(x,y-1)==="W")neighbours|=Side.Bottom;
if(get(x,y+1)==="W")neighbours|=Side.Top;
if(get(x-1,y-1)==="W")neighbours|=Side.LeftBottom;
if(get(x-1,y+1)==="W")neighbours|=Side.LeftTop;
if(get(x+1,y-1)==="W")neighbours|=Side.RightBottom;
if(get(x+1,y+1)==="W")neighbours|=Side.RightTop;
return{
x,
y,
neighbours
}
}
IntoEnemy,wesettheinitiallocationoftheenemyandthetargetlocationtothesamevalues:
functiontoEnemy(kind:string,x:number,y:number){
if(kind!=="E")returnundefined;
return{
x,
y,
toX:x,
toY:y
};
}
}
Finally,wecreateasmallfunctionthatcheckswhetheranobjectisalignedonthegrid:
exportfunctiononGrid({x,y}:Object){
returnisInt(x)&&isInt(y);
}
Wehavenowcreatedthedefaultlevel.Youcaneasilyaddanotherlevellateron,whenthemainmenuhasbeenadded.
CreatingthestateWestorethestateinanewinterface.Wewillalsodefinethedefaultstateforourgame:
exportinterfaceState{
level:Level;
}
exportconstdefaultState:State={
level:defaultLevel
};
Thisinterfaceis,atthemoment,notveryusefulasyoumightbebetteroffusingtheLevelasthegamestate.However,lateroninthischapter,wewillalsoaddamenuthatshouldexistinthestate.
DrawingtheviewInlib/game/view.ts,wewillrenderthegame.Westartwithimportingtypesthatwedefinedearlier:
import{State,Level,Object,Wall,Side,Menu}from"./model";
import{Picture,Pictures,Translate,Scale,Rotate,Rectangle,Line,Circle,
Color,Text,Empty}from"../framework/picture";
Wewillstorethefontnameinavariable,sowecaneasilychangeitlater:
constfont="Arial";
Indraw,wewillrenderthegame.Fornow,thatmeansonlydrawingthelevel.Lateron,wewilladdamenutothegame:
exportfunctiondraw(state:State,width:number,height:number){
drawLevel(state.level,width,height),
}
WerenderthelevelindrawLevel.Wecalculatethesizeofallobjectswiththesizeofthegridandthecanvas:
functiondrawLevel(level:Level,width:number,height:number){
constscale=Math.min(width/(level.width+1),height/(level.height+
1));
Wescaleandcenterthewholelevelwiththiscalculatedscale:
returnnewScale(scale,scale,
newTranslate(-level.width/2+0.5,-level.height/2+0.5,new
Pictures([
Next,wedrawallobjectsonthecanvas.Weuseseveralfunctionsthatwecreateasfollows:
drawObjects(level.walls,drawWall),
drawObjects(level.walls,drawWallLines),
drawObjects(level.dots,drawDot),
drawObjects(level.enemies,drawEnemy),
drawObject(drawPlayer)(level.player)
]))
);
IndrawObject,wedrawanobjectusingthespecifiedcallback.Wetranslatethepictureoftheobjecttotherightlocation:
functiondrawObject<UextendsObject>(callback:(item:U)=>Picture){
return(item:U)=>
newTranslate(item.x,item.y,callback(item));
}
WithdrawObjects,wecandrawalistofobjects:
functiondrawObjects<UextendsObject>(items:U[],callback:(item:U)=>
Picture){
returnnewPictures(items.map(drawObject(callback)));
}
}
IndrawWall,werenderthebackgroundofawall:
functiondrawWall(){
returnnewColor("#111",newRectangle(0,0,1,1));
}
WerendertheedgesofawallindrawWallLines.Wechecktheneighborsofawallwiththebitwiseenumthatwedefinedearlier.First,welistallpossiblesidesinanarray:
constleftTop:[number,number]=[-0.5,0.5];
constleftBottom:[number,number]=[-0.5,-0.5];
constrightTop:[number,number]=[0.5,0.5];
constrightBottom:[number,number]=[0.5,-0.5];
constwallLines:[Side,Line][]=[
[Side.Left,newLine([leftTop,leftBottom],0.1)],
[Side.Right,newLine([rightTop,rightBottom],0.1)],
[Side.Top,newLine([leftTop,rightTop],0.1)],
[Side.Bottom,newLine([leftBottom,rightBottom],0.1)]
];
Wefilterthisarraywiththebitwiseenum,andcolortheremainingpieces:
functiondrawWallLines({neighbours}:Wall){
constlines=wallLines
.filter(([side])=>(side&neighbours)===0)
.map(([side,line])=>line);
returnnewColor("#0021b3",newPictures(lines));
}
IndrawDot,wewillshowasmallcircleforadot:
functiondrawDot(){
returnnewColor("#f0c0a8",newCircle(0,0,0.2,0.2));
}
Werendertheplayerasacircle.Youcantrytocreatethefamous,eatingPacManyourselflateron:
functiondrawPlayer(){
returnnewColor("#ffff00",newCircle(0,0,0.8,0.8));
}
Wedosomemoreworktodrawanenemy.Theenemywilllookasfollows:
Thebackgroundoftheenemyconsistsofacircleforitshead,arectangleforthebody,andtworotatedrectanglesforthefeet.
functiondrawEnemy(){
constshape=newColor("#ff0000",newPictures([
newCircle(0,0.15,0.6),
newRectangle(0,-0.05,0.6,0.4),
newTranslate(-0.15,-0.25,
newRotate(Math.PI/4,newRectangle(0,0,0.2,Math.SQRT2*0.15))),
newTranslate(0.15,-0.25,
newRotate(Math.PI/4,newRectangle(0,0,0.2,Math.SQRT2*0.15)))
]));
Theeyesconsistoftwowhitecircleswithtwosmallerblackcirclesaspupils.
consteyes=newColor("#fff",newPictures([
newCircle(-0.12,0.15,0.2),
newCircle(0.12,0.15,0.2)
]));
constpupils=newColor("#000",newPictures([
newCircle(-0.12,0.15,0.06),
newCircle(0.12,0.15,0.06)
]));
returnnewPictures([shape,eyes,pupils]);
}
HandlingeventsWewillcreateaneventhandlerinlib/game/event.ts.Theeventhandlermustsetthecorrectmovementdirectioninthestate.Thetimehandlerwillthenusethistoupdatethedirectionoftheplayer.Thestepcanonlydothatwhentheplayerisalignedtothegrid.Iftheplayerisbetweentwofieldsonthegrid,wewillnotchangethedirectionoftheplayer,sincehewillthenprobablyheadintoawall.
WorkingwithkeycodesAneventprovidesthekeycodeofthepressedorreleasedkey.Wecangetthiscodeofacertaincharacterwith"x".charCodeAt(0)(wherexisthecharacter).Thekeycodesofleft,top,right,andbottomare37,38,39,and40.
First,wemustcreateahelperfunctionthattransformsakeycodetotheMovementenumthatwedefinedearlier.Westorethedifferentkeysthatweuseinanewenum:
import{KeyEvent,KeyEventKind}from"../framework/event";
import{State,Movement}from"./model";
import{update}from"./utils";
enumKeys{
Top=38,
Left=37,
Bottom=40,
Right=39,
Space="".charCodeAt(0)
}
NowwecantransformakeycodetoaMovement:
functiongetMovement(key:number){
switch(key){
caseKeys.Top:
returnMovement.Top;
caseKeys.Left:
returnMovement.Left;
caseKeys.Bottom:
returnMovement.Bottom;
caseKeys.Right:
returnMovement.Right;
}
returnundefined;
}
TheeventhandlerwillinvokeeventHandlerPlaying,whichwewilldefinelateroninthissection.Whenweaddamenutotheapplication,wewilladjustthishandler:
exportfunctioneventHandler(state:State,event:KeyEvent){
returneventHandlerPlaying(state,event);
}
IneventHandlerPlaying,weupdatethemovementinthestate.Whentheuserpressesakey,wesetthemovementtothatcorrespondingdirection.Whentheuserreleasesthekeythatmapstothecurrentmovement,wesetthemovementtoNone:
functioneventHandlerPlaying(state:State,event:KeyEvent){
if(eventinstanceofKeyEvent){
constinputMovement=getMovement(event.keyCode);
if(event.kind===KeyEventKind.Press){
if(inputMovement){
returnupdate(state,{
level:update(state.level,{inputMovement})
});
}
}else{
if(inputMovement===state.level.inputMovement){
returnupdate(state,{
level:update(state.level,{inputMovement:Movement.None})
});
}
}
}
returnstate;
}
Wehavenowfinishedtheeventhandlerforthegame.Whentheuserpressesorreleasesakey,thisisupdatedinthestate.However,therealworkisbeingdoneinthetimehandler,whichwecreateinthenextsection.
CreatingthetimehandlerThetimehandlerrequiressomemorework.First,weimportothertypesandfunctions.
import{State,Level,Object,Enemy,Wall,Movement,isOppositeMovement,onGrid,
Difficulty}from"./model";
import{update,randomInt,chance,distance,isInt}from"./utils";
Wedefineastepfunctionsothatwecanaddthemenulateron.
exportfunctionstep(state:State){
returnstepLevel(state);
}
InstepLevel,wecanupdatetheobjectsinthelevel.First,weupdatethelocationoftheenemies.WeusestepEnemy,whichwedefinelateron.
functionstepLevel(state:State):State{
constlevel=state.level;
constenemies=level.enemies.map(enemy=>stepEnemy(enemy,level.player,
level.walls,level.difficulty));
Weupdatethelocationoftheplayerbasedonthecurrentmovement:
constplayer=stepPlayer(level.player,level.currentMovement,level.walls);
Dotsthatareneartheplayer,areeatenbytheplayerandremovedfromthelevel:
constdots=stepDots(level.dots,player);
Wechangethecurrentmovementiftheplayerisalignedonthegridorwhentheywantstomoveintheoppositedirection:
constcurrentMovement=onGrid(player)||
isOppositeMovement(level.inputMovement,level.currentMovement)?
level.inputMovement:level.currentMovement;
Weusethesevaluestoupdatethelevel:
constnewLevel=update(level,{enemies,dots,player,currentMovement});
returnupdate(state,{level:newLevel});
}
Now,wecreateafunctionthatcheckswhetheranobjectcollideswithawall:
functioncollidesWall(x:number,y:number,walls:Wall[]){
for(constwallofwalls){
if(Math.abs(wall.x-x)<1&&Math.abs(wall.y-y)<1){
returntrue;
}
}
returnfalse;
}
Next,wecreateafunctionthatupdatesthepositionofanenemy.Theenemycanwalk0.0125pointsifthedifficultyiseasy,otherwise,theycanmove0.025point.Thesevaluesarechosensothatafteracertainamountofsteps,theenemyhaswalkedexactly1pointonthegrid.Thus,theenemywillalwaysbealignedtothegridagain:
functionstepEnemy(enemy:Enemy,player:Object,walls:Wall[],difficulty:
Difficulty):Enemy{
constenemyStepSize=difficulty===Difficulty.Easy?0.0125:0.025;
let{x,y,toX,toY}=enemy;
Withacertainchance,theenemywilltargetontheplayeragain.Anenemycannotalwaysseewheretheplayeris,andthechancesimulatesthat.Also,theenemywillgetasmalldeviation:
if(chance(1/(difficulty===Difficulty.Extreme?30:10))){
toX=Math.round(player.x)+randomInt(-2,2);
toY=Math.round(player.y)+randomInt(-2,2);
}
Iftheenemyisalignedonthegrid,itcanmoveinalldirections.Otherwise,itcanonlywalkaheadorback:
if(!isInt(x)){
x+=toX>x?enemyStepSize:-enemyStepSize;
}elseif(!isInt(y)){
y+=toY>y?enemyStepSize:-enemyStepSize;
}else{
Theplayerisalignedonthegrid,butthelocationmighthaveasmallroundingerror.Thus,weroundthevalueshere.
x=Math.round(x);
y=Math.round(y);
Towalkaround,wefirstcreateanarrayofalloptions.Then,wefiltertheseoptionsandsortthem.Withachanceof0.2,theenemywillchoosethesecond-bestoption.Otherwise,itwillchoosethebestoption.Thebestoptionistheoptionthatbringstheenemyasclosetotheenemy:
constoptions:[number,number][]=[
[x+enemyStepSize,y],
[x-enemyStepSize,y],
[x,y+enemyStepSize],
[x,y-enemyStepSize]
];
constpossible=options
.filter(([x,y])=>!collidesWall(x,y,walls))
.sort(compareDistance);
if(possible.length!==0){
if(possible.length>1&&chance(0.2)){
[x,y]=possible[1];
}
[x,y]=possible[0];
}
}
return{
x,y,toX,toY
};
Attheendofthisfunction,wedefinethecomparefunctionthatweusedtosortthearray.Suchcomparefunctionsshouldreturnanegativevalueifthefirstargumentcomesafterthesecondargument,andapositivevalueifthefirstargumentshouldcomebeforetheother:
functioncompareDistance([x1,y1]:[number,number],[x2,y2]:[number,
number]){
returndistance(toX,toY,x1,y1)-distance(toX,toY,x2,y2);
}
}
WeupdatethelocationoftheplayerinstepPlayer:
constplayerStepSize=0.04;
functionstepPlayer(player:Object,movement:Movement,walls:Wall[]):Object{
let{x,y}=player;
Whentheplayerisalignedonthegrid,weroundthelocationtoeliminateroundingerrors:
if(onGrid(player)){
x=Math.round(x);
y=Math.round(y);
}
Iftheuserhasnomovement,wedonotmodifytheplayerandwecanreturnitdirectly:
switch(movement){
caseMovement.None:
returnplayer;
Otherwise,weupdatethexorycoordinateoftheplayer:
caseMovement.Left:
x-=playerStepSize;
break;
caseMovement.Right:
x+=playerStepSize;
break;
caseMovement.Top:
y+=playerStepSize;
break;
caseMovement.Bottom:
y-=playerStepSize;
break;
}
Iftheuserwasnotalignedonthegrid,wedonothavetocheckwhethertheplayercollideswithawall.Otherwise,wemustvalidateit.Iftheuserthendoescollidewithawall,wereturntheoldplayerwiththeoldlocation:
if(onGrid(player)&&collidesWall(x,y,walls)){
returnplayer;
}
return{x,y};
}
WecanfilterthedotsbycalculatingthedistancetoPacMan.Whentheyareclosetotheplayer,theyareeatenbyPacManandfilteredout:
functionstepDots(dots:Object[],player:Object){
returndots.filter(dot=>distance(dot.x,dot.y,player.x,player.y)>=0.55)
}
Thetimehandlercannowupdatethestate:theplayermoves,theenemiestrytomovetowardtheplayer,andtheplayercaneatdots.
RunningthegameTostartthegame,wemustcallthegamefunctionwiththedefaultstate,drawfunction,timehandler,andeventhandler.Inlib/game/index.ts,wewritethefollowingcodetostartthegame:
import{game}from"../framework/game";
import{defaultState}from"./model";
import{draw}from"./view";
import{step}from"./step";
import{eventHandler}from"./event";
constcanvas=<HTMLCanvasElement>document.getElementById("game");
game(canvas,document.body,60,defaultState,draw,step,eventHandler);
Wecancompilethegamebyexecutinggulp.Youcanplaythegamebyopeningstatic/index.html.
Asyouwillsee,nothinghappenswhenyouhaveeatenallofthedots,orwhenyougethitbyanenemy.Inthenextsection,wewillimplementamenu.Whentheplayerwinsorloses,wewillshowthismenu.
AddingamenuTofinishoffthegame,wewilladdsomemenustoit.Inthemainmenu,theusercanchooseadifficulty.Theusercanselectanoptionusingthearrowkeysandconfirmusingthespacebar.Themenuwilllooklikethis:
Toimplementthemenu,wemustaddittothestate.Thenwecanrenderthemenuandupdatethemenustateintheeventhandler.Westartbyupdatingthestate.
ChangingthemodelInlib/game/model.ts,wewilladdthemenustothestate.First,wewillcreateanewtypeforthemenu.Themenucontainsatitle,alistofoptions,andtheindexoftheselectedbutton.Eachoptionhasastringandafunctionthatappliestheactionbytransformingthestate:
exportinterfaceMenu{
title:string;
options:[string,(state:State)=>State][];
selected:number;
}
WeaddthemenutotheState:
exportinterfaceState{
menu:Menu|undefined;
level:Level;
}
Themainmenuwillcontainthreebuttons;tostartaneasy,hard,orextremegame.Wewilldefineafunctionthatcanstartthegamewithaspecifieddifficulty:
conststartGame=(difficulty:Difficulty)=>(state:State)=>({
menu:undefined,
level:update(defaultLevel,{difficulty})
});
Nowwecandefinethemainmenu:
exportconstmenuMain:Menu={
title:"PacMan",
options:[
["Easy",startGame(Difficulty.Easy)],
["Hard",startGame(Difficulty.Hard)],
["Extreme",startGame(Difficulty.Extreme)]
],
selected:0
}
Wecandefinetwomoremenus,whichareshownwhentheuserwinsordies:
exportconstmenuWon:Menu={
title:"Youwon!",
options:[
["Back",state=>({menu:menuMain,level:state.level})]
],
selected:0
}
exportconstmenuLost:Menu={
title:"Gameover!",
options:[
["Back",state=>({menu:menuMain,level:state.level})]
],
selected:0
}
Wecanusethismenuinthestartingstateoftheapplication:
exportconstdefaultState:State={
menu:menuMain,
level:defaultLevel
};
Sincethemenuisapartofthedefaultstate,thegamewillstartwiththemenu.Inthenextsections,wewillrenderthemenuandhandleitsevents.
RenderingthemenuWemustupdatelib/game/view.tstodrawthemenuonthecanvas.Wechangethedrawfunction:
exportfunctiondraw(state:State,width:number,height:number){
returnnewPictures([
drawLevel(state.level,width,height),
drawMenu(state.menu,width,height)
]);
}
Next,wecreatedrawMenu,thatwillrenderthelevel.Itwillshowthetitleandthebuttons.Theselectedbuttongetsadifferentcolor:
functiondrawMenu(menu:Menu|undefined,width:number,height:number):
Picture{
if(menu===undefined)returnnewEmpty();
constselected=menu.selected;
constbackground=newColor("rgba(40,40,40,0.8)",new
Rectangle(0,0,width,height));
consttitle=newTranslate(0,200,newScale(4,4,
newColor("#fff",newText(menu.title,font))
));
constoptions=newPictures(menu.options.map(showOption));
returnnewPictures([background,title,options]);
functionshowOption(item:[string,(state:State)=>State],
index:number){
constisSelected=index===selected;
returnnewTranslate(0,100-index*50,newPictures([
newColor(isSelected?"#ff0000":"#000000",
newRectangle(0,0,200,40)),
newColor(isSelected?"#000000":"#ffffff",
newScale(1.6,1.6,newText(item[0],font)))
]));
}
}
Thisfunctionwillnowdrawthemenuwhenitisactive.Wemuststillhandletheeventsofthemenu.Wewilldothatinthenextsection.
HandlingeventsInlib/game/event.ts,wewillhandletheeventsforthemenu.Wemustupdatetheindexoftheselecteditemwhentheuserpressestheupordownkey.Whentheuserpressesspace,weexecutetheactionoftheselectedbutton.First,wemustadjusteventHandlertocalleventHandlerMenuwhenthemenuisvisible.
exportfunctioneventHandler(state:State,event:KeyEvent){
if(state.menu){
returneventHandlerMenu(state,event);
}else{
returneventHandlerPlaying(state,event);
}
}
Next,wecreateeventHandlerMenu.
functioneventHandlerMenu(state:State,event:KeyEvent){
if(eventinstanceofKeyEvent&&event.kind===KeyEventKind.Press){
constmenu=state.menu!;
letselected=menu.selected;
switch(event.keyCode){
caseKeys.Top:
selected--;
if(selected<0){
selected=menu.options.length-1;
}
return{
menu:update(menu,{
selected
}),
level:state.level
};
caseKeys.Bottom:
selected++;
if(selected>=menu.options.length){
selected=0;
}
return{
menu:update(menu,{
selected
}),
level:state.level
};
caseKeys.Space:
constoption=menu.options[menu.selected];
returnoption[1](state);
default:
returnstate;
}
}
returnstate;
}
Youcannavigatethroughthemenuusingthearrowkeysandthespacebar.However,inthebackground,thegameisstillrunning.Inthenextsection,wewillnotupdatethestateofthelevelwhenthemenuisactive.Also,wewillshowamenuwhentheuserhaswonorlost.
ModifyingthetimehandlerInlib/game/step.ts,wemustshowthemenuwhentheuserwonorlost.Wemustchangetheimport-statementtoimportmenuLostandmenuWonfrommodel:
import{State,Level,Object,Enemy,Wall,Movement,isOppositeMovement,
menuLost,menuWon,onGrid,Difficulty}from"./model";
InnewMenu,wecheckwhethersuchamenushouldbeshown.
functionnewMenu(player:Object,dots:Object[],enemies:Enemy[]){
for(constenemyofenemies){
if(distance(enemy.x,enemy.y,player.x,player.y)<=1){
returnmenuLost;
}
}
if(dots.length===0)returnmenuWon;
returnundefined;
}
InstepLevel,wemustcallthisfunction.
functionstepLevel(state:State):State{
constlevel=state.level;
constenemies=level.enemies.map(enemy=>stepEnemy(enemy,level.player,
level.walls,level.difficulty));
constplayer=stepPlayer(level.player,level.currentMovement,level.walls);
constdots=stepDots(level.dots,player);
constcurrentMovement=onGrid(player)||
isOppositeMovement(level.inputMovement,level.currentMovement)?
level.inputMovement:level.currentMovement;
constmenu=newMenu(player,dots,enemies);
constnewLevel=update(level,{enemies,dots,player,currentMovement});
returnupdate(state,{level:newLevel,menu});
}
Finally,wemustnotcallstepLevelinstepifthemenuisactive.
exportfunctionstep(state:State){
if(state.menu===undefined){
returnstepLevel(state);
}else{
returnstate;
}
}
Wecannowcompilethegameagainwithgulpandrunitbyopeningstatic/index.html.
SummaryInthischapter,wehaveexploredtheHTMLcanvas.Wehaveseenhowwecandesignaframeworktousefunctionalprogramming.Theframeworkprovidesabstractionarounddrawingonthecanvas,whichisnotpure.
WehavebuiltthegamePacMan.ThestructureofthisapplicationwassimilartoaFluxarchitecture,likewesawinthepreviouschapter.
Theenemiesinthisgamearenotverysmart.Theyeasilygetstuckbehindawall.Inthenextchapter,wewilltakealookatanothergame,butwewillonlyfocusontheartificialintelligence(AI).WewillcreateanapplicationthatcanplayTic-Tac-Toewithoutlosing.WewillseehowaMinimaxstrategyworksandhowwecanimplementitinTypeScript.
Chapter9.PlayingTic-Tac-ToeagainstanAIWebuiltthegamePacManinthepreviouschapter.Theenemieswerenotverysmart;youcaneasilyfoolthem.Inthischapter,wewillbuildagameinwhichthecomputerwillplaywell.ThegameiscalledTic-Tac-Toe.Thegameisplayedbytwoplayersonagrid,usuallythreebythree.Theplayerstrytoplacetheirsymbolsthreeinarow(horizontal,verticalordiagonal).Thefirstplayercanplacecrosses,thesecondplayerplacescircles.Iftheboardisfull,andnoonehasthreesymbolsinarow,itisadraw.
Thegameisusuallyplayedonathree-by-threegridandthetargetistohavethreesymbolsinarow.Tomaketheapplicationmoreinteresting,wewillmakethedimensionandtherowlengthvariable.
Wewillnotcreateagraphicalinterfaceforthisapplication,sincewehavealreadydonethatinChapter6,AdvancedProgramminginTypeScript.Wewillonlybuildthegamemechanicsandtheartificialintelligence(AI).AnAIisaplayercontrolledbythecomputer.Ifimplementedcorrectly,thecomputershouldneverloseonastandardthreebythreegrid.Whenthecomputerplaysagainstthecomputer,itwillresultinadraft.Wewillalsowritevariousunittestsfortheapplication.
Wewillbuildthegameasacommand-lineapplication.Thatmeansyoucanplaythegameinaterminal.Youcaninteractwiththegameonlywithtextinput:
It'splayerone'sturn!
Chooseoneoutoftheseoptions:
1X|X|
-+-+-
|O|
-+-+-
||
2X||X
-+-+-
|O|
-+-+-
||
3X||
-+-+-
X|O|
-+-+-
||
4X||
-+-+-
|O|X
-+-+-
||
5X||
-+-+-
|O|
-+-+-
X||
6X||
-+-+-
|O|
-+-+-
|X|
7X||
-+-+-
|O|
-+-+-
||X
Wewillbuildthisapplicationinthefollowingsteps:
CreatingtheprojectstructureAddingutilityfunctionsCreatingthemodelsImplementingtheAIusingMinimaxCreatingtheinterfaceTestingtheAISummary
CreatingtheprojectstructureWewilllocatethesourcefilesinlibandthetestsinlib/test.WeusegulptocompiletheprojectandAVAtoruntests.WecaninstallthedependenciesofourprojectwithNPM:
npminit-y
npminstallavagulpgulp-typescript--save-dev
Ingulpfile.js,weconfiguregulptocompileourTypeScriptfiles:
vargulp=require("gulp");
varts=require("gulp-typescript");
vartsProject=ts.createProject("./lib/tsconfig.json");
gulp.task("default",function(){
returntsProject.src()
.pipe(ts(tsProject))
.pipe(gulp.dest("dist"));
});
ConfigureTypeScriptWecandownloadtypedefinitionsforNodeJSwithNPM:
npminstall@types/node--save-dev
WemustexcludebrowserfilesinTypeScript.Inlib/tsconfig.json,weaddtheconfigurationforTypeScript:
{
"compilerOptions":{
"target":"es6",
"module":"commonjs"
}
}
Forapplicationsthatruninthebrowser,youwillprobablywanttotargetES5,sinceES6isnotsupportedinallbrowsers.However,thisapplicationwillonlybeexecutedinNodeJS,sowedonothavesuchlimitations.YouhavetouseNodeJS6orlaterforES6support.
AddingutilityfunctionsSincewewillworkalotwitharrays,wecanusesomeutilityfunctions.First,wecreateafunctionthatflattensatwodimensionalarrayintoaonedimensionalarray:
exportfunctionflatten<U>(array:U[][]){
return(<U[]>[]).concat(...array);
}
Next,wecreateafunctionthatreplacesasingleelementofanarraywithaspecifiedvalue.Wewillusefunctionalprogramminginthischapteragain,sowemustuseimmutabledatastructures.Wecanusemapforthis,sincethisfunctionprovidesboththeelementandtheindextothecallback.Withthisindex,wecandeterminewhetherthatelementshouldbereplaced:
exportfunctionarrayModify<U>(array:U[],index:number,newValue:U){
returnarray.map((oldValue,currentIndex)=>
currentIndex===index?newValue:oldValue);
}
Wealsocreateafunctionthatreturnsarandomintegerunderacertainupperbound:
exportfunctionrandomInt(max:number){
returnMath.floor(Math.random()*max);
}
Wewillusethesefunctionsinthenextsessions.
CreatingthemodelsInlib/model.ts,wewillcreatethemodelforourgame.Themodelshouldcontainthegamestate.
Westartwiththeplayer.Thegameisplayedbytwoplayers.Eachfieldofthegridcontainsthesymbolofaplayerornosymbol.Wewillmodelthegridasatwodimensionalarray,whereeachfieldcancontainaplayer:
exporttypeGrid=Player[][];
AplayeriseitherPlayer1,Player2,ornoplayer:
exportenumPlayer{
Player1=1,
Player2=-1,
None=0
}
Wehavegiventhesemembersvaluessowecaneasilygettheopponentofaplayer:
exportfunctiongetOpponent(player:Player):Player{
return-player;
}
Wecreateatypetorepresentanindexofthegrid.Sincethegridistwodimensional,suchanindexrequirestwovalues:
exporttypeIndex=[number,number];
Wecanusethistypetocreatetwofunctionsthatgetorupdateonefieldofthegrid.Weusefunctionalprogramminginthischapter,sowewillnotmodifythegrid.Instead,wereturnanewgridwithonefieldchanged:
exportfunctionget(grid:Grid,[rowIndex,columnIndex]:Index){
constrow=grid[rowIndex];
if(!row)returnundefined;
returnrow[columnIndex];
}
exportfunctionset(grid:Grid,[row,column]:Index,value:Player){
returnarrayModify(grid,row,
arrayModify(grid[row],column,value)
);
}
ShowingthegridToshowthegametotheuser,wemustconvertagridtoastring.First,wewillcreateafunctionthatconvertsaplayertoastring,thenafunctionthatusesthepreviousfunctiontoshowarow,finallyafunctionthatusesthesefunctionstoshowthecompletegrid.
Thestringrepresentationofagridshouldhavelinesbetweenthefields.Wecreatetheselineswithstandardcharacters(+,-,and|).Thisgivesthefollowingresult:
X|X|O
-+-+-
|O|
-+-+-
X||
Toconvertaplayertothestring,wemustgettheirsymbol.ForPlayer1,thatisacrossandforPlayer2,acircle.Ifafieldofthegridcontainsnosymbol,wereturnaspacetokeepthegridaligned:
functionshowPlayer(player:Player){
switch(player){
casePlayer.Player1:
return"X";
casePlayer.Player2:
return"O";
default:
return"";
}
}
Wecanusethisfunctiontothetokensofallfieldsinarow.Weaddaseparatorbetweenthesefields:
functionshowRow(row:Player[]){
returnrow.map(showPlayer).reduce((previous,current)=>previous+"|"+
current);
}
Sincewemustdothesamelateron,butwithadifferentseparator,wecreateasmallhelperfunctionthatdoesthisconcatenationbasedonaseparator:
constconcat=(separator:string)=>(left:string,right:string)=>
left+separator+right;
Thisfunctionrequirestheseparatorandreturnsafunctionthatcanbepassedtoreduce.WecannowusethisfunctioninshowRow:
functionshowRow(row:Player[]){
returnrow.map(showPlayer).reduce(concat("|"));
}
Wecanalsousethishelperfunctiontoshowtheentiregrid.Firstwemustcomposetheseparator,whichisalmostthesameasshowingasinglerow.Next,wecanshowthegridwiththisseparator:
exportfunctionshowGrid(grid:Grid){
constseparator="\n"+grid[0].map(()=>"-").reduce(concat("+"))+"\n";
returngrid.map(showRow).reduce(concat(separator));
}
CreatingoperationsonthegridWewillnowcreatesomefunctionsthatdooperationsonthegrid.Thesefunctionscheckwhethertheboardisfull,whethersomeonehaswon,andwhatoptionsaplayerhas.
Wecancheckwhethertheboardisfullbylookingatallfields.Ifnofieldexiststhathasnosymbolonit,theboardisfull,aseveryfieldhasasymbol:
exportfunctionisFull(grid:Grid){
for(constrowofgrid){
for(constfieldofrow){
if(field===Player.None)returnfalse;
}
}
returntrue;
}
Tocheckwhetherauserhaswon,wemustgetalistofallhorizontal,verticalanddiagonalrows.Foreachrow,wecancheckwhetherarowconsistsofacertainamountofthesamesymbolsonarow.Westorethegridasanarrayofthehorizontalrows,sowecaneasilygetthoserows.Wecanalsogettheverticalrowsrelativelyeasily:
functionallRows(grid:Grid){
return[
...grid,
...grid[0].map((field,index)=>getVertical(index)),
...
];
functiongetVertical(index:number){
returngrid.map(row=>row[index]);
}
}
Gettingadiagonalrowrequiressomemorework.Wecreateahelperfunctionthatwillwalkonthegridfromastartpoint,inacertaindirection.Wedistinguishtwodifferentkindsofdiagonals:adiagonalthatgoestothelower-rightandadiagonalthatgoestothelower-left.
Forastandardthreebythreegame,onlytwodiagonalsexist.However,alargergridmayhavemorediagonals.Ifthegridis5by5,andtheusersshouldgetthreeinarow,tendiagonalswithalengthofatleastthreeexist:
1. 0,0to4,42. 0,1to3,43. 0,2to2,44. 1,0to4,35. 2,0to4,26. 4,0to0,47. 3,0to0,38. 2,0to0,2
9. 4,1to1,410. 4,2to2,4
Thediagonalsthatgotowardthelower-right,startatthefirstcolumnoratthefirsthorizontalrow.Otherdiagonalsstartatthelastcolumnoratthefirsthorizontalrow.Inthisfunction,wewilljustreturnalldiagonals,eveniftheyonlyhaveoneelement,sincethatiseasytoimplement.
Weimplementthiswithafunctionthatwalksthegridtofindthediagonal.Thatfunctionrequiresastartpositionandastepfunction.Thestepfunctionincrementsthepositionforaspecificdirection:
functionallRows(grid:Grid){
return[
...grid,
...grid[0].map((field,index)=>getVertical(index)),
...grid.map((row,index)=>getDiagonal([index,0],stepDownRight)),
...grid[0].slice(1).map((field,index)=>getDiagonal([0,index+1],
stepDownRight)),
...grid.map((row,index)=>getDiagonal([index,grid[0].length-1],
stepDownLeft)),
...grid[0].slice(1).map((field,index)=>getDiagonal([0,index],
stepDownLeft))
];
functiongetVertical(index:number){
returngrid.map(row=>row[index]);
}
functiongetDiagonal(start:Index,step:(index:Index)=>
Index){
constrow:Player[]=[];
letindex:Index|undefined=start;
letvalue=get(grid,index);
while(value!==undefined){
row.push(value);
index=step(index);
value=get(grid,index);
}
returnrow;
}
functionstepDownRight([i,j]:Index):Index{
return[i+1,j+1];
}
functionstepDownLeft([i,j]:Index):Index{
return[i+1,j-1];
}
functionstepUpRight([i,j]:Index):Index{
return[i-1,j+1];
}
}
Tocheckwhetherarowhasacertainamountofthesameelementsonarow,wewillcreatea
functionwithsomenicelookingfunctionalprogramming.Thefunctionrequiresthearray,theplayer,andtheindexatwhichthecheckingstarts.Thatindexwillusuallybezero,butduringrecursionwecansetittoadifferentvalue.originalLengthcontainstheoriginallengththatasequenceshouldhave.Thelastparameter,length,willhavethesamevalueinmostcases,butinrecursionwewillchangethevalue.Westartwithsomebasecases.Everyrowcontainsasequenceofzerosymbols,sowecanalwaysreturntrueinsuchacase:
functionisWinningRow(row:Player[],player:Player,index:number,
originalLength:number,length:number):boolean{
if(length===0){
returntrue;
}
Iftherowdoesnotcontainenoughelementstoformasequence,therowwillnothavesuchasequenceandwecanreturnfalse:
if(index+length>row.length){
returnfalse;
}
Forothercases,weuserecursion.Ifthecurrentelementcontainsasymboloftheprovidedplayer,thisrowformsasequenceifthenextlength-1fieldscontainthesamesymbol:
if(row[index]===player){
returnisWinningRow(row,player,index+1,originalLength,length-1);
}
Otherwise,therowshouldcontainasequenceoftheoriginallengthinsomeotherposition:
returnisWinningRow(row,player,index+1,originalLength,originalLength);
}
Ifthegridislargeenough,arowcouldcontainalongenoughsequenceafterasequencethatwastooshort.Forinstance,XXOXXXcontainsasequenceoflengththree.ThisfunctionhandlestheserowscorrectlywiththeparametersoriginalLengthandlength.
Finally,wemustcreateafunctionthatreturnsallpossiblesetsthataplayercando.Toimplementthisfunction,wemustfirstfindallindices.Wefiltertheseindicestoindicesthatreferenceanemptyfield.Foreachoftheseindices,wechangethevalueofthegridintothespecifiedplayer.Thisresultsinalistofoptionsfortheplayer:
exportfunctiongetOptions(grid:Grid,player:Player){
constrowIndices=grid.map((row,index)=>index);
constcolumnIndices=grid[0].map((column,index)=>index);
constallFields=flatten(rowIndices.map(
row=>columnIndices.map(column=><Index>[row,column])
));
returnallFields
.filter(index=>get(grid,index)===Player.None)
.map(index=>set(grid,index,player));
}
TheAIwillusethistochoosethebestoptionandahumanplayerwillgetamenuwiththeseoptions.
CreatingthegridBeforethegamecanbestarted,wemustcreateanemptygrid.Wewillwriteafunctionthatcreatesanemptygridwiththespecifiedsize:
exportfunctioncreateGrid(width:number,height:number){
constgrid:Grid=[];
for(leti=0;i<height;i++){
grid[i]=[];
for(letj=0;j<width;j++){
grid[i][j]=Player.None;
}
}
returngrid;
}
Inthenextsection,wewilladdsometestsforthefunctionsthatwehavewritten.Thesefunctionsworkonthegrid,soitwillbeusefultohaveafunctionthatcanparseagridbasedonastring.
Wewillseparatetherowsofagridwithasemicolon.Eachrowcontainstokensforeachfield.Forinstance,"XXO;O;X"resultsinthisgrid:
X|X|O
-+-+-
|O|
-+-+-
X||
Wecanimplementthisbysplittingthestringintoanarrayoflines.Foreachline,wesplitthelineintoanarrayofcharacters.WemapthesecharacterstoaPlayervalue:
exportfunctionparseGrid(input:string){
constlines=input.split(";");
returnlines.map(parseLine);
functionparseLine(line:string){
returnline.split("").map(parsePlayer);
}
functionparsePlayer(character:string){
switch(character){
case"X":
returnPlayer.Player1;
case"O":
returnPlayer.Player2;
default:
returnPlayer.None;
}
}
}
Inthenextsectionwewillusethisfunctiontowritesometests.
AddingtestsJustlikeinChapter5,NativeQRScannerApp,wewilluseAVAtowritetestsforourapplication.Sincethefunctionsdonothavesideeffects,wecaneasilytestthem.
Inlib/test/winner.ts,wetestthefindWinnerfunction.First,wecheckwhetherthefunctionrecognizesthewinnerinsomesimplecases:
importtestfrom"ava";
import{Player,parseGrid,findWinner}from"../model";
test("playerwinner",t=>{
t.is(findWinner(parseGrid(";XXX;"),3),Player.Player1);
t.is(findWinner(parseGrid(";OOO;"),3),Player.Player2);
t.is(findWinner(parseGrid(";;"),3),Player.None);
});
Wecanalsotestallpossiblethree-in-a-rowpositionsinthethreebythreegrid.Withthistest,wecanfindoutwhetherhorizontal,vertical,anddiagonalrowsarecheckedcorrectly:
test("3x3winner",t=>{
constgrids=[
"XXX;;",
";XXX;",
";;XXX",
"X;X;X",
"X;X;X",
"X;X;X",
"X;X;X",
"X;X;X"
];
for(constgridofgrids){
t.is(findWinner(parseGrid(grid),3),Player.Player1);
}
});
Wemustalsotestthatthefunctiondoesnotclaimthatsomeonewontoooften.Inthenexttest,wevalidatethatthefunctiondoesnotreturnawinnerforgridsthatdonothaveawinner:
test("3x3nowinner",t=>{
constgrids=[
"XXO;OXX;XOO",
";;",
"XXO;;OOX",
"X;X;X"
];
for(constgridofgrids){
t.is(findWinner(parseGrid(grid),3),Player.None);
}
});
Sincethegamealsosupportsotherdimensions,weshouldcheckthesetoo.Wecheckthatalldiagonalsofafourbythreegridarecheckedcorrectly,wherethelengthofasequenceshould
betwo:
test("4x3winner",t=>{
constgrids=[
"X;X;",
"X;X;",
"X;X;",
";X;X",
"X;X;",
"X;X;",
"X;X;",
";X;X"
];
for(constgridofgrids){
t.is(findWinner(parseGrid(grid),2),Player.Player1);
}
});
Youcanofcourseaddmoretestgridsyourself.
Tip
Addtestsbeforeyoufixabug.Thesetestsshouldshowthewrongbehaviorrelatedtothebug.Whenyouhavefixedthebug,thesetestsshouldpass.Thispreventsthebugreturninginafutureversion.
RandomtestingInsteadofrunningthetestonsomepredefinedsetoftestcases,youcanalsowriteteststhatrunonrandomdata.Youcannotcomparetheoutputofafunctiondirectlywithanexpectedvalue,butyoucanchecksomepropertiesofit.Forinstance,getOptionsshouldreturnanemptylistifandonlyiftheboardisfull.WecanusethispropertytotestgetOptionsandisFull.
First,wecreateafunctionthatrandomlychoosesaplayer.Tohigherthechanceofafullgrid,weaddsomeextraweightontheplayerscomparedtoanemptyfield:
importtestfrom"ava";
import{createGrid,Player,isFull,getOptions}from"../model";
import{randomInt}from"../utils";
functionrandomPlayer(){
switch(randomInt(4)){
case0:
case1:
returnPlayer.Player1;
case2:
case3:
returnPlayer.Player2;
default:
returnPlayer.None;
}
}
Wecreate10000randomgridswiththisfunction.Thedimensionsandthefieldsarechosenrandomly:
test("get-options",t=>{
for(leti=0;i<10000;i++){
constgrid=createGrid(randomInt(10)+1,randomInt(10)+1)
.map(row=>row.map(randomPlayer));
Next,wecheckwhetherthepropertythatwedescribeholdsforthisgrid:
constoptions=getOptions(grid,Player.Player1)
t.is(isFull(grid),options.length===0);
Wealsocheckthatthefunctiondoesnotgivethesameoptiontwice:
for(leti=1;i<options.length;i++){
for(letj=0;j<i;j++){
t.notSame(options[i],options[j]);
}
}
}
});
Dependingonhowcriticalafunctionis,youcanaddmoretests.Inthiscase,youcouldcheck
thatonlyonefieldismodifiedinanoptionorthatonlyanemptyfieldcanbemodifiedinanoption:
Nowyoucanrunthetestsusinggulp&&avadist/test.Youcanaddthistoyourpackage.jsonfile.Inthescriptssection,youcanaddcommandsthatyouwanttorun.Withnpmrunxxx,youcanruntaskxxx.npmtestthatwasaddedasshorthandfornpmruntest,sincethetestcommandisoftenused:
{
"name":"chapter-7",
"version":"1.0.0",
"scripts":{
"test":"gulp&&avadist/test"
},
...
ImplementingtheAIusingMinimaxWecreateanAIbasedonMinimax.Thecomputercannotknowwhathisopponentwilldointhenextsteps,buthecancheckwhathecandointheworst-case.Theminimumoutcomeoftheseworstcasesismaximizedbythisalgorithm.ThisbehaviorhasgivenMinimaxitsname.
TolearnhowMinimaxworks,wewilltakealookatthevalueorscoreofagrid.Ifthegameisfinished,wecaneasilydefineitsvalue:ifyouwon,thevalueis1;ifyoulost,-1andifitisadraw,0.Thus,forplayer1thenextgridhasvalue1andforplayer2thevalueis-1:
X|X|X
-+-+-
O|O|
-+-+-
X|O|
Wewillalsodefinethevalueofagridforagamethathasnotbeenfinished.Wetakealookatthefollowinggrid:
X||X
-+-+-
O|O|
-+-+-
O|X|
Itisplayer1'sturn.Hecanplacehisstoneonthetoprow,andhewouldwin,resultinginavalueof1.Hecanalsochoosetolayhisstoneonthesecondrow.Thenthegamewillresultinadraft,ifplayer2isnotdumb,withscore0.Ifhechoosestoplacethestoneonthelastrow,player2canwinresultingin-1.Weassumethatplayer1issmartandthathewillgoforthefirstoption.Thus,wecouldsaythatthevalueofthisunfinishedgameis1.
Wewillnowformalizethis.Inthepreviousparagraph,wehavesummedupalloptionsfortheplayer.Foreachoption,wehavecalculatedtheminimumvaluethattheplayercouldgetifhewouldchoosethatoption.Fromtheseoptions,wehavechosenthemaximumvalue.
Minimaxchoosestheoptionwiththehighestvalueofalloptions.
ImplementingMinimaxinTypeScriptAsyoucansee,thedefinitionofMinimaxlookslikeyoucanimplementitwithrecursion.Wecreateafunctionthatreturnsboththebestoptionandthevalueofthegame.Afunctioncanonlyreturnasinglevalue,butmultiplevaluescanbecombinedintoasinglevalueinatuple,whichisanarraywiththesevalues.
First,wehandlethebasecases.Ifthegameisfinished,theplayerhasnooptionsandthevaluecanbecalculateddirectly:
import{Player,Grid,findWinner,isFull,getOpponent,getOptions}from
"./model";
exportfunctionminimax(grid:Grid,rowLength:number,player:Player):[Grid,
number]{
constwinner=findWinner(grid,rowLength);
if(winner===player){
return[undefined,1];
}elseif(winner!==Player.None){
return[undefined,-1];
}elseif(isFull(grid)){
return[undefined,0];
Otherwise,welistalloptions.Foralloptions,wecalculatethevalue.Thevalueofanoptionisthesameastheoppositeofthevalueoftheoptionfortheopponent.Finally,wechoosetheoptionwiththebestvalue:
}else{
letoptions=getOptions(grid,player);
constopponent=getOpponent(player);
returnoptions.map<[Grid,number]>(
option=>[option,-(minimax(option,rowLength,opponent)[1])]
).reduce(
(previous,current)=>previous[1]<current[1]?current:previous
)!;
}
}
Whenyouusetupletypes,youshouldexplicitlyaddatypedefinitionforit.Sincetuplesarearraystoo,anarraytypeisautomaticallyinferred.Whenyouaddthetupleasreturntype,expressionsafterthereturnkeywordwillbeinferredasthesetuples.Foroptions.map,youcanmentiontheuniontypeasatypeargumentorbyspecifyingitinthecallbackfunction(options.map((option):[Grid,number]=>...);).
YoucaneasilyseethatsuchanAIcanalsobeusedforotherkindsofgames.Actually,theminimaxfunctionhasnodirectreferencetoTic-Tac-Toe,onlyfindWinner,isFullandgetOptionsarerelatedtoTic-Tac-Toe.
OptimizingthealgorithmTheMinimaxalgorithmcanbeslow.Choosingthefirstset,especially,takesalongtimesincethealgorithmtriesallwaysofplayingthegame.Wewillusetwotechniquestospeedupthealgorithm.
First,wecanusethesymmetryofthegame.Whentheboardisemptyitdoesnotmatterwhetheryouplaceastoneintheupper-leftcornerorthelower-rightcorner.Rotatingthegridaroundthecenter180degreesgivesanequivalentboard.Thus,weonlyneedtotakealookathalftheoptionswhentheboardisempty.
Secondly,wecanstopsearchingforoptionsifwefoundanoptionwithvalue1.Suchanoptionisalreadythebestthingtodo.
Implementingthesetechniquesgivesthefollowingfunction:
import{Player,Grid,findWinner,isFull,getOpponent,getOptions}from
"./model";
exportfunctionminimax(grid:Grid,rowLength:number,player:Player):[Grid,
number]{
constwinner=findWinner(grid,rowLength);
if(winner===player){
return[undefined,1];
}elseif(winner!==Player.None){
return[undefined,-1];
}elseif(isFull(grid)){
return[undefined,0];
}else{
letoptions=getOptions(grid,player);
constgridSize=grid.length*grid[0].length;
if(options.length===gridSize){
options=options.slice(0,Math.ceil(gridSize/2));
}
constopponent=getOpponent(player);
letbest:[Grid,number]|undefined=undefined;
for(constoptionofoptions){
constcurrent:[Grid,number]=[option,-(minimax(option,rowLength,
opponent)[1])];
if(current[1]===1){
returncurrent;
}elseif(best===undefined||current[1]>best[1]){
best=current;
}
}
returnbest!;
}
}
ThiswillspeeduptheAI.InthenextsectionswewillimplementtheinterfaceforthegameandwewillwritesometestsfortheAI.
CreatingtheinterfaceNodeJScanbeusedtocreateservers,aswedidinchapters2and3.Youcanalsocreatetoolswithacommandlineinterface(CLI).Forinstance,gulp,NPMandtypingsarecommandlineinterfacesbuiltwithNodeJS.WewilluseNodeJStocreatetheinterfaceforourgame.
HandlinginteractionTheinteractionfromtheusercanonlyhappenbytextinputintheterminal.Whenthegamestarts,itwillasktheusersomequestionsabouttheconfiguration:width,height,rowlengthforasequence,andtheplayer(s)thatareplayedbythecomputer.Thehighlightedlinesaretheinputoftheuser:
Tic-Tac-Toe
Width
3
Height
3
Rowlength
2
Whocontrolsplayer1?
1You
2Computer
1
Whocontrolsplayer2?
1You
2Computer
1
Duringthegame,thegameaskstheuserwhichofthepossibleoptionshewantstodo.Allpossiblestepsareshownonthescreen,withanindex.Theusercantypetheindexoftheoptionhewants:
X||
-+-+-
O|O|
-+-+-
|X|
It'splayerone'sturn!
Chooseoneoutoftheseoptions:
1X|X|
-+-+-
O|O|
-+-+-
|X|
2X||X
-+-+-
O|O|
-+-+-
|X|
3X||
-+-+-
O|O|X
-+-+-
|X|
4X||
-+-+-
O|O|
-+-+-
X|X|
5X||
-+-+-
O|O|
-+-+-
|X|X
ANodeJSapplicationhasthreestandardstreamstointeractwiththeuser.Standardinput,stdin,isusedtoreceiveinputfromtheuser.Standardoutput,stdout,isusedtoshowtexttotheuser.Standarderror,stderr,isusedtoshowerrormessagestotheuser.Youcanaccessthesestreamswithprocess.stdin,process.stdoutandprocess.stderr.
Youhaveprobablyalreadyusedconsole.logtowritetexttotheconsole.Thisfunctionwritesthetexttostdout.Wewilluseconsole.logtowritetexttostdoutandwewillnotusestderr.
Wewillcreateahelperfunctionthatreadsalinefromstdin.Thisisanasynchronoustask,thefunctionstartslisteningandresolveswhentheuserhitsenter.Inlib/cli.ts,westartbyimportingthetypesandfunctionthatwehavewritten.
import{Grid,Player,getOptions,getOpponent,showGrid,findWinner,isFull,
createGrid}from"./model";
import{minimax}from"./ai";
Wecanlistentoinputfromstdinusingthedataevent.Theprocesssendseitherthestringorabuffer,anefficientwaytostorebinarydatainmemory.Withonce,thecallbackwillonlybefiredonce.Ifyouwanttolistentoalloccurrencesoftheevent,youcanuseon:
functionreadLine(){
returnnewPromise<string>(resolve=>{
process.stdin.once("data",(data:string|Buffer)=>
resolve(data.toString()));
});
}
WecaneasilyusereadLineinasyncfunctions.Forinstance,wecannowcreateafunctionthatreads,parsesandvalidatesaline.Wecanusethistoreadtheinputoftheuser,parseittoanumber,andfinallycheckthatthenumberiswithinacertainrange.Thisfunctionwillreturnthevalueifitpassesthevalidator.Otherwiseitshowsamessageandretries.
asyncfunctionreadAndValidate<U>(message:string,parse:(data:string)=>U,
validate:(value:U)=>boolean):Promise<U>{
constdata=awaitreadLine();
constvalue=parse(data);
if(validate(value)){
returnvalue;
}else{
console.log(message);
returnreadAndValidate(message,parse,validate);
}
}
Wecanusethisfunctiontoshowaquestionwheretheuserhasvariousoptions.Theusershouldtypetheindexofhisanswer.Thisfunctionvalidatesthattheindexiswithinbounds.Wewillshowindicesstartingat1totheuser,sowemustcarefullyhandlethese.
asyncfunctionchoose(question:string,options:string[]){
console.log(question);
for(leti=0;i<options.length;i++){
console.log((i+1)+"\t"+options[i].replace(/\n/g,"\n\t"));
console.log();
}
returnawaitreadAndValidate(
`Enteranumberbetween1and${options.length}`,
parseInt,
index=>index>=1&&index<=options.length
)-1;
}
CreatingplayersAplayercouldeitherbeahumanorthecomputer.Wecreateatypethatcancontainbothkindsofplayers.
typePlayerController=(grid:Grid)=>Grid|Promise<Grid>;
Nextwecreateafunctionthatcreatessuchaplayer.Forauser,wemustfirstknowwhetherheisthefirstorthesecondplayer.Thenwereturnanasyncfunctionthataskstheplayerwhichstephewantstodo.
constgetUserPlayer=(player:Player)=>async(grid:Grid)=>{
constoptions=getOptions(grid,player);
constindex=awaitchoose("Chooseoneoutoftheseoptions:",
options.map(showGrid));
returnoptions[index];
};
FortheAIplayer,wemustknowtheplayerindexandthelengthofasequence.WeusethesevariablesandthegridofthegametoruntheMinimaxalgorithm.
constgetAIPlayer=(player:Player,rowLength:number)=>(grid:Grid)=>
minimax(grid,rowLength,player)[0]!;
Nowwecancreateafunctionthataskstheplayerwhetheraplayershouldbeplayedbytheuserorthecomputer.
asyncfunctiongetPlayer(index:number,player:Player,rowLength:number):
Promise<PlayerController>{
switch(awaitchoose(`Whocontrolsplayer${index}?`,["You","Computer"]))
{
case0:
returngetUserPlayer(player);
default:
returngetAIPlayer(player,rowLength);
}
}
Wecombinethesefunctionsinafunctionthathandlesthewholegame.First,wemustasktheusertoprovidethewidth,heightandlengthofasequence.
exportasyncfunctiongame(){
console.log("Tic-Tac-Toe");
console.log();
console.log("Width");
constwidth=awaitreadAndValidate("Enteraninteger",parseInt,isFinite);
console.log("Height");
constheight=awaitreadAndValidate("Enteraninteger",parseInt,isFinite);
console.log("Rowlength");
constrowLength=awaitreadAndValidate("Enteraninteger",parseInt,
isFinite);
Weasktheuserwhichplayersshouldbecontrolledbythecomputer.
constplayer1=awaitgetPlayer(1,Player.Player1,rowLength);
constplayer2=awaitgetPlayer(2,Player.Player2,rowLength);
Theusercannowplaythegame.Wedonotusealoop,butweuserecursiontogivetheplayertheirturns.
returnplay(createGrid(width,height),Player.Player1);
asyncfunctionplay(grid:Grid,player:Player):Promise<[Grid,Player]>{
Ineverystep,weshowthegrid.Ifthegameisfinished,weshowwhichplayerhaswon.
console.log();
console.log(showGrid(grid));
console.log();
constwinner=findWinner(grid,rowLength);
if(winner===Player.Player1){
console.log("Player1haswon!");
return<[Grid,Player]>[grid,winner];
}elseif(winner===Player.Player2){
console.log("Player2haswon!");
return<[Grid,Player]>[grid,winner];
}elseif(isFull(grid)){
console.log("It'sadraw!");
return<[Grid,Player]>[grid,Player.None];
}
Ifthegameisnotfinished,weaskthecurrentplayerorthecomputerwhichsethewantstodo.
console.log(`It'splayer${player===Player.Player1?"one's":"two's"}
turn!`);
constcurrent=player===Player.Player1?player1:player2;
returnplay(awaitcurrent(grid),getOpponent(player));
}
}
Inlib/index.ts,wecanstartthegame.Whenthegameisfinished,wemustmanuallyexittheprocess.
import{game}from"./cli";
game().then(()=>process.exit());
Wecancompileandrunthisinaterminal:
gulp&&node--harmony_destructuringdist
Atthetimeofwriting,NodeJSrequiresthe--harmony_destructuringflagtoallowdestructuring,like[x,y]=z.InfutureversionsofNodeJS,thisflagwillberemovedand
youcanrunitwithoutit.
TestingtheAIWewilladdsometeststocheckthattheAIworksproperly.Forastandardthreebythreegame,theAIshouldneverlose.ThatmeanswhenanAIplaysagainstanAI,itshouldresultinadraw.Wecanaddatestforthis.Inlib/test/ai.ts,weimportAVAandourowndefinitions.
importtestfrom"ava";
import{createGrid,Grid,findWinner,isFull,getOptions,Player}from
"../model";
import{minimax}from"../ai";
import{randomInt}from"../utils";
Wecreateafunctionthatsimulatesthewholegameplay.
typePlayerController=(grid:Grid)=>Grid;
functionrun(grid:Grid,a:PlayerController,b:PlayerController):Player{
constwinner=findWinner(grid,3);
if(winner!==Player.None)returnwinner;
if(isFull(grid))returnPlayer.None;
returnrun(a(grid),b,a);
}
WewriteafunctionthatexecutesastepfortheAI.
constaiPlayer=(player:Player)=>(grid:Grid)=>
minimax(grid,3,player)[0]!;
NowwecreatethetestthatvalidatesthatagamewheretheAIplaysagainsttheAIresultsinadraw.
test("AIvsAI",t=>{
constresult=run(createGrid(3,3),aiPlayer(Player.Player1),
aiPlayer(Player.Player2))
t.is(result,Player.None);
});
TestingwitharandomplayerWecanalsotestwhathappenswhentheAIplaysagainstarandomplayerorwhenaplayerplaysagainsttheAI.TheAIshouldwinoritshouldresultinadraw.Werunthesemultipletimes;whatyoushouldalwaysdowhenyouuserandomizationinyourtest.
Wecreateafunctionthatcreatestherandomplayer.
constrandomPlayer=(player:Player)=>(grid:Grid)=>{
constoptions=getOptions(grid,player);
returnoptions[randomInt(options.length)];
};
Wewritethetwoteststhatbothrun20gameswitharandomplayerandanAI.
test("randomvsAI",t=>{
for(leti=0;i<20;i++){
constresult=run(createGrid(3,3),randomPlayer(Player.Player1),
aiPlayer(Player.Player2))
t.not(result,Player.Player1);
}
});
test("AIvsrandom",t=>{
for(leti=0;i<20;i++){
constresult=run(createGrid(3,3),aiPlayer(Player.Player1),
randomPlayer(Player.Player2))
t.not(result,Player.Player2);
}
});
Wehavewrittendifferentkindsoftests:
TeststhatchecktheexactresultsofsinglefunctionTeststhatcheckacertainpropertyofresultsofafunctionTeststhatcheckabigcomponent
Alwaysstartwritingtestsforsmallcomponents.IftheAItestsshouldfail,thatcouldbecausedbyamistakeinhasWinner,isFullorgetOptions,soitishardtofindthelocationoftheerror.Onlytestingsmallcomponentsisnotenough;biggertests,suchastheAItests,areclosertowhattheuserwilldo.Biggertestsarehardertocreate,especiallywhenyouwanttotesttheuserinterface.Youmustalsonotforgetthattestscannotguaranteethatyourcoderunscorrectly,itjustguaranteesthatyourtestcasesworkcorrectly.
SummaryInthischapter,wehavewrittenanAIforTic-Tac-Toe.Withthecommandlineinterface,youcanplaythisgameagainsttheAIoranotherhuman.YoucanalsoseehowtheAIplaysagainsttheAI.Wehavewrittenvarioustestsfortheapplication.
YouhavelearnedhowMinimaxworksforturn-basedgames.Youcanapplythistootherturn-basedgamesaswell.Ifyouwanttoknowmoreonstrategiesforsuchgames,youcantakealookatgametheory,themathematicalstudyofthesegames.
ReadingthismeansthatyouhavefinishedtheFunctionalProgrammingpartofthisbook.Onechapterremains,whichwillexplainhowyoucanmigratealegacyJavaScriptcodebasetoTypeScript.
Chapter10.MigrateJavaScripttoTypeScriptInthepreviouschapters,wehavebuiltnewapplicationsinTypeScript.However,youmightalsohaveoldcodebasesinJavaScriptwhichyouwanttomigratetoTypeScript.WewillseehowtheseprojectscanbeconvertedtoTypeScript.
Youcouldrewritethewholeprojectfromscratch,butthatwouldrequirealotofworkforbigprojects.SinceTypeScriptisbasedonJavaScript,thistransitioncanbedonemoreefficiently.
Sincethemigrationprocessisdependentontheproject,thischaptercanonlygiveguidance.Forvariouscommontopics,thischaptercontainsarecipetomigratecode.Migratingaprojectrequiresknowledgeoftheframeworksandthestructureoftheproject.
Thefollowingstepsarerelatedtomigratingacodebase:
GraduallymigratingtoTypeScriptAddingTypeScriptMigratingeachfileRefactoringtheproject
GraduallymigratingtoTypeScriptAsofTypeScript1.8,itispossibletocombineJavaScriptandTypeScriptinthesameproject.Thus,youcanmigrateaprojectfilebyfile.
Duringthemigrationofthefiles,theprojectshouldbeworkingateverystep.Thatway,youcancheckthattheworkisgoingwell,andyoucanstillworkontheproject.Ifanurgentbugisreported,youdonothavetofixitintheoldandmigratedversion;youonlyhavetofixitinthecurrentversion.
Youcanconverttheprojectinthefollowingsteps:
AddtheTypeScriptcompilertotheprojectMigrateeachfileRefactorandenablestricterchecksofTypeScript
Inthenextsections,wewillseehowthesestepscanbedone.
AddingTypeScriptBeforeyoucanconvertJavaScriptfilestoTypeScript,youmustaddtheTypeScriptcompilertoaproject.Iftheprojectalreadyusesabuildstep,theTypeScriptcompilermustbeintegratedintothebuildstep.Otherwise,anewbuildstepmustbecreated.Inthefollowingsections,wewillsetupTypeScriptandthebuildsystem.
ConfiguringTypeScriptInallcases,youshouldstartwithconfiguringTypeScript.Thisconfigurationwillbeusedbycodeeditorsandthebuildtool.ThemostimportantsettingisallowJs.ThissettingallowsJavaScriptfilesintheTypeScriptproject.Otherimportantoptionsaretargetandmodule.Fortarget,youcanchoosebetweenes3,es5,andes2015.ThelatestversionofJavaScript,es2015,isnotsupportedinallbrowsersatthetimeofwriting.Youcantargetes2015whenyouwriteanapplicationforNodeJS.Youcantargetes5forbrowsers.Forveryoldenvironments,youmusttargetes3.
Iftheprojectusesexternalmodules,youshouldalsosetthemodulesetting.IfyourprojectusesCommonJS,mostlyusedincombinationwithNodeJS,browserifyorwebpack,youshoulduse"module":"commonjs".AnimportinCommonJScanberecognizedbyacalltorequireandanexportbyanassignmenttoexports.[...]ormodule.exports,andfilesarenotwrappedinadefinefunction:
varpath=require("path");
exports.lorem=...;
module.exports=...;
AnothermoduleformatisAMD,AsynchronousModuleDefinition.Thisformatisusedalotforwebapplications.YoucanconfigureTypeScriptforAMDwith"module":"amd".ThemostpopularimplementationofAMDisrequire.js.
AnAMDfilestartswithadefinecall.
define(["require","exports","path"],function(require,exports,path){
exports.lorem=...;
});
Recentprojectsmightusees2015modules,withabuildstep.Youcanrecognizesuchfilesbytheimportandexportkeywords.
import*aspathfrom"path";
exportvarlorem=...;
Ifyouusees2015modules,youcanset"module":"es2015".However,sincethesemodulesareoftenusedwithacertainbuildsteptocompilethesemodulestoCommonJS,AMDorSystemJS,youcanalsodothatdirectlywithTypeScript.TheTypeScriptcompilerwillalsodothistransformationfortheJavaScriptfilesoftheproject,soyoucanremovetheotherbuildstepthatdoesthis(forinstance,Babel).Ifyouwanttodothis,youmustuse"commonjs","amd",or"systemjs".
Ifyourprojectdidnotuseabuildstep,youmightwanttochangethedirectorystructureofyourproject.Youmustnotstorethesourcefiles(TypeScript/JavaScript)inthesamedirectoryasthecompiledfiles(JavaScript).Inthepreviouschapters,weusedlibforthe
sourcesanddistforthecompiledfiles.Wecanconfigurethatbysetting"outDir":"dest".Ifyouuseabuildtoolsuchasgulpwheretemporaryfilescanstayinmemoryandarenotwrittentothedisk,youdonotneedtosetthisoptionsincethecompiledfilesarenotdirectlywrittentothedisk.
Thisshouldresultinatsconfig.jsonfilesimilartothisone:
{
"compilerOptions":{
"target":"es5",
"module":"commonjs",
"outDir":"dist"
}
}
Youshouldplacethisfileinthedirectorythatcontainsthesourcefiles.Ifyourprojectdidnotuseabuildtool,youcannowcompiletheprojectwithtsc-p./lib(where./libreferencesthedirectorythatcontainsthesourcefilesandtsconfig.jsonfile),providedthatyouhaveTypeScriptinstalled(npminstalltypescript-g).Ifyourprojectalreadyusedabuildsystem,youhavetointegratetheTypeScriptcompilerintoit,whichwewilldointhenextsection.
ConfiguringthebuildtoolConfiguringthebuilddependsonthebuildtoolyouuse.Forafewcommonlyusedtools,youcanfindthestepshere.MostbuildtoolsrequireyoutoinstallaTypeScriptplugin.Forbrowserify,youmustinstalltsifyusingNPM;forGrunt,grunt-ts;forgulp,gulp-typescript;andforwebpack,ts-loader.IfyourprojectusesJSPM,youdonothavetoinstallaplugin.
Youcanfindvariousconfigurationswiththesetoolsat:http://www.typescriptlang.org/docs/handbook/integrating-with-build-tools.html.Ifyouuseadifferentbuildtool,youshouldlookinthedocumentationofthetoolandsearchforaTypeScriptplugin.
SinceTypeScriptaccepts(andneeds)theJavaScriptfilesinyourproject,youmustprovideallsourcefilestotheTypeScriptcompiler.Forgulp,thatwouldmeanthatyouaddTypeScriptbeforeothercompilationsteps.Imagineataskinyourgulpfilelookslikethis:
gulp.src("lib/**/*.js")
.pipe(plugin())
.pipe(gulp.dist("dest"));
YoucanaddTypeScripttothisgulpfile:
varts=require("gulp-typescript");
vartsProject=ts.createProject("lib/tsconfig.json");
...
gulp.src("lib/**/*.js")
.pipe(ts(tsProject))
.pipe(plugin())
.pipe(gulp.dist("dest"));
Forabuildtoolthatcannotstoretemporaryfilesinmemory,suchasGrunt,youshouldcompileTypeScripttoatemporarydirectory.Otherstepsfromthebuildshouldreadthesourcesfromthisdirectory.
Formoreinformationonhowtoconfigureaspecificbuildtool,youcanlookatthedocumentationofthetoolandtheplugin.
AcquiringtypedefinitionsFordependenciesthatyouuse,suchasjQuery,youmustacquiretypedefinitions.Youcaninstallthemusingnpm.Youcanfindthesetypedefinitionsonhttps://aka.ms/types.
TestingtheprojectBeforegoingtothenextstep,makesurethatthebuildisworking.TypeScriptshouldonlyshowsyntaxerrorsinJavaScriptfiles.Also,theapplicationshouldbeworkingatruntime.
Tip
IfyouarenowusingTypeScripttotranspileESmodulestoCommonJS,youmightrunintoproblems.BabelandTypeScripthandledefaultimportsdifferently.Babellooksforthedefaultproperty,andifthatdoesnotexist,itbehavesthesameasafullmoduleimport.TypeScriptwillonlylookforthedefaultproperty.Ifyougetruntimeerrorsafterthemigration,youmightneedtoreplaceadefaultimport(importnamefrom"package")withafullmoduleimport(import*asnamefrom"package").
MigratingeachfileWhenthebuildsystemissetup,youcanstartwithmigratingfiles.Itiseasiesttostartwithfilesthatdonotdependonotherfiles,asthesedonotdependontypesofotherfiles.Tomigrateafile,youmustrenamethefileextensionto.ts,convertthemoduleformattoESmodules,correcttypesthatareinferredincorrectly,andaddtypestountypedentities.Inthenextsections,wewilltakealookatthesetasks.
ConvertingtoESmodulesInTypeScriptfilesyoucannotuseCommonJSorAMDdirectly.InsteadyoumustuseESmodules,likewedidinthepreviouschapters.Foranimport,youmustchoosefromthese:
import*asnamefrom"package",importsthewholepackage,similartovarname=require("package")inCommonJS.importnamefrom"package",importsthedefaultexport,similartovarname=require("package").default.import{name}from"package",importsanamedexport,similartovarname=require("package").name.
ESmodulesoffervariousconstructstoexportvaluesfrommodules:
exportfunctionname(){},exportclassName{},exportvarname,exportsanamedvariable.Compilesto:
functionname(){}
exports.name=name;
exportdefaultfunctionname(){},exportdefaultclassName{},exportdefaultvarname,exportsavariableasthedefaultexport.Compilesto:
functionname(){}
exports.default=name;
exportdefaultx,wherexisanexpression,exportsanexpressionasthedefaultexport.export{x,y},exportsvariablesxandyasnamedexports.Thiscompilesto:
exports.x=x;
exports.y=y;
WithCommonJSandAMDyoucanalsoexportanexpressionasthefullmodule,withmodule.exports=xinCommonJSorreturnxinAMD.ThisisnotpossiblewithESmodules.Ifthispatternisusedinafilethatyoumustmigrate,youcaneitherswitchtoanESexportorpatchallfilesthatimportthisfile,orusealegacyexportstatementinTypeScript.Withexport=x,youcanexportanexpressionasthefullmodule.However,sincethisisnotstandard,itisnotadvisedtodothisbutitcanhelpduringthemigration.Thiscompilestomodule.exports=xorreturnx.
Thefileshouldgivenosyntaxerrorswhenyoucompileit.Itmightshowtypeerrors,whichthenextsessionwilldiscuss.
CorrectingtypesSincethefilewasaJavaScriptfile,itdoesnothaveanytypeannotations.Atsomelocations,TypeScriptwillinfertypesforvariables.Whenyoudeclareavariableanddirectlyassigntoit,TypeScriptwillinferthetypebasedonthetypeoftheassignment.Thus,whenyouwriteletx="",TypeScriptwilltypexasastring.However,insomecasestheinferredtypeisnotcorrect.Youcanseethatinthenextexamples.
letx={};
x.a=true;
Thetypeofxisinferredas{},anemptyobject.Thus,theassignmenttox.aisnotallowed,sincethepropertyadoesnotexist.Youcanfixthisbyaddingatypetothedefinition:letx:{a?:boolean}={}.
classBase{
a:boolean;
}
classDerivedextendsBase{
b:string;
}
letx=newDerived();
x=newBase();
Inthiscase,thetypeofxisDerived.Theassignmentonthelastlineisnotallowed,sinceBaseisnotassignabletoDerived.Youcanagainfixthisbyaddingatype:letx:Base=newDerived().
Ifthetypeofavariableisunknownorverycomplex,youcanuseanyasthetypeforthevariable,whichdisablestypecheckingforthatvariable.
Otherpossiblesourcesoferrorsareclasses.Whenyouusetheclasskeywordtocreateclasses,youcangeterrorsthatapropertydoesnotexistintheclass.
classA{
constructor(){
this.x=true;
}
}
Inthisexample,youwouldgetanerrorthatthepropertyxdoesnotexistinA.InTypeScript,youmustdeclareallpropertiesofaclass.
classA{
x:boolean;
constructor(){
this.x=true;
}
}
MosterrorsofTypeScriptshouldnowbefixed.Somecaseshowevercanstillgeneratetypeerrors,forexamplewhenavariablehasdifferenttypes,whichisdiscussedinthenextsession.
AddingtypeguardsandcastsAcommonpatterninJavaScriptisthatafunctionacceptseitherasinglevalueofacertaintype,oranarrayofmultipletypes.Youcanexpresssuchatypewithauniontype:
functionfoo(input:string|string[]){
...
}
Inthebodyofsuchafunction,youwouldcheckiftheargumentisanarrayorasinglestring.Inmostcases,TypeScriptcancorrectlyfollowthispattern.Forinstance,TypeScriptcanchangethetypeofinputinthenextexample.
functionfoo(input:string|string[]){
if(typeofinput==="string"){
}else{
}
}
Thetypeofinputisstringintheblockaftertheifandstring[]intheelse-block.Thechangingofatypeiscallednarrowingandthechecksforatypearecalledtypeguards.TypeScripthasbuilt-insupportfortypeofandinstanceoftypeguards,butyoucanalsodefineyourown.Auserdefinedtypeguardfunctionisafunctionthattakesthevalueasoneofitsargumentsandreturnstruewhenthevalueisofacertaintype.Atypeguardcanbewrittenlikethis:
functionisBar(value:Foo):valueisBar{
...
}
Asyoucansee,thereturntypeofisBarisvalueisBar,aspecialbooleantype.Ifyouhaveafunctionthatchecksthatavalueisofacertaintype,youshouldaddareturntypetomakeitatypeguard.
IftheTypeScriptcompilerstillcannotcorrectlynarrowthetypeofavariableonacertainlocation,youmustaddacast.Acastisacompilerinstructioninwhichtheprogrammerguaranteesthatavalueisofacertaintype.Atypeguardcanbewrittenintwodifferentways.
<Bar>value
valueasBar
Thefirstsyntaxismostused,butnotsupportedinJSXorTSXfiles.InaTSXfile,youmustusethesecondsyntax.
Whenyouhavefixedalltheseerrors,theprojectshouldcompilewithouterrorsagain.
UsingmodernsyntaxTheclasskeywordwasintroducedinES2015,arecentversionofJavaScript.Olderprojectscreatedclasseswithafunctionandprototypesshouldmigratetothenewclasssyntax.InTypeScript,theseclassescanbetypedbetter.Followingaretwocodefragments,whichshowthesameclasswrittenwiththeprototypeandwiththeclasssyntax.
varA=(function(){
functionA(){
this.x=true;
}
A.prototype.a=function(){
};
returnA;
}());
classA{
x:boolean;
constructor(){
this.x=true;
}
a(){
}
}
Youcanalsousethenew,blockscopedvariabledeclarations.Insteadofvarxyoushouldwriteconstxforavariablethatisnotreassignedandletxforavariablethatwillbereassigned.
Finally,youcanalsousearrowfunctions(orlambdaexpressions).Usingnormalfunctiondefinitions,thevalueofthisislostincallbacks.Thus,youhadtostorethatinavariable(selfor_thiswascommonlyused).Youcanreplacethatwithanarrowfunction.
var_this=this;
functionmyCallback(){
_this...
}
Thiscodefragmentcanberewrittento:
constmyCallback=()=>{
this...
};
AddingtypesThefilecompilesnow,butlotsofvariablesandparametersmightbetypedasany.Forcomplextypes,youcanfirstcreateaninterface,forobjecttypes,oratypealias,forfunctiontypesoruniontypes.
TypeScriptdoesnotinfertypesinthefollowingcases:
Variabledeclarationwithoutaninitializer(likevarx;)ParametersofafunctiondefinitionwithoutadefaultvalueReturntypeofafunctionthatusesrecursion
InaneditorlikeVSCode,youcancheckthetypeofavariable,parameterorfunctionbyhoveringoverit.Ontheselocationsyoushouldaddatypeannotationyourself.
RefactoringtheprojectWhenyouhaveportedtheprojecttoTypeScript,youcanrefactortheprogrammoreeasily.YoushouldremovepatternsthatdonotfitwellwithTypeScript.Forinstance,magicstringvaluesshouldbereplacedbyenums.Whenyouhaveaprojectthatusesaframework,youcanalsodosomeframeworkrelatedrefactoring.ForaReactproject,youmightwanttoupgradefromtheoldclasscreationwithReact.createClasstothenewclasssyntax.
Apropereditorcanhelpduringrefactoring.VSCodecanrenameanidentifierinthewholeprojectorfindallreferencesofanidentifier.Itcanalsoformatyourcodeifit'smessyorjumptothedefinitionofanidentifier.Youcanaccesstheseoptionswitharight-clickontheidentifierinthecode.
Whichstepsyoumustdoforrefactoringdependsonyourproject.Youshouldlookforpartsofthecodethatarenottypedorincorrectlytyped,becauseofabadstructureorsomedynamicbehavior.
EnablestrictchecksYoucanenablestricterchecksinTypeScript.Thesecheckscanimprovethequalityofyourprogram.Hereareafewoptionsthatcanbeuseful.
noImplicitAny:Checksthatnovariablesaretypedasanyunlessyouexplicitlyannotatedthemwithany.noImplicitReturns:Checksthatallexecutionpathsofafunctionreturnavalue.strictNullChecks:Enablesstrictchecksforvariablesthatmightbeundefinedornull.
SummaryInthischapter,wehavelookedatvariousstepsinvolvedinmigratingaprojectfromJavaScripttoTypeScript.Wesawhowaprojectcanbemigratedgradually.WelookedatvariouswaystoupdateanoldprojectsothatitcanusenewJavaScriptfeaturesandhowitcanusethetypesystemofTypeScript.Youcanuseyourknowledgefromthepreviouschapterstomaketheprojectevenbetter.
AppendixA.BibliographyThislearningpathhasbeenpreparedforyoutobuildstunningapplicationsbyleveragingthefeaturesofTypeScript.ItcomprisesofthefollowingPacktproducts:
LearningTypeScript,RemoH.JansenTypeScriptDesignPatterns,VilicVaneTypeScriptBlueprints,IvoGabedeWolff
IndexA
action/Dataflowafter()function/Creatingtestassertions,specs,andsuiteswithMochaandChaiaggregation
about/Association,aggregation,andcompositionAkamai
URL/NetworkperformanceanduserexperienceAMDmodules
about/AMDmodules–runtimeonlyannotations
anddecorators/Annotationsanddecoratorsabout/Annotationsanddecoratorsclassdecorators/Theclassdecoratorsmethoddecorators/Themethoddecoratorspropertydecorators/Thepropertydecoratorsparameterdecorators/Theparameterdecoratorsdecoratorfactory/Thedecoratorfactorydecoratorswitharguments/DecoratorswithargumentsreflectionmetadataAPI/ThereflectionmetadataAPI
Anytype/Variables,basictypes,andoperatorsapplication/Applicationapplicationevents/WritinganMVCframeworkfromscratch,Applicationeventsapplicationprogramminginterface(API)/Theinterfacesegregationprincipleapplymethod/Thecall,apply,andbindmethodsapplyMixinsmethod/Mixinsarchitecture,single-pageapplication(SPA)
about/Theapplication'sarchitectureMarketControllercontroller,implementing/Theapplication'sarchitecture
arithmeticoperatorsabout/Arithmeticoperators+/Arithmeticoperators-/Arithmeticoperators*/Arithmeticoperators
arrowfunction/Arrowfunctionsassertion/Assertionsassignmentoperators
about/Assignmentoperators=/Assignmentoperators+=/Assignmentoperators-=/Assignmentoperators*=/Assignmentoperators
/=/Assignmentoperators%=/Assignmentoperators
associationabout/Association,aggregation,andcomposition
asynchronouscodetesting/Testingtheasynchronouscode
asynchronousflowcontrol,typesconcurrent/Promisesseries/Promiseswaterfall/Promisescomposite/Promises
asynchronousfunctionsasync/Asynchronousfunctions–asyncandawaitabout/Asynchronousfunctions–asyncandawaitawait/Asynchronousfunctions–asyncandawait
asynchronousprogrammingabout/AsynchronousprogramminginTypeScriptcallbacks/Callbacksandhigher-orderfunctionshigherorderfunctions/Callbacksandhigher-orderfunctionsarrowfunctions/Arrowfunctionscallbackhell/Callbackhellpromises/Promisesgenerators/Generatorsasynchronousfunctions/Asynchronousfunctions–asyncandawait
Atomabout/AtomURL/Atom
BBDD
about/Behavior-drivendevelopment(BDD)URL/Behavior-drivendevelopment(BDD)
before()function/Creatingtestassertions,specs,andsuiteswithMochaandChaibeforeEach()function/Creatingtestassertions,specs,andsuiteswithMochaandChai,Spiesbindmethod/Thecall,apply,andbindmethodsBitBucket
URL/Sourcecontroltoolsbitwiseoperators
about/Bitwiseoperators&/Bitwiseoperators|/Bitwiseoperators^/Bitwiseoperators~/Bitwiseoperators<</Bitwiseoperators>>/Bitwiseoperators>>>/Bitwiseoperators
BOM(BrowserObjectModel)/AmbientdeclarationsBower
about/BowerURL/Bower
browserifiedfunction/OptimizingaTypeScriptapplicationBrowserify
URL/CommonJSmodules–runtimeonly,UMDmodules–runtimeonlyBrowserObjectModel(BOM)/TheenvironmentBrowserSync
URL/Synchronizedcross-devicetesting
Ccallback/Callbacksandhigher-orderfunctionscallbackhell/Callbackhellcallmethod/Thecall,apply,andbindmethodsCentralProcessingUnit(CPU)/PerformanceandresourcesChai
about/Chaiused,forcreatingspecs/Creatingtestassertions,specs,andsuiteswithMochaandChaiused,forcreatingtestassertions/Creatingtestassertions,specs,andsuiteswithMochaandChaiused,forcreatingsuites/Creatingtestassertions,specs,andsuiteswithMochaandChaiURL/AssertingexceptionsTDD,versusBDD/TDDversusBDDwithMochaandChai
Chaplin/Controllerschartmodel,single-pageapplication(SPA)
implementing/Implementingthechartmodelchartview,single-pageapplication(SPA)
implementing/Implementingthechartviewcircular1.ts/Circulardependenciescircular2.ts/Circulardependenciescirculardependency
about/CirculardependenciesURL/Circulardependencies
class/Classesclassdecorators/Theclassdecoratorsclient-siderendering/Thesingle-pageapplicationarchitectureclosures
about/Closuresstaticvariables,usingwith/Staticvariableswithclosuresprivatemembers,usingwith/Privatememberswithclosures
collections/Collectionscollectionviews/CollectionviewsCommonJSmodules
runtime/CommonJSmodules–runtimeonlycomparisonoperators
about/Comparisonoperators==/Comparisonoperators===/Comparisonoperators!=/Comparisonoperators>/Comparisonoperators</Comparisonoperators
>=/Comparisonoperators<=/Comparisonoperators
complextypeserialization/ThereflectionmetadataAPIcomponents,TypeScript
language/TypeScriptcomponentscompiler/TypeScriptcomponentslanguageservices/TypeScriptcomponentsIDEintegration/TypeScriptcomponents
compositionabout/Association,aggregation,andcomposition,Composition
configurationtesting/Performancetestingautomationconstructor/ClassesContinuousIntegration(CI)tools
about/ContinuousIntegrationtoolscontroller/Controllers,WritinganMVCframeworkfromscratch,ControllercreateElementproperty/Specializedoverloadingsignatures
Ddata,single-pageapplication(SPA)
about/Theapplication'sdatamarketdata/Theapplication'sdatastockquotedata/Theapplication'sdatachartdata/Theapplication'sdata
datatypesBoolean/Variables,basictypes,andoperatorsnumber/Variables,basictypes,andoperatorsstring/Variables,basictypes,andoperatorsarray/Variables,basictypes,andoperatorsenum/Variables,basictypes,andoperatorsany/Variables,basictypes,andoperatorsvoid/Variables,basictypes,andoperators
declarationfiles/tsddecoratorfactory/Thedecoratorfactorydecorators
prerequisites/Prerequisitesandannotations/Annotationsanddecoratorswitharguments/Decoratorswitharguments
DefinitelyTypedabout/tsdURL/tsd
dependencyinversion(DI)principleabout/ThedependencyinversionprincipleURL/Thedependencyinversionprinciple
depthoftheinheritancetree(DIT)/Inheritancedesigntimecode/Designgoalsdevelopmentworkflow
about/Amoderndevelopmentworkflowprerequisites/Prerequisitespackagemanagementtools/Packagemanagementtoolstaskrunners/Taskrunnerstestrunner/Testrunnerssynchronizedcross-devicetesting/Synchronizedcross-devicetestingContinuousIntegration(CI)tools/ContinuousIntegrationtoolsscaffoldingtools/Scaffoldingtools
diamondproblem/Mixinsdispatcher/Dispatcher,WritinganMVCframeworkfromscratch,Dispatcherdo-whileexpression/Theexpressionistestedatthebottomoftheloop(do…while)DocumentObjectModel(DOM)/tsd,TheenvironmentDOM(DocumentObjectModel)/Ambientdeclarationsdon’trepeatyourself(DRY)/Generics
double-selectionstructure(if…else)/Thedouble-selectionstructure(if…else)dummy/Dummies
Eend-to-end(E2E)test
about/SeleniumandNightwatch.jsrunning,withSelenium/RunningE2EtestswithSeleniumandNightwatch.jsrunning,withNightwatch.js/RunningE2EtestswithSeleniumandNightwatch.jscreating,withNightwatch.js/Creatingend-to-endtestswithNightwatch.js
Errorclass/TheErrorclassES6modules
designtime/ES6modules–runtimeanddesigntimeruntime/ES6modules–runtimeanddesigntime
eventemitter/Eventemittereventloop
about/Theruntime,Theeventloopframes/Framesstack/Stackqueue/Queueheap/Heap
eventsabout/Eventsuserevents/Eventsapplicationevents/Events
Exceptionclass/TheErrorclassexceptionhandling
about/ExceptionhandlingErrorclass/TheErrorclasstry…catchstatements/Thetry…catchstatementsandthrowstatementsthrowstatements/Thetry…catchstatementsandthrowstatements
exceptionsasserting/Assertingexceptions
executiontimecode/Designgoalsexternalmodules
about/Externalmodules–designtimeonlydesigntime/Externalmodules–designtimeonly
Ffilestructure,single-pageapplication(SPA)
about/Theapplication'sfilestructure,Configuringtheautomatedbuildflowcontrolstatements
about/Flowcontrolstatementssingle-selectionstructure(if)/Thesingle-selectionstructure(if)double-selectionstructure(if…else)/Thedouble-selectionstructure(if…else)inlineternaryoperator(?)/Theinlineternaryoperator(?)multiple-selectionstructure(switch)/Themultiple-selectionstructure(switch)whileexpression/Theexpressionistestedatthetopoftheloop(while)do-whileexpression/Theexpressionistestedatthebottomoftheloop(do…while)for-instatement/Iterateoneachobject'sproperties(for…in)forstatement/Countercontrolledrepetition(for)
foofunction/Framesfor-instatement/Iterateoneachobject'sproperties(for…in)forstatement/Countercontrolledrepetition(for)frames/Framesframespersecond(FPS)/Framespersecond(FPS)framework/Frameworkfunctions,TypeScript
workingwith/WorkingwithfunctionsinTypeScriptdeclaring/Functiondeclarationsandfunctionexpressionsexpressions/Functiondeclarationsandfunctionexpressionstypes/Functiontypeswithoptionalparameters/Functionswithoptionalparameterswithdefaultparameters/Functionswithdefaultparameterswithrestparameters/FunctionswithrestparametersURL/Functionswithrestparametersoverloading/Functionoverloadingspecializedoverloadingsignature/Specializedoverloadingsignaturesfunctionscope/Functionscopeimmediatelyfunctions/Immediatelyinvokedfunctionsgenerics/Genericstagfunctions/Tagfunctionsandtaggedtemplatestaggedtemplates/Tagfunctionsandtaggedtemplates
Ggarbagecollection/Functionscopegenerators
URL/Scaffoldingtools/Generatorsgenericclasses
about/Genericclassesgenericconstraints
about/Genericconstraintsmultipletypes/Multipletypesingenerictypeconstraints
Gitabout/GitandGitHubURL/GitandGitHub,Sourcecontroltools
GitHubabout/GitandGitHubURL/GitandGitHub,Framespersecond(FPS)
GoogleAnalyticsURL/Performancemonitoringautomation
GooglePageSpeedInsightsURL/Networkperformancebestpracticesandrules
GPUperformanceanalysisabout/GPUperformanceanalysisframespersecond(FPS)/Framespersecond(FPS)
GraphicsProcessorUnit(GPU)/PerformanceandresourcesGrunt
URL/TaskrunnersGulp
tasksexecutionorder,managing/ManagingtheGulptasks'executionorderURL/ManagingtheGulptasks'executionorderabout/Gulpused,forbuildingapplication/BuildingtheapplicationwithGulp
gulp-typescriptdocumentationURL/CompilingtheTypeScriptcode
gulpsrcfunction/CheckingthequalityoftheTypeScriptcode
Hhandlebars
URL/CallbackhellHardDiskDrive(HDD)/PerformanceandresourcesHARviewer
URL/Performancemonitoringautomationhash(#)navigation
about/Routerandhash(#)navigationheap/Heaphigherorderfunctions/Callbacksandhigher-orderfunctionsHTTPArchive(HAR)/Performancemonitoringautomation
Iimmediatelyinvokedfunctionexpression(IIFE)/Immediatelyinvokedfunctionsimmediatelyinvokedfunctionexpression(IIFE)/Prototypesindependent(free)variables/Closuresinheritance
about/Inheritancemixins/Mixins
initializemethod/Application,Implementingthemarketcontrollerinlineternaryoperator(?)/Theinlineternaryoperator(?)instanceofoperator/Theclassdecoratorsinstanceproperties
versusclassproperties/Instancepropertiesversusclasspropertiesabout/Instancepropertiesversusclassproperties
interfaces/Interfaces,Interfacesinterfacesegregationprinciple(ISP)/TheinterfacesegregationprincipleInversionofControl(IoC)/ThedependencyinversionprincipleIstanbul
URL/Testcoverageabout/Istanbul
itemviews/Itemviews
JJavadocumentation
URL/SeleniumandNightwatch.jsJQuery/npm
KKarma
about/Karmaused,forrunningunittest/RunningtheunittestwithKarmaURL/RunningtheunittestwithKarma
karmaconfigurationdocumentation,URL/Testrunners
Llanguagefeatures
about/TypeScriptlanguagefeaturestypes/Typesvariables/Variables,basictypes,andoperatorsvar/Var,let,andconstlet/Var,let,andconstconst/Var,let,andconstuniontypes/Uniontypestypeguards/Typeguardstypealiases/Typealiasesambientdeclarations/Ambientdeclarationsarithmeticoperators/Arithmeticoperatorscomparisonoperators/Comparisonoperatorsflowcontrolstatements/Flowcontrolstatementsfunctions/Functionsclasses/Classesinterfaces/Interfacesmodules/Namespaces
LastInFirstOut(LIFO)/Stacklayout,single-pageapplication(SPA)
about/Theapplication'slayoutLiskovsubstitutionprinciple(LSP)/TheLiskovsubstitutionprincipleloadtesting/Performancetestingautomationlogicaloperators
about/Logicaloperators&&/Logicaloperators||/Logicaloperators!/Logicaloperators
Mmark/Thegarbagecollectormark-and-sweepalgorithm/Thegarbagecollectormarketcontroller,single-pageapplication(SPA)
implementing/Implementingthemarketcontrollermarkettemplate,single-pageapplication(SPA)
implementing/Implementingthemarkettemplatemarketview,single-pageapplication(SPA)
implementing/ImplementingthemarketviewMarkit
about/Theapplication'sdataURL/Theapplication'sdata
mediator/Mediator,WritinganMVCframeworkfromscratch,Mediatormemoryleak
about/Memoryperformanceanalysisissues,preventing/Thegarbagecollector
methoddecoratorsabout/Themethoddecoratorsinvoking/Themethoddecorators
methodoverriding/Inheritancemixin/Mixins
about/MixinsMocha
about/Mochaused,forcreatingtestassertions/Creatingtestassertions,specs,andsuiteswithMochaandChaiused,forcreatingspecs/Creatingtestassertions,specs,andsuiteswithMochaandChaiused,forcreatingsuites/Creatingtestassertions,specs,andsuiteswithMochaandChaiTDD,versusBDD/TDDversusBDDwithMochaandChai
mocksabout/Mocks
modelabout/Models,WritinganMVCframeworkfromscratch,Modelandmodelsettingssettings/Modelandmodelsettings
Model-View-Controller(MVC)/TheMV*architectureModel-View-Presenter(MVP)/TheMV*architectureModel-View-ViewModel(MVVM)/TheMV*architectureModernizr
about/TheenvironmentURL/Theenvironment
moduleloaderabout/ModulesRequireJS/ModulesBrowserify/ModulesSystemJS/Modules
modules/Namespacesabout/ModulesES6modules/ES6modules–runtimeanddesigntimeexternalmodules/Externalmodules–designtimeonlyAMDmodules/AMDmodules–runtimeonlyCommonJSmodules/CommonJSmodules–runtimeonlyUMDmodules/UMDmodules–runtimeonlySystemJSmodules/SystemJSmodules–runtimeonly
multiple-selectionstructure(switch)/Themultiple-selectionstructure(switch)multipleinheritance/MixinsMV*architecture
about/TheMV*architectureMV*frameworks
components/CommoncomponentsandfeaturesintheMV*frameworksfeatures/CommoncomponentsandfeaturesintheMV*frameworksmodel/Modelscollections/Collectionsitemviews/Itemviewscollectionviews/Collectionviewscontrollers/Controllersevents/Eventsrouter/Routerandhash(#)navigationhash(#)navigation/Routerandhash(#)navigationmediator/Mediatordispatcher/Dispatcherclient-siderendering/Client-siderenderingandVirtualDOMVirtualDOM/Client-siderenderingandVirtualDOMuserinterface(UI)databinding/Userinterfacedatabindingdataflow/Dataflowwebcomponents/WebcomponentsandshadowDOMshadowDOM/WebcomponentsandshadowDOMapplicationframework,selecting/Choosinganapplicationframework
MVCframeworkwriting,fromscratch/WritinganMVCframeworkfromscratchprerequisites/Prerequisites
MVCframeworkcomponentsapplication/WritinganMVCframeworkfromscratch,Applicationmediator/WritinganMVCframeworkfromscratch,Mediatorapplicationevents/WritinganMVCframeworkfromscratch,Applicationevents
router/WritinganMVCframeworkfromscratch,Routerroutes/WritinganMVCframeworkfromscratch,Routecontrollers/WritinganMVCframeworkfromscratchmodels/WritinganMVCframeworkfromscratch,Modelandmodelsettingsviews/WritinganMVCframeworkfromscratcheventemitter/Eventemitterdispatcher/Dispatchercontroller/Controllermodelsettings/Modelandmodelsettingsviewsettings/Viewandviewsettingsview/Viewandviewsettingsframework/Framework
N@namedecorator/ThereflectionmetadataAPInamespaces
about/NamespacesNASDAQmodel,single-pageapplication(SPA)
implementing/ImplementingtheNASDAQmodelNationalAssociationofSecuritiesDealersAutomatedQuotations(NASDAQ)/Theapplication'srequirementsnetsniff.jsfile
URL/Performancemonitoringautomationnetworkperformance
anduserexperience/Networkperformanceanduserexperiencebestpracticesandrules/Networkperformancebestpracticesandrules
NewYorkstockexchange(NYSE)/Theapplication'srequirementsnext()function/GeneratorsNightwatch.js
about/SeleniumandNightwatch.jsused,forrunningend-to-end(E2E)test/RunningE2EtestswithSeleniumandNightwatch.jsURL/RunningE2EtestswithSeleniumandNightwatch.jsused,forcreatingend-to-endtests/Creatingend-to-endtestswithNightwatch.js
Node.jsURL/TypeScriptlanguagefeatures,Node.jsabout/Node.js
nodepackagemanager(npm)/Prerequisitesnpm
about/npmURL/npm
npminitcommand/npmNYSEmodel,single-pageapplication(SPA)
implementing/ImplementingtheNYSEmodel
Oobject-orientedprogramming(OOP)/SOLIDprinciplesObject.definePropertyfunction/ThepropertydecoratorsObject.getOwnPropertyDescriptor()method/Themethoddecoratorsobjectprototype,accessing
person.prototype/Accessingtheprototypeofanobjectperson.getPrototypeOf(person)/Accessingtheprototypeofanobjectperson.__proto__/Accessingtheprototypeofanobject
onSubmit()function/Spiesoptionalstatictypenotation/Optionalstatictypenotation
P@parameterTypesdecorator/ThereflectionmetadataAPIpackage.jsonconfiguration
URL/npmpackagemanagementtools
about/Packagemanagementtoolsnpm/npmBower/Bowertsd/tsd
parameterdecorators/Theparameterdecoratorsperformance-bookmarklet
about/NetworkperformanceanalysisURL/Networkperformanceanalysis
performanceanalysisabout/Performanceanalysisnetworkperformance/NetworkperformanceanalysistimingAPIdatapoints/Networkperformanceanalysisuserexperience/NetworkperformanceanduserexperienceGPUperformanceanalysis/GPUperformanceanalysisCPUperformanceanalysis/CPUperformanceanalysismemoryperformanceanalysis/Memoryperformanceanalysisgarbagecollector/Thegarbagecollector
performanceautomationabout/Performanceautomationoptimizationautomation/Performanceoptimizationautomationmonitoringautomation/Performancemonitoringautomationtestingautomation/Performancetestingautomation
performancemetricsabout/Performancemetricsavailability/Availabilityresponsetime/Theresponsetimeprocessingspeed(clockrate)/Processingspeedlatency/Latencybandwidth/Bandwidthscalability/Scalability
performancemonitoringautomationabout/Performancemonitoringautomationrealusermonitoring(RUM)/Performancemonitoringautomationsimulatedbrowsers/Performancemonitoringautomationreal-browsermonitoring/Performancemonitoringautomation
performancetestingautomationabout/Performancetestingautomationloadtesting/Performancetestingautomation
stresstesting/Performancetestingautomationsoaktesting/Performancetestingautomationspiketesting/Performancetestingautomationconfigurationtesting/Performancetestingautomation
Personclass/ClassesPhantomJS/Performancemonitoringautomation
URL/Performancemonitoringautomationabout/PhantomJS
prerequisites,applicationtestingabout/PrerequisitesGulp/GulpKarma/KarmaIstanbul/IstanbulMocha/MochaChai/ChaiSinon.JS/Sinon.JStypedefinitions/TypedefinitionsPhantomJS/PhantomJSSelenium/SeleniumandNightwatch.jsNightwatch.js/SeleniumandNightwatch.js
prerequisites,decoratorsabout/Prerequisites
prerequisites,developmentworkflowabout/PrerequisitesNode.js/Node.jsAtom/AtomGit/GitandGitHubGitHub/GitandGitHub
prerequisites,single-pagewebapplicationabout/Prerequisites
privatemembersusing,withclosures/Privatememberswithclosures
promisesabout/Promisespendingstate/Promisesfulfilledstate/Promisesrejectedstate/Promises
propertydecorators/Thepropertydecoratorspropertyshadowing/Theprototypechainprototypes
about/Prototypesinstanceproperties,versusclassproperties/Instancepropertiesversusclasspropertiesinheritance/Prototypalinheritance
chain/Theprototypechainofobject,accessing/Accessingtheprototypeofanobject
pub/sub/Mediatorpublish/subscribedesignpattern
publishmethod/Mediatorsubscribemethod/Mediatorunsubscribe/Mediator
QQ(version1.0.1)
URL/Promisesqueue/Queue
R@returnTypedecorator/ThereflectionmetadataAPIRandomAccessMemory(RAM)/PerformanceandresourcesReflect.getMetadata()function/ThereflectionmetadataAPIreflectionmetadataAPI/ThereflectionmetadataAPIrendermethod/SpiesrequestAsyncmethod/ModelandmodelsettingsRequireJS
URL/AMDmodules–runtimeonlyrequisites,single-pagewebapplication/Theapplication'srequirementsresources
CentralProcessingUnit(CPU)/PerformanceandresourcesGraphicsProcessorUnit(GPU)/PerformanceandresourcesRandomAccessMemory(RAM)/PerformanceandresourcesHardDiskDrive(HDD)/PerformanceandresourcesSolidStateDrive(SSD)/Performanceandresourcesnetworkthroughput/Performanceandresources
resourcetimingURL/Networkperformanceanalysis
responsetimeabout/Theresponsetimewaittime/Theresponsetimeservicetime/Theresponsetimetransmissiontime/Theresponsetime
rootcomponent,single-pageapplication(SPA)implementing/Implementingtherootcomponent
routerabout/Thesingle-pageapplicationarchitecture,Routerandhash(#)navigation,WritinganMVCframeworkfromscratch,Router
routes/Routeruntimecode/Designgoalsruntimeenvironment
about/Theenvironment
Sscaffoldingtool
about/ScaffoldingtoolsSelenium
about/SeleniumandNightwatch.jsused,forrunningend-to-end(E2E)test/RunningE2EtestswithSeleniumandNightwatch.js
serializemethod/RouteshadowDOM/WebcomponentsandshadowDOMshooterfunction/Closuressingle-pagewebapplication
prerequisites/Prerequisitesrequisites/Theapplication'srequirementsdata/Theapplication'sdataarchitecture/Theapplication'sarchitecturefilestructure/Theapplication'sfilestructureautomatedbuild,configuring/Configuringtheautomatedbuildlayout/Theapplication'slayoutrootcomponent,implementing/Implementingtherootcomponentmarketcontroller,implementing/ImplementingthemarketcontrollerNASDAQmodel,implementing/ImplementingtheNASDAQmodelNYSEmodel,implementing/ImplementingtheNYSEmodelmarketview,implementing/Implementingthemarketviewmarkettemplate,implementing/Implementingthemarkettemplatesymbolcontroller,implementing/Implementingthesymbolcontrollersymbolview,implementing/Implementingthesymbolviewchartmodel,implementing/Implementingthechartmodelchartview,implementing/Implementingthechartviewtesting/Testingtheapplicationpreparing,forproductionrelease/Preparingtheapplicationforaproductionrelease
single-selectionstructure(if)/Thesingle-selectionstructure(if)Sinon.JS
about/Sinon.JSused,fortestingspies/TestspiesandstubswithSinon.JS,Spiesused,fortestingstubs/TestspiesandstubswithSinon.JS,StubsURL/Spies
soaktesting/Performancetestingautomationsoftwaretesting
about/Softwaretestingglossaryassertion/Assertionsspecs/Specstestcase/Testcases
suites/Suitesspies/Spiesdummy/Dummiesstub/Stubsmocks/Mockstestcoverage/Testcoverage
SOLIDprinciplesabout/SOLIDprinciples,ApplyingtheSOLIDprinciplessingleresponsibilityprinciple(SRP)/SOLIDprinciplesopen/closedprinciple(OCP)/SOLIDprinciplesLiskovsubstitutionprinciple(LSP)/SOLIDprinciples,TheLiskovsubstitutionprincipleinterfacesegregationprinciple(ISP)/SOLIDprinciples,Theinterfacesegregationprincipledependencyinversion(DI)principle/SOLIDprinciples,Thedependencyinversionprinciple
SolidStateDrive(SSD)/Performanceandresourcessourcecontroltools
about/SourcecontroltoolsSPA
architecture/Thesingle-pageapplicationarchitecturespecs
about/Specscreating,withMono/Creatingtestassertions,specs,andsuiteswithMochaandChaicreating,withChai/Creatingtestassertions,specs,andsuiteswithMochaandChai
spiesabout/Spiestesting,withSinon.JS/TestspiesandstubswithSinon.JS,Spies
spiketesting/Performancetestingautomationstack/Stackstaticvariables
using,withclosures/Staticvariableswithclosuresstresstesting/Performancetestingautomationstubs
about/Stubstesting,withSinon.JS/TestspiesandstubswithSinon.JS,Stubs
suitesabout/Suitescreating,withMono/Creatingtestassertions,specs,andsuiteswithMochaandChaicreating,withChai/Creatingtestassertions,specs,andsuiteswithMochaandChai
sweep/Thegarbagecollectorsymbolcontroller,single-pageapplication(SPA)
implementing/Implementingthesymbolcontrollerquotemodel,implementing/Implementingthequotemodel
symbolview,single-pageapplication(SPA)implementing/Implementingthesymbolview
synchronizedcross-devicetestingabout/Synchronizedcross-devicetesting
SystemJSmodulesabout/SystemJSmodules–runtimeonlyURL/SystemJSmodules–runtimeonlymistakes,URL/SystemJSmodules–runtimeonly
T@typedecorator/ThereflectionmetadataAPItagfunction/Tagfunctionsandtaggedtemplatestaskrunners
about/TaskrunnersTypeScriptcodequality,checking/CheckingthequalityoftheTypeScriptcodeTypeScriptcode,compiling/CompilingtheTypeScriptcodeTypeScriptapplication,optimizing/OptimizingaTypeScriptapplicationGulptasksexecutionorder,managing/ManagingtheGulptasks'executionorder
TDDabout/Test-drivendevelopmentversusBDD,withChai/TDDversusBDDwithMochaandChai
TemplateStrings/Functionoverloadingtestassertions
creating,withMocha/Creatingtestassertions,specs,andsuiteswithMochaandChaicreating,withChai/Creatingtestassertions,specs,andsuiteswithMochaandChai
testcase/Testcasestestcoverage
about/Testcoveragetestcoveragereports
generating/Generatingtestcoveragereportstestinfrastructure
settingup/Settingupatestinfrastructureapplication,buildingwithGulp/BuildingtheapplicationwithGulpunittest,runningwithKarma/RunningtheunittestwithKarmaE2Etests,runningwithSelenium/RunningE2EtestswithSeleniumandNightwatch.jsE2Etests,runningwithNightwatch.js/RunningE2EtestswithSeleniumandNightwatch.js
testingplanningabout/TestingplanningandmethodologiesTDD/Test-drivendevelopmentBDD/Behavior-drivendevelopment(BDD)behavior-drivendevelopment(BDD)/Behavior-drivendevelopment(BDD)termtestplan/Testsplansandtesttypestesttypes/Testsplansandtesttypes
testrunnerabout/TestrunnersKarma/Testrunners
testtypesunittest/Testsplansandtesttypespartialintegrationtests/Testsplansandtesttypes
fullintegrationtests/Testsplansandtesttypesregressiontests/Testsplansandtesttypesperformance/loadtests/Testsplansandtesttypesend-to-end(E2E)tests/Testsplansandtesttypesuseracceptancetests(UAT)/Testsplansandtesttypes
thisoperatorabout/ThethisoperatorURL/Thethisoperatoringlobalcontext/Thethisoperatorintheglobalcontextinfunctioncontext/Thethisoperatorinafunctioncontextcallmethod/Thecall,apply,andbindmethodsapplymethod/Thecall,apply,andbindmethodsbindmethod/Thecall,apply,andbindmethods
TodoMVCURL/Choosinganapplicationframework
TraceEventProfilingToolURL/Framespersecond(FPS)
transpiler/TypeScriptcomponentsTravisCI
URL/ContinuousIntegrationtoolsconfigurationoptions,URL/ContinuousIntegrationtoolsdocumentation,URL/ContinuousIntegrationtools
try…catchstatement/Promisestypedefinitionfile/tsdtypedefinitions/Typedefinitionstypeguards/TypeguardsTypeinference/Optionalstatictypenotationtypes,languagefeatures
about/Typesoptionalstatictypenotation/Optionalstatictypenotation
TypeScriptarchitecture/TheTypeScriptarchitecturecomponents/TypeScriptcomponentsURL/TypeScriptcomponents,TypeScriptlanguagefeatureslanguagefeatures/TypeScriptlanguagefeaturesplugins,URL/TypeScriptlanguagefeaturesexample/Puttingeverythingtogetherfunctions,workingwith/WorkingwithfunctionsinTypeScriptasynchronousprogramming/AsynchronousprogramminginTypeScript
TypeScriptarchitectureabout/TheTypeScriptarchitecturedesigngoals/Designgoals
TypeScriptclasses/Classesabout/Classes
TypeScriptcodequality,checking/CheckingthequalityoftheTypeScriptcodecompiling/CompilingtheTypeScriptcode
TypeScriptDefinitions(tsd)/tsdTypeScriptextension
URL,fordownload/TypeScriptlanguagefeaturesTypeScriptfunctions/Functions
Uuniversalmoduledefinition(UMD)
about/UMDmodules–runtimeonlyunsubscribeToEventsmethod/Eventemitteruserinterface(UI)databinding
about/Userinterfacedatabindingone-waydatabinding/One-waydatabindingtwo-waydatabinding/Two-waydatabinding
VvalidateEmailmethod/ClassesViewclass
containerproperty/Callbackhelltemplateproperty/Callbackhellserviceproperty/Callbackhell
viewsabout/WritinganMVCframeworkfromscratch,Viewandviewsettingssettings/Viewandviewsettings
VisualStudio(VS)/TypeScriptcomponentsURLs/Atom
Wwebcomponents/WebcomponentsandshadowDOMwebperformanceanalysis
prerequisites/Prerequisitesresources/Performanceandresources
webworkers/Theeventloopwhileexpression/Theexpressionistestedatthetopoftheloop(while)
YYSlow
URL/Networkperformancebestpracticesandrules