Created
June 4, 2019 17:14
-
-
Save jarhoads/232c52e0439f965493cf6455f3731dfd to your computer and use it in GitHub Desktop.
typescript classes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// constructors | |
// All classes in TypeScript have a constructor, whether you specify one or not. | |
// If you leave out the constructor, the compiler will automatically add one. | |
// For a class that doesn't inherit from another class, the automatic constructor will be | |
// parameterless and will initialize any class properties. | |
// Where the class extends another class, the automatic constructor will | |
// match the superclass signature and will pass arguments to the superclass before | |
// initializing any of its own properties. | |
class Song { | |
constructor(private artist: string, private title: string) { } | |
play() { | |
console.log('Playing ' + this.title + ' by ' + this.artist); | |
} | |
} | |
class Jukebox { | |
constructor(private songs: Song[]) { } | |
play() { | |
const song = this.getRandomSong(); | |
song.play(); | |
} | |
private getRandomSong() { | |
const songCount = this.songs.length; | |
const songIndex = Math.floor(Math.random() * songCount); | |
return this.songs[songIndex]; | |
} | |
} | |
const songs = [ | |
new Song('Bushbaby', 'Megaphone'), | |
new Song('Delays', 'One More Lie In'), | |
new Song('Goober Gun', 'Stereo'), | |
new Song('Sohnee', 'Shatter'), | |
new Song('Get Amped', 'Celebrity') | |
]; | |
const jukebox = new Jukebox(songs); | |
jukebox.play(); | |
// You can also add static properties to your class, which are defined in the same way | |
// as instance properties, but with the static keyword between the access modifier | |
// (if one is specified) and the identifier. | |
// You can make both static and instance properties read-only to prevent the values being overwritten. | |
class Playlist { | |
private songs: Song[] = []; | |
static readonly maxSongCount = 30; | |
constructor(public name: string) { | |
} | |
addSong(song: Song) { | |
if (this.songs.length >= Playlist.maxSongCount) { | |
throw new Error('Playlist is full'); | |
} | |
this.songs.push(song); | |
} | |
} | |
// Creating a new instance | |
const playlist = new Playlist('My Playlist'); | |
// Accessing a public instance property | |
const playName = playlist.name; | |
// Calling a public instance method | |
playlist.addSong(new Song('Therapy?', 'Crooked Timber')); | |
// Accessing a public static property | |
const maxSongs = Playlist.maxSongCount; | |
// Error: Cannot assign to a readonly property | |
// Playlist.maxSongCount = 20; | |
// TypeScript supports property getters and setters, if you are targeting ECMAScript 5 or above | |
// property getters and setters allow you to wrap property access with a method while | |
// preserving the appearance of a simple property to the calling code. | |
interface StockItem { | |
description: string; | |
asin: string; | |
} | |
class WarehouseLocation { | |
private _stockItem: StockItem; | |
constructor(public aisle: number, public slot: string) { } | |
get stockItem() { | |
return this._stockItem; | |
} | |
set stockItem(item: StockItem) { | |
this._stockItem = item; | |
} | |
} | |
const figure = { asin: 'B001TEQ2PI', description: 'Figure' }; | |
const warehouseSlot = new WarehouseLocation(15, 'A6'); | |
warehouseSlot.stockItem = figure; | |
// There are two types of class heritage in TypeScript. | |
// A class can implement an interface using the implements keyword and | |
// a class can inherit from another class using the extends keyword. | |
// If you do specify the interface using the implements keyword, | |
// your class will be checked to ensure that it complies with the contract promised by the interface. | |
interface Audio { | |
play(): any; | |
} | |
class AudioSong implements Audio { | |
constructor(private artist: string, private title: string) { } | |
play(): void { | |
console.log('Playing ' + this.title + ' by ' + this.artist); | |
} | |
static Comparer(a: AudioSong, b: AudioSong) { | |
if (a.title === b.title) { | |
return 0; | |
} | |
return a.title > b.title ? 1 : -1; | |
} | |
} | |
class AudioPlaylist { | |
constructor(public songs: Audio[]) { | |
} | |
play() { | |
var song = this.songs.pop(); | |
song.play(); | |
} | |
sort() { | |
this.songs.sort(AudioSong.Comparer); | |
} | |
} | |
//An extends clause makes your class a derived class, | |
// and it will gain all the properties and methods of the base class from which it inherits. | |
// You can override a public member of the base class by adding a member of the same name and kind as the base class member. | |
// The RepeatingPlaylist inherits from the Playlist class and uses the songs property from the base class using this.songs, | |
// but overrides the play method with a specialized implementation that plays the next song in a repeating loop. | |
class RepeatingPlaylist extends AudioPlaylist { | |
private songIndex = 0; | |
// the constructor could be omitted because the automatic constructor | |
// that would be generated would match it exactly. | |
constructor(songs: AudioSong[]) { | |
super(songs); | |
} | |
play() { | |
this.songs[this.songIndex].play; | |
this.songIndex++; | |
if (this.songIndex >= this.songs.length) { | |
this.songIndex = 0; | |
} | |
} | |
} | |
// If the subclass accepts additional arguments there are a couple of rules you need to follow. | |
// The super call to the base class must be the first statement in the subclass constructor and | |
// you cannot specify a more restrictive access modifier for a parameter on the subclass than it has on the base class. | |
// There are some rules that must be followed for inheritance: | |
// A class can only inherit from a single superclass. | |
// A class cannot inherit from itself, either directly or via a chain of inheritance. | |
// It is possible to create a class that inherits from another class and implements multiple interfaces. | |
// Abstract Class | |
// An abstract class can be used as a base class, but can't be instantiated directly. | |
// Abstract classes can contain implemented methods as well as abstract methods, | |
// which have no implementation and must be implemented by any subclass. | |
// abstract logger class with an abstract notify method, and an implemented protected getMessage method. | |
// Each subclass must implement the notify method, but can share the getMessage implementation from the base class. | |
// Abstract class | |
abstract class Logger { | |
abstract notify(message: string): void; | |
protected getMessage(message: string): string { | |
return `Information: ${new Date().toUTCString()} ${message}`; | |
} | |
} | |
class ConsoleLogger extends Logger { | |
notify(message) { | |
console.log(this.getMessage(message)); | |
} | |
} | |
class InvasiveLogger extends Logger { | |
notify(message) { | |
alert(this.getMessage(message)); | |
} | |
} | |
let logger: Logger; | |
// Error. Cannot create an instance of an abstract class | |
// logger = new Logger(); | |
// Create an instance of a sub-class | |
logger = new InvasiveLogger(); | |
logger.notify('Hello World'); | |
// Abstract classes are like interfaces, in that they contain a contract that may have no implementation. | |
// They can add to this with implementation code, and can specify access modifiers against the members: | |
// two things that interfaces cannot do. | |
// Scope | |
// If you call a class method from an event, or use it as a callback, | |
// the original context of the method can be lost, | |
// which results in problems using instance methods and instance properties. | |
// When the context is changed, the value of the this keyword is replaced. | |
// typical example of lost context. | |
// If the registerClick method is called directly against the clickCounter instance, | |
// it works as expected. | |
// When the registerClick method is assigned to the onclick event, | |
// the context is lost and this.count is undefined in the new context. | |
class ClickCounter { | |
private count = 0; | |
registerClick() { | |
this.count++; | |
alert(this.count); | |
} | |
} | |
const clickCounter = new ClickCounter(); | |
document.getElementById('target').onclick = clickCounter.registerClick; | |
// several techniques that can be used to preserve the context to enable this to work | |
// Proprty and Arrow Function | |
// You can replace the method with a property and initialize the property using an arrow function | |
// This is a reasonable technique if you know that the class will be consumed with events or callbacks, | |
// but it is less of an option if your class has no knowledge of when and where it may be called. | |
class ClickCounterArrow { | |
private count = 0; | |
registerClick = () => { | |
this.count++; | |
alert(this.count); | |
} | |
} | |
// Function Wrapping at Point of Call | |
// If you want to leave your class untouched, you can wrap the call to the instance method in a function | |
// to create a closure that keeps the context alongside the function. | |
document.getElementById('target').onclick = function () { | |
clickCounter.registerClick(); | |
}; | |
// ECMAScript 5 Bind Function | |
// Another technique that leaves the original class untouched is to use JavaScript's bind function, | |
// which is available in ECMAScript 5 and higher. | |
// The bind function sets the context for the method. | |
// It can be used more generally to permanently replace the context, but here it is used to fix the context | |
// for the registerClick method to be the clickCounter instance. | |
const clickHandler = clickCounter.registerClick.bind(clickCounter); | |
document.getElementById('target').onclick = clickHandler; | |
// Event Capturing | |
// If you need to capture the event argument, the simplest way is to use an arrow function | |
// The registerClick method has been updated to take an identifier, | |
// which is obtained using the event target (or source element in older versions of Internet Explorer). | |
// This preserves the context and captures the event information in one terse statement. | |
class ClickCounterID { | |
private count = 0; | |
registerClick(id: string) { | |
this.count++; | |
alert(this.count); | |
} | |
} | |
const clickCounterID = new ClickCounterID(); | |
document.getElementById('target').onclick = (e) => { | |
const target = <Element>e.target || e.srcElement; | |
clickCounterID.registerClick(target.id); | |
}; | |
// One thing to consider when designing your program will be the number of instances of each class being created at runtime. | |
// If you are creating hundreds or thousands of instances it is more efficient for the methods to be normal instance methods, | |
// not arrow functions assigned to properties. | |
// This is because normal instance methods are defined once and used by all instances. | |
// If you use a property and arrow function, it will be duplicated on every instance. | |
// This duplication can become a big overhead when a large number of instances are created. | |
// If you want to keep a clear divide between responsibilities, follow this guideline; | |
// when you need to preserve the scope of a callback, | |
// prefer to preserve it when setting up the callback, not by adjusting the class itself. | |
// Type Information | |
// Checking types and branching off in different directions based on the type | |
// is a strong indicator that you have broken encapsulation. | |
// To test the type of a class instance, you use the instanceof operator. | |
class Display { | |
name: string = ''; | |
} | |
class Television extends Display { } | |
class HiFi { } | |
const display = new Display(); | |
const television = new Television(); | |
const hiFi = new HiFi(); | |
let isDisplay; | |
// true | |
isDisplay = display instanceof Display; | |
// true (inherits from Display) | |
isDisplay = television instanceof Display; | |
// false | |
isDisplay = hiFi instanceof Display; | |
// You can also test the presence of specific properties using the in keyword. | |
let hasName; | |
// true | |
hasName = 'name' in display; | |
// true | |
hasName = 'name' in television; | |
// false | |
hasName = 'name' in hiFi; | |
// It is important to note that due to the code generation in the TypeScript compiler, | |
// an uninitialized property will not be detected because unless the property has a value, | |
// it does not appear in the compiled JavaScript code. | |
// the hasName property will be false because, | |
// although a name property is declared, the name property is never initialized. | |
// If the name property had been assigned a value, hasName would be true. | |
// Don't forget the quotes around the property name when using the in keyword as you will need to pass a string. | |
// Without the quotes, you would be testing the value of a variable, which may not even be defined. | |
class DisplayName { | |
name: string; | |
} | |
const displayName = new DisplayName(); | |
// false | |
const hasNameDisplay = 'name' in displayName; | |
// If you want to obtain the type name at runtime, you may be tempted to use the typeof operator. | |
// Unfortunately, this will return the type name 'object' for all classes. | |
// This means you need to inspect the constructor of the instance to find the type name. | |
const tv = new Television(); | |
const radio = new HiFi(); | |
// Television | |
const tvType = tv.constructor.name; | |
// HiFi | |
const radioType = radio.constructor.name; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment