There are few videos on the fireship channel regarding TypeScript. Most recent on top.
Years ago most React developers would’ve laughed at using something like TypeScript
Should you use TypeScript? There is a case to be made on both sides of the argument. Take a look at two projects.
npx create-react-app myApp --template typescript
This creates a project with files ending in “.ts” or “.tsx” but the code is the same as the JS files. TS is a superset of JS, meaning JS is valid TS (same as CSS is same as SCSS). TS brings in extra optional features. Relatively minimal learning curve. Browsers don’t understand TS code so we need to compile down to regular JavaScript.
Customizing the TS Compiler
The purpose of the “tsconfig.json” file is to customize the behavior of the compiler which itself converts TS to any flavor of JS to run in the browser. This is cool cause we can use the latest and greatest in JS and the compiler works its magic to let code run on legacy browsers. The drawback is there’s a gazillion options and it can be frustrating if you don’t know which option is breaking your code.
You probably want to set “strict” to “true”, makes your code initially more difficult to write but future refactoring and working with developers much better.
The “target” option is another important one that tells the compiler what flavor of code to output. For example, “es5” is the 2009 flavor of JS.
But a tool like Babel does the same thing doesn’t it? Under the hood the “create-react-app” uses babel that does the exact same thing. Can write modern code in both types of projects.
Catching Bugs Early
Often here developers say you can catch silly bugs and development before they become a disaster in production. This is great. Keep in mind that TS does not replace test driven development (using Jest, etc). TS can detect poorly structured code in advance but can’t detect bad logic.
Type System
Type system and pros and cons of using it. Props have a type of “any” which is pretty broad type overall. React has a built in type called FC which is a function component and you can assign that type to the component. By default the only known React prop is “children.”
import {FC} from 'react'
const Cool:FC = (props) => {
return <>{ props.children }</>
}
export default Cool
We can define the shape of our own props using a TypeScript interface. An interface allows you to describe the shape of an object matching a property name to a type. Make property names optional by adding a question mark? Take the interface and pass it to the function component type by wrapping in angle brackets. This is like saying we have a function component that also includes props of its own custom shape. This gives us really useful “IntelliSense” when working with any props on this component. This also guarantees we use the required props when working with this component in JSX. However that did come at a pretty significant cost we’ve added a lot of boilerplate to the code base without adding any new functionality for the user.
interface CoolProps {
foo?: number
bar?: string
}
const Cool:FC<CoolProps> = (props) => {
return <>{ props.children }</>
}
Bypass type checking with the “any” type. Allows you to opt out of TypeScript but also negates any of its benefits so try to never use “any.”
Automatic Documentation
TypeScript can provide automatic documentation for your code. Your editor will automatically pick up the type definitions so anybody using your code will get “IntelliSense” on the shape and purpose of it which is way more efficient than having to go read some documentation online.
Type Inference
Now in many cases Typescript can automatically infer the type without you having to add any additional code to the code base. For example, if the default value for the “useState” hook is a string, TS will add a “string” type automatically. And if you try to change it to anything but a string you’ll get this pre-cog error.
Is TypeScript Worth iI?
Case to be made on both sides of the argument. It’s managed by Microsoft. Enterprise loves it. Some guy on the internet (Eric Elliot) says it only catches 15% of the bugs. Airbnb says it catches 38% of bugs. Would you rather write more code or would you rather be more confident in the code that you do right? Would you rather look at your documentation on the website or would you rather have it be automatic in your editor. Would you rather write faster code now or deal with chaotic refactoring later.
Decorators are one of the coolest features in TS that help us write more elegant code.
@component()
We use them all the time but you can create your own from scratch.
What is a decorator? A function that allows you to hook into your source code and either extend the functionality of it or annotate it with metadata. Why would you want to annotate your code with metadata? You probably don’t, maybe if you’re building a compiler.
So decorators can provide this metadata but they can also let us hook into your code and alter the behavior of it. Useful to the average app developer because it allows you to write abstractions that are clear and concise. But they are almost too good at creating abstractions, so be careful not to overuse decorators.
@Cool()
export class MyComponent {
@Rad(0) thing
@Awesome()
method(@Neat foo, @Neat bar) {}
}
There are five different things that you can decorate
The function that you implement will be dependent on the thing that you are decorating. Another words, the function arguments required to decorate a class, will be different from the arguments required to decorate a method.
Uses Angular for the demo so lost me here
Object.defineProperty() is a static method that defines a new property directly on an object, or modifies an existing property on an object, and returns the object.
// syntax is Object.defineProperty(obj, prop, descriptor)
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
Decorators are composable so they can be stacked on top of one another
An example of a getter would be a price getter that calculates a price based on a user’s input. You are not allowed to pass arguments to a getter so you could create a decorator to extend the functionality so it can also calculate a variable tax value with this method.
@WithTax(0.15)
get price() {
return 10.50 + 1.25 * this.toppings.length
}
See the decorators code:
function WithTax(rate: number) {
return function(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.get
descriptor.get = function() {
const result = original.apply(this)
return (result * (1 + rate)).toFixed(2)
}
return descriptor
}
}
Implementing React Hooks in Angular. Insert sarcastic joke here as hooks aren’t necessary in Angular.
A hook is basically a function that returns a getter and a setter. If you update the value with the setter then it will update the UI automatically. Angular 2 has been able to do this from the very beginning.
Which is better OOP or Functional, Composition or Inheritance?
There is always more than one way to solve a problem, especially in JS. Great to debate but no absolutes.
The most important concept in functional programming is writing “pure” functions, meaning the outcome should only depend on it’s inputs. The should not produce any “side effects” or rely on any outside value to produce a return value. These function are easier to test and easier to reason about since you don’t have to worry about anything outside of the function itself.
function toString(val) {
return val.toString()
}
Another core principle is immutable data. Functional code is stateless mning when data is created it is never mutated. Don’t use “Array.push()” in a function program.
const data = Object.freeze([1, 2, 3, 4, 5, 6])
But obviously need data to change somehow, so you will often pass functions as arguments to other functions.
A first-order function is one that takes in a value and returns a different value.
const addEmoji = (val) => toString(value) + ' emoji'
A higher-order function is one that either takes a function as an argument or returns a function itself. JS has a lot of built-in higher order functions for arrays such as map. The “map” function doesn’t mutate but transforms values in an array. Also see things like “.concat(), .slice(), and .filter()”
const emojiData = data.map(addEmoji)
We can also create functions that return functions. Useful when you want to start with some base functionality and then extend it with some dynamic data. End result is concise and readable code that doesn’t rely on any shared state that would make it difficult to test.
const appendEmoji = (fixed) => (dynamic) => fixed + dynamic
const rain = appendEmoji('cloudEmojiHere')
const sun = appendEmoji('sunEmojiHere')
console.log( rain(' today') )
Read this post here by Malte Skarupke that talks about functional programming to bake a cake. I liked this line, “Imperative languages have this huge benefit of having implicit state. Both humans and machines are really good at implicit state attached to time.” This made me remember that React is “declarative” and not “imperative” and we do not mutate state in React.
Object Oriented TS Code
A class by itself doesn’t do anything but defines a blueprint for instantiating objects. The “constructor” method is special because it runs once when the object is instantiated.
class Emoji {
icon: string
constructor(icon) {
this.icon = icon
}
}
const sun = new Emoji('sunEmoji')
In TS there is an easier way to do this because we have the concept of “public” and “private” members. If we use the “public” keyword in front of an argument in a constructor, TS will automatically know to set the property as public on each object, (without the ” this.icon = icon ” syntax). When you declare a property or method public, it means it is available to the class itself and any instances of the class.
class Emoji {
constructor(public icon) {}
}
const sun = new Emoji('sunEmoji')
sun.icon = "poopEmoji"
console.log(sun) // Emoji {icon: 'poopEmoji'}
This strategy can be good and bad. In the code above we change the “icon” property by mutating its value on the “sun” object. This can be convenient but if you have a lot of code doing this, it can be hard to keep track of and test this code.
Classes in JS are syntactic sugar for functions and prototype inheritance. If we compile the code above down to ES3, you will see that it is just function with a closure that prevents the local variables from bleeding out into the global scope.
// compiled into closure
var Emoji = (function() {
function Emoji(value) {
this.value = value
}
return Emoji
}())
// IIFE syntax: IIFE stands for immediately invoked function expression
TS also provides tools for us to improve the tooling that we have when writing object oriented code. For example, we can mark members as private so they can only be used inside of this class definition. This means that we can separate our public API from internal logic for this class. For example, if we want to make this icon value immutable, we can make it “private,” and define a getter can so the user can read the value but not change the value. So in the code below you cannot do what we did two code blocks earlier, where we changed the “sun’s icon” value to a “poopEmoji.”
class Emoji {
constructor(private _icon) {}
get icon() {
return this._icon
}
}
const sun = new Emoji('sunEmoji')
sun.icon = "poopEmoji" // throws an error now
Class instances can have their own internal state
Let’s pretend we have a button where the user can toggle the emoji to go back and forth between different states. This is simple to implement in OOP. We create another private property called “previous” and we use a getter to retrieve that value. Then we define a “change” method that will mutates the actual “icon” value on this instance. When that happens we will change the “previous” value to the current “icon” and then update the current “icon” to a new value. The end result is a class that has encapsulated all the logic for how an “emoji” should work. And with TS we automatically have an interface and documentation for this class.
class Emoji {
private _prev
constructor(private _icon) {}
get icon() {
return this._icon
}
get prev() {
return this._prev
}
change(val) {
this._prev = this._icon
this._icon = val
}
}
const emoji = new Emoji('sunEmoji')
console.log(emoji.icon, emoji.prev) // result is the sun emoji and undefined
emoji.change('lightningBoltEmoji')
emoji.change('monkeyEmoji')
console.log(emoji.icon, emoji.prev) // result is the two emojis bolt and monkey
Another cool things you can do with classes is define static methods. The neat thing about static methods is that it is on the class itself, and not an instance of a class. You can simply use the classes’ namespace, in the example the “Emoji” class, to run the function.
class Emoji {
static addOneTo(val) {
return val + 1
}
}
Emoji.addOneTo(3) // result is 4
Composition vs Inheritance for Code Reusability
People get very strong opinions here. The actual definition of composition tends to be a little convoluted.
With inheritance you start with a larger base class and then have child classes inherit all of this functionality and override and extend it with the custom behaviors needed.
Composition breaks apart the interfaces and the logic into smaller pieces and then builds up larger functions or objects by combining these pieces together.
Example with Inheritance
Create a class of “Human” with a “public” property of “name” and the ability to say “hi” and leverage this class to create a “regularHuman” character (object). Use inheritance to create another “SuperHuman” class that extends the “Human” class.
We want the object created by “SuperHuman” to have access to a “superpower” method that the regular human does not. We also want the “SuperHuman” to be able to call all of the methods defined in the parent “Human” class.
In TS we simply inherit all the functionality of the “Human” class with the “extends” keyword. We do have an argument in the constructor so we also need to add that to the constructor without the “public” or “private” keywords. Also call “super” which will execute the code in the constructor of the parent class.
class Human {
constructor(public name) {}
sayHi() {
return "Hello, " + this.name"
}
}
const patrick = new Human('Patrick Mullot')
console.log( patrick.sayHi() ) // Hello, Patrick Mullot
class SuperHuman extends Human {
heroName
constructor(name) {
super(name)
this.heroName = `HERO ${name}`
}
superpower() {
return `${this.heroName} spits "fireEmojiHere"`
}
}
const spiderMan = new SuperHuman('Spiderman')
Inheritance can be great in the right situation but you want to avoid creating really deeply nested classes because it becomes very hard to debug when things go wrong somewhere in the middle.
With Composition
Multiple different ways to apply this pattern.
Note: In Angular we can do it by injecting services into our components. Also at the template level by creating directives. One of the awesome things about Angular that is covered in other videos.
Another alternative is to concatenate objects together. The idea is that you decouple your properties or behaviors into objects or functions that return objects. We can then merge all of the objects together into a final function that does everything that we need it to.
Often referred to as a “mixin” pattern and it is just a certain type of multiple inheritance. So the terminology between composition and inheritance is sort of convoluted. In any case this “mixin” pattern can be very powerful, but in its current form below we lose all of the ergonomics of class-based OOP. That might be a good or bad thing depending on who you ask, but TS gives us the flexibility to use “mixins” in a class-based format.
const hasName = (name) => {
return { name }
}
const canSayHi(name) {
return {
sayHi: () => `Hello, ${name}`
}
}
const Person = function(name) {
return {
...hasName(name),
...canSayHi(name)
}
}
const person = Person('Jeff')
console.log(person.sayHi()) // Hello, Jeff
An example of TS allowing us to use “mixins” in a class-based format starts with this really ugly function from the TS docs which we will use after creating some classes.
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name]
})
})
}
From here we create a couple of small behavior classes that define the individual behaviors instead of trying to encapsulate everything in a single class. So these classes are more concerned with what something “does” instead of what something “is.”
In the “SuperHero” class there is a very subtle difference from the previous example in that, instead of “extending” the class we use the “implements” keyword to implement multiple classes. When you implement something you are only concerned about its interface and not its underlying code.
It is the “applyMixins” function in the code block above that will actually take these interfaces and apply their code to this class. This leaves us with some extra boilerplate code where we actually have to type the return values on the methods for this class. In this case we have two methods, (“sayHi” and “superpower”), both of which return strings.
And the final step is the requirement to call that “applyMixins” function with the base class as the first argument and the mixed-in-classes as the second argument.
class CanSayHi {
name
sayHi() {
return ("Hello, " + this.name) }
}
class HasSuperPower {
heroName
superpower() {
return ("Shoot some fireballs, " + this.name)
}
}
class SuperHero implements CanSayHi, HasSuperPower{
heroName
constructor(public name) {
this.heroName = `SUPER ${name}`
}
sayHi: () => string
superpower: () => string
}
applyMixins(SuperHero, [canSayHi, HasSuperPower])
const ts = new SuperHero('TypeScript')
console.log(ts.superpower()) // results is: SUPER TypeScript
Now we can answer the “is a hotdog a sandwich” question. If you use inheritance then you are going to have to inherit from some base “Sandwich” class and so the answer is yes. If you use composition, you can just pull in the hotdog and a bun meaning the answer is no.
Jeff’s tool with biggest productivity gain was TS
Basaret Ali Syed is the author of the TS deep dive book that is good for advanced concepts free and open-source.
Writing more code now pays dividends later.
Tooling in IDE. Auto-documented-code. Also, compiler catches bugs in advance. “Would you rather have silly errors in development or insanity-inducing errors in production.”
Minimal learning since it is a subset of JS.
Can write code using the latest features JS.
Install TS globally with NPM (running 3.1 at the time of video)
Went through some compiler options, (target, watch, lib).
See intellisense and tooling in VSCode. Hover over class to get integrated docs and error messages. Right click and click typings to see detail.
A lot of packages ship with declarations automatically. Some 3rd party libraries like “lodash” do not ship with type declarations automatically so you need to install an @types package.
implicit versus explicitly defined typings.
Opt out with the “any” keyword.
Type inference
Create your own types from scratch using the “type” keyword
Enforce the shape of an object with an interface. Can allow any additional properties by adding a “[key: string]: any” key/value.
Functions can be a little more complex because you have parameters and a return value
function pow(x:number, y:number): string {
return Math.pow(x, y).toString()
}
“void” type for functions that do not return a value or create some sort of side effect, (event listeners).
Types for arrays
TS opens the door to a new data structure called Tuples seen in other programming language. fixed length array.
type MyList = [number?, string?, boolean]
Question mark syntax to make optional
Generics to specify type at some later point in our code. Typically use generics more than create them but good to know how to create.