TypeScript |
---|
This help article explains what I learned when converting the Mystery Master Logic Puzzle Solver Application (aka Mystery Master App, or simply app) from JavaScript to TypeScript. Wikipedia says TypeScript, developed by Microsoft, is a strict syntactical superset of JavaScript with optional static typing. TutorialsTeacher says TypeScript increases code quality, readability and makes it easy to maintain and refactor codebase. More importantly, errors can be caught at compile time rather than at runtime. It supports object-oriented programming features like data types, classes, enums, and more. TypeScript transcompiles to JavaScript. A JavaScript program is also a valid TypeScript program.
I will say this: the things I don't like about JavaScript are the things I don't like about TypeScript. I think JavaScript is more functional than object-oriented language. I don't want to define a function as a class
, then initiate an instance of the class with the constructor
method. And I really don't like prefixing every class member with the keyword this
. That said, I do like I can easily export
and input
classes, and use them as the data type for a variable. Example: const viewer: Viewer = new Viewer();
I just wish I could use a function as a data type.
Node.js (aka node) is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node allows you to run JavaScript on your development computer (aka server), compared to running JavaScript in a web browser (aka client). The Node Package Manager (npm) is a package manager for the JavaScript programming language maintained by npm, Inc. Open the terminal window of VS for the following examples.
npm -v
.npm install -g npm
node -v
Here are ways to maintain TypeScript. If you want to learn how to compile, please see my metawork article after reading this article.
npm install -g typescript
I updated my comments from JSDoc to TSDoc. To see how I generated documentation from my comments, see my article on metawork.
In object-oriented programming, the concept of encapsulation is used to make class members public or private i.e. a class can control the visibility of its data members. This is done using access modifiers. TypeScript has three access modifiers: public, private and protected.
public
- all class members in TypeScript are public by default and can be accessed anywhere.private
- private members are visible only to that class and are not accessible outside the class.protected
- similar to private, except protected members can be accessed from a child class that extends the parent class.
JavaScript (and TypeScript) have three primitive data types: boolean, number, and string. In TypeScript, an array can be defined as a data type followed by brackets. Examples: boolean[], number[], and string[]. The any
type means not to restrict the type. In TypeScript, you can specify the types of both the input and output values of functions. A class I define can be the type for a variable. Implicitly, const myObj = new MyClass();
is explicitly given as const myObj: MyClass = new MyClass();
. I can even define types that look similar to object literals.
I didn't use modules in JavaScript, but with TypeScript I am. The TypeScript website says modules are executed within their own scope, not in the global scope; this means that variables, functions, classes, etc. declared in a module are not visible outside the module unless they are explicitly exported using one of the export forms. Conversely, to consume a variable, function, class, interface, etc. exported from a different module, it has to be imported using one of the import forms. I like modules because there is no this
to worry about.
I grouped my TypeScript (ts) files into these subfolders under the ts
folder: puzzle
, puzzles
, server
, solver
, and viewer
. Each folder can be considered a library or assembly of files, where all the files in that folder have the same objective. I must point out that files in the server folder only run on my development server under Node.js - they are NOT uploaded to the server hosting my website.
Name | Description |
---|---|
puzzle | Builds a logic puzzle. These files are the building blocks of a logic puzzle. |
puzzles | Loads a unique logic puzzle. A file in this folder is called a puzzle module. |
server | Performs "metawork" on logic puzzles. Only runs under Node.js on the server. |
solver | Solves a logic puzzle. These files help solve a logic puzzle. |
viewer | Displays a logic puzzle in a browser. These files show stats and data for solving a logic puzzle. |
The table belows lists the TypeScript files under each folder sorted by name, and gives the dependencies (imports) it has. The puzzle modules in the puzzles
folder are not given here.
Folder | Filename | Type | Dependencies |
---|---|---|---|
puzzle | |||
Fact | Class | Verb, Noun, Link | |
Helper | Module | Verb, Noun, Puzzle | |
ISolver | Interface | Verb, NounType, Noun, Rule, Mark | |
Link | Class | Verb, NounType, Noun | |
Mark | Class | Verb, Noun, Fact, Rule | |
Noun | Class | NounType, Fact, Helper | |
NounType | Class | Noun | |
Puzzle | Class | Verb, NounType, Noun, Link, Fact, Rule, SmartLink, Helper | |
Rule | Class | Noun, Mark | |
SmartLink | Module | Verb, Noun, LinkFunction | |
SmartRule | Module | Verb, NounType, Noun, Link, Rule, RuleFunction, Mark, Helper, Solver | |
Verb | Class | [none] | |
server | |||
Filer | Module | Puzzle, Solver, Viewer, Former | |
solver | |||
Finder | Function | Verb, NounType, Noun, Fact, Puzzle, Mark, Helper, Solver, Loner | |
IViewer | Interface | Fact, Rule, Mark | |
Lawyer | Function | Verb, NounType, Noun, Fact, Puzzle, Mark, Helper | |
Loner | Class | Verb, NounType, Noun, Puzzle, Solver | |
Solver | Class | Verb, NounType, Noun, Link, Fact, Rule, Mark, MarkType, Puzzle, Helper, Finder, Lawyer, Viewer | |
Stats | Class | Mark, Solver | |
viewer | |||
Board | Function | Puzzle, Solver, Viewer, UIX | |
Filer | Stub | Puzzle, Solver | |
Former | Module | Puzzle, Helper, Solver, Stats, LevelCounter, UIX | |
Locker | Module | Helper | |
Setup | Function | Solver, Viewer, UIX | |
Tabby | Function | Verb, Fact, Rule, Puzzle, Mark, Helper, Solver, Stats, Viewer, Locker, Former, UIX | |
UIX | Module | Verb, Puzzle, Viewer, Locker | |
Viewer | Class | Verb, Fact, Rule, Mark, Puzzle, Helper, Solver, IViewer, Board, Tabby, Setup |
A type alias is a name for a defined type. This is convenient when the same type is needed more than once. Below is a table with the file and the type defined in the file.
Filename | Type Alias |
---|---|
Link | export type LinkFunction = (noun1: Noun, noun2: Noun) => Verb; |
Rule | export type RuleFunction = (mark: Mark) => number; |
Mark | export type MarkType = { num: number; name: string; } |
An Interface is a contract that a class must implement. Classes that are derived from an interface must follow the structure provided by their interface. The interfaces are given below.
When the solver calls the viewer, this is an event that the program may pause on. When the program resumes, the viewer must know what method (function) to call. This function the viewer is calling is a callback. The callback invoked with the setTimeout statement. Using setTimeout
allows time for the user to interact with the user interface (UI). In other programming languages, threads could be used to pause/resume a running program. JavaScript has Web Workers which run on their own thread, but managing communication between the worker and the main thread is difficult.
Callbacks are not required for modules within the puzzle and puzzles folder. Callbacks are performed internally within the Finder object. Callbacks are essential between the Solver object and the Viewer object. The viewer has several methods called by the solver (hence the IViewer interface), but only the viewer's doResume
method needs to make the callback. This method could be written in one line:
if (this.solver.callback !== null) setTimeout(this.solver.callback, 0);
Question: Could I make the callback without using setTimeout
? Below is a table containing how the solver sets the callbacks that the doResume
method calls.
Solver Method | Edited Callback Code |
---|---|
sayStarted | finder.doWork |
sayStopped | null |
sayLevel | Set by finder.doNextLevel to doLevel1, doLevel2, doLevel3, or doLevel4 |
saySolution | quitFlag ? sayStopped : undoAssumption |
sayAddMark | doResume |
sayRemoveMark | if (who === 1) { mark.type === Mark.Type.User ? null : undoUserMark } if (who === 2) { mark.type === Mark.Type.Level ? finder.doResume : undoAssumption } |
sayValidMark | doResume |
sayContradiction | quitFlag ? doQuit : undoAssumption |
sayFactViolation | quitFlag ? doQuit : undoAssumption |
sayRuleViolation | quitFlag ? doQuit : undoAssumption |
sayLawViolation | quitFlag ? doQuit : undoAssumption |
sayPlacers | doResume |
The solver's doResume
method is the callback function for sayAddMark, sayValidMark, and sayPlacers
. Depending on the number of validated marks, this method will call either lawyer.doWork
, saySolution
, finder.doResume
, or nothing.
The finder's doResume
method is the callback function for sayRemoveMark
. Depending on whether assumptions are being made, it will either continue to make assumptions, or start again at level 1.
In JavaScript, I created an object literal to contain helper methods. When I converted to TypeScript, I changed the object literal to a class that was exported. Now that I'm using modules, I converted my "helper" class to a module file that contains my exported methods. I can then import all methods in one gulp.
Helper.js as Object Literal | Helper.ts as Class | Helper.ts as Module |
---|---|---|
const Helper = { method1() { }, method2() { }, method3() { } }; |
export class Helper { static method1() { } static method2() { } static method3() { } } import { Helper } from "./path/Helper"; |
export function method1() { } export function method2() { } export function method3() { } import * as Helper from "./path/Helper"; |
let
and is not referenced elsewhere, set it to the empty array. This technique redefines the array, so it does not work for constants. If the array is defined with const
, set its length to zero. I think its best to set its length to zero in either case.
let a = [1, 2, 3]; a = []; |
const a = [1, 2, 3]; a.length = 0; |
obj?.prop
. This will return "undefined" instead of throwing an error.
window
. Globals defined in JavaScript are also in this global scope. Example: let myName = "Michael"
is the same as window.myName
.
global
, but I receive an error when I try to refence this in a Visual Studio Code terminal window.
fs
module. As explained by TutorialsTeacher, the fs module is responsible for all the asynchronous or synchronous file I/O operations.