Skip to content

Latest commit

 

History

History
686 lines (524 loc) · 18.4 KB

chapter-3-notes.md

File metadata and controls

686 lines (524 loc) · 18.4 KB

Chapter 3 Notes

All About Types

type: a set of values and the things you can do with them

Examples

  • boolean
    • the set of all booleans (just true / false)
    • the operations include ||, &&, !
  • number
    • the set of all numbers
    • the operations include +, *, / etc
    • also includes the methods like toString, toFixed
  • string
    • the set of all strings
    • the operations include +, ||, &&
    • also includes the methods like toUppercase and concat

Knowing something is of type T tells you 3 things:

  1. What type it is
  2. What you can do with it
  3. What you can NOT do with it

TypeScript Type Hierarchy Tree

Talking about Types

function squareOf(n: number) {
    return n * n;
}

squareOf(2); // 4
squareOf('z'); // Error TS2345
// Argument of type '"z"' is not assignable to parameter of type 'number'.

Annotating the parameter of squareOf allows us to say the following:

  1. squareOf's parameter is "constrained" to number
  2. The type of the value 2 is assignable to (i.e. compatible with) number

The ABCs of Types

any

The type to use as a last resort. It is the set of all values

unknown

  • Similar to any in the set of values it includes.
  • But TypeScript will require checking what it is before using it unsafely
  • Must be explicitly set. TypeScript will not infer something as unknown
  • You can compare values of type unknown
  • But you cannot do things that assume an unknown value is of a certain
    type
    without first proving it to TypeScript
let a: unknown = 30; // unknown
let b = a === 123; // boolean
let c = a + 10; // Error TS2571: Object is of type `unknown`
if (typeof a === 'number') {
    let d = a + 10; // number
}

boolean

  • only 2 possible values: true & false
  • operations include those that compare them & negate them
    • === & !
let a = true; // boolean (inferred)
var b = false; // boolean (inferred)
const c = true; // true (inferred specific boolean value)
let d: boolean = true; // boolean (specifically a boolean)
let e: true = true; // true (specific boolean value)
// Error TS2322: type `false`is not assignable to type `true`
let f: true = false; // (specific boolean value)
  • type literals: a type that represents a single value and nothing else
    • example above for how e & f are set
    • using const (vs let) will tell TypeScript to set a type literal
      (see
      Type Widening for why)
    • powerful feature that adds extra safety

number

  • The set of all numbers:
    • integers, floats, positives, negatives, Infinity, NaN, etc
    • operations include +, -, *, etc
let a = 1234; // number (inferred)
var b = Infinity * 0.1; // number (inferred)
const c = 5678; // 5678 (inferred as a specific number)
let d = a < b; // boolean
let e: number = 100; // number (explicity)
let f: 26.218 = 26.218; // 26.218 (explicitly set as specific value / type literal)
let g: 26.218 = 10; // Error TS2322: Type '10' not assignable to type '26.218

bigInt

  • The set of all BigInts
  • Operations include normal number operations +, -, *, etc
let a = 1234n; // bigint
const b = 5678n; // 5678n
var c = a + b; // bigint
let d = a < 1235; // boolean
// let e = 88.5n // Error TS1353: a bigint literal must be an integer
let f: bigint = 100n; // bigint
let g: 100n = 100n; // 100n
let h: bigint = 100; // Error TS 2322: Type '100' not assignable to type `bigInt`

As with boolean & number, TypeScript will generally do the work of
inferring
the value of bigints, so strive to let it do so

string

  • The set of all strings
  • operations include concatenation and the methods like .slice() &
    .startsWith
let a = 'hello'; // string
var b = 'billy'; // string
const c = '!'; // '!' (type literal)
let d = a + ' ' + b + c; // string
let e: string = 'zoom'; // string
let f: 'john' = 'john'; // 'john'
let g: 'john' = 'zoe'; // Error TS2322: Type "zoe" is not assignable to type "john"

As with boolean & number, TypeScript will generally do the work of
inferring
the value of bigints, so strive to let it do so

symbol

  • Relatively new feature to JavaScript
  • Not much can be done with them in regard to operations
let a = Symbol('a'); // symbol
let b: symbol = Symbol('b'); // symbol
var c = a === b; // boolean
let d = a + 'x'; // Error TS2469: The '+' operator cannot be applied to type 'symbol'

Declaring w/ const and unique symbol

const e = Symbol('e'); // typeof e (inferred as unique symbol)
const f: unique symbol = Symbol('f'); // typeof f
let g: unique symbol = Symbol('f'); // Error TS1332
// A variable whose type is a 'unique symbol' type must be 'const'

let h = e === e; // boolean (unique symbol is always equal to itself)
let i = e === f; // Error TS2367: This condition will always return 'false'
// since the types 'unique symbol' and 'unique symbol' have no overlap

object

Used to specify the shape of objects

NOTE: TypeScript objects cannot tell the difference between simple
objects (declared with {}) and other more complicated ones (i.e.
new Blah).
This is because JavaScript is Structurally Typed and not Nominal Typed.

Structural Typing: a style of programming where you just care that an
object
has certain properties, and not what its name is (nominal typing)

Ways to describe objects

First Way: Declare with object
let a: object = { b: 'x' };

a.b; // Error TS2339: Property 'b' does not exist on type 'object'

Only describing the with object is not enough. You must also describe the
shape

Second Way: Let Typescript Infer
let a = { b: 'x' }; // { b: string }
a.b; // string

let b = { c: { d: 'f' } }; // { c: { d: string } }
Fourth Way: Explicitly Describe in Curly Braces
let a: { b: number } = { b: 12 }; // { b: number }

Note: Declaring the above with const will infer the type as
{ b: 12 },
but will not type it as a type literal. This is because of Type Widening

Object Literal vs. Class

The following object & class are structurally the same

let c: {
    firstName: string;
    lastName: string;
} = {
    firstName: 'john',
    lastName: 'barrowman'
};

class Person {
    constructor(public firstName: string, public lastName: string) {}
}

c = new Person('matt', 'smith'); // OK

{firstName: string; lastName: string} describes the shape of an object. The
object literal & class above both satisfy that shape.

What happens when a property is left out?

let a: { b: number };

a = {}; // Error TS2741:
// Property 'b' is missing in type '{}' but required in type '{ b: number }'

What happens when an extra property is added?

a = { b: 1, c: 2 };

// Error TS2322:
// Type '{ b: number; c: number; }' not assignable to type '{ b: number; }'
// Object literal may only specify known properties, and 'c' does not exist in type '{ b: number; }'

Definitive Assignment

A variable can be declared without being assigned a value. TypeScript will
ensure that the variable is assigned a value before being used.

let i: number;
let j = i * 3; // Error TS2454: Variable 'i' is used before being assigned
let i;
let j = i * 3; // Error TS2532: Object is possibly 'undefined'

Optional Properties

let a: {
    b: number; // a has a property 'b' that is a number
    c?: string; // a might have a property 'c' that is a string
    // a may have any number of numeric properties that are boolean
    [key: number]: boolean;
};

a = { b: 1 }; // OK
a = { b: 1, c: undefined }; // OK
a = { b: 1, c: 'd' }; // OK
a = { b: 1, 10: true }; // OK
a = { b: 1, 10: true, 20: false }; // OK
a = { 10: true }; // Error TS2741: Property 'b' is missing in type '{ 10: true }'
a = { b: 1, 33: 'red' }; // Error TS2741:
// Type 'string' is not assignable to type 'boolean'

Index Signatures

  • Used to describe the types of properties that are not known ahead of time
  • Tell TypeScript that the given object may contain more keys
  • i.e. For this object, all keys of type T must have values of type U
  • The key's type must assignable to either string or number
  • Any word can be used for the key name. Especially if the key is a
    string,
    it's best to use a word that describes the value
let airplaneSeatingAssignments: {
    [seatNumber: string]: string;
} = {
    '34D': 'Boris Cherny',
    '34E': 'Bill Gates'
};

readonly Properties

  • like const for object properties 😄
let user: { readonly firstName: string; } = { firstName: 'abby' };

user.firstName; // string
user.firstName = 'abbey with an e'; // Error TS2540: Cannot assign to 'firstName' because it is a read-only property

Avoid empty object types {}

Every type (except for null and undefined) is a subtype of {}. So avoid using whenever possible.

let danger: {}
danger = {}
danger = {x: 1}
danger = []
danger = 2

// Not ideal at all. So don't do it!

The Four ways to declare objects in Typescript

  1. Object Literal: { a: string }
  2. Empty Object Literal: {} ---Avoid---
  3. The object type.
  4. The Object type. ---Avoid---

TL;DR Use 1 or 3

Be careful to avoid the option 2 & 4. Go to extremes to avoid.

  • Use a linter to warn about it
  • complain about them in code reviews
  • print posters if needed 😆
  • Use your team's preferred tool to keep them far away from your code

Is the value an object?

Value {} object Object
{} Yes Yes Yes
['a'] Yes Yes Yes
function () {} Yes Yes Yes
new String('a') Yes Yes Yes
'a' Yes No Yes
1 Yes No Yes
Symbol('a') Yes No Yes
null No No No
undefined No No No

Type Aliases, Unions, and Intersections

Type Aliases

To declare a type alias that points to a type:

type Age = number

type Person = {
	name: string
	age: Age
}
  • Aliases are never inferred. They must be typed explicitly
  • Because Age is just an alias for number, it is also assignable to number as shown below
let age = 55

let driver: Person = {
	name: 'functionalStoic'
	age: age
}

Types cannot be declared twice

type Color = 'red'
type Color = 'blue' // Error TS2300: Duplicate identifier 'Color'

Types are block-scoped just as let & const are:

type Color = 'red'

let x = Math.random() < .5

if (x) {
	type Color = 'blue' // A new & different `Color` type. Shadows above
	let b: Color = 'blue' // no error
} else {
	let c: Color = 'red' // uses `Color` type above
}

Type aliases are useful for DRYing up code. They can also be used to increase the clarity of code by using descriptive type names in the same way that descriptive variable names can be useful.

Unions & Intersections

If you have two things, A & B:

  • The Union of them is the sum, or both. Everything in A or B, or Both
  • The Intersection is what A & B have in common

It is useful to think of them as sets, or as a Venn Diagram

![[Union_And_Intersection.png]]

Symbols used to describe these relationships:

  • | (pipe) is used to describe Union
  • & (ampersand) is used to describe Intersection
type Cat = {name: string, purrs: boolean}
type Dog = {name: string, barks: boolean, wags: boolean}
type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog

CatOrDogOrBoth tells you that an object has a name. Otherwise, it can have one or all of the remaining properties

// Cat
let a: CatOrDogOrBoth = {
	name: 'Bonkers',
	purrs: true
}

// Dog
a = {
	name: 'Domino',
	barks: true,
	wags: true
}

// Both
a = {
	name: 'Donkers',
	purrs: true,
	barks: true,
	wags: true
}

Unions are not just one member of the type, it can be any combination. My first impression would point to these being more open that I generally prefer.

CatAndDog tells you that an object has all 4 properties: name, purrs, barks, wags

let b: CatAndDog = {
	name: 'Domino',
	purrs: true,
	barks: true,
	wags: true
}

I don't expect Intersections will be very common.

Two examples of the Union type for a function

function trueOrNull(isTrue: boolean) {
	if(isTrue) {
		return 'true'
	}
	return null
}

type Returns = string | null

Another example

function(a: string, b: number) {
	return a || b
}

type Returns = string | number

Arrays

let a = [1,2,3] // number[]
var b = ['a', 'b'] // string[]
let c: string[] = ['a'] // string[]
let d = [1, 'a'] // (string | number)[]
const e = [2, 'b'] // (string | number)[]

let f = ['red'] // string[]
f.push ('blue')
f.push(true) // Error TS2345: Argument of type 'true' is not assignable to parameter of type 'string'

let g = [] // any[]
g.push(1) // number[]
g.push('red') // (string | number)[]

let h: number[] = [] // number[]
h.push(1) // number[]
h.push('red') // Error TS2345: Argument of type 'red' is not assignable to parameter of type 'number' 

Two syntaxes are supported for arrays:

  • T[] i.e. string[]
  • Array<T> i.e. Array<string>

Pro Tip: Items stored in an array should be of the same type.

I generally already do this, but not doing so in Typescript will make it difficult. For example, running map on an array of different types will force checking the type (using typeof) before performing an operation on the item.

In most cases, Typescript infers the type of an array. But if an empty array is declared, it will decide the type as the code is executed, but begin as any[]. In some cases, Typescript will not allow expanding the type further, such as when returned from a function.

All in all, I already use arrays in this way.

Tuples

  • subtypes of [[#Arrays|array]]
  • special way to type arrays that have a fixed length
  • and where values at each index have specific & known types
  • must be explicitly typed at declaration
let a: [number] = [1]

// A tuple of [first name, last name, birth year]
let b: [string, string, number] = ['malcolm', 'gladwell', 1963]

b = ['queen', 'elizabeth', 'ii', 1926] // Error TS2322: Type 'string' is not assignable to type 'number'

Tuple with optional elements:

let trainFares: [number, number?][] = [
	[3.75],
	[8.25, 7.70],
	[10.50]
]

// The above is the same as:
let moreTrainFares: ([number] | [number, number])[] = [
	// ...
]

Tuples also support rest elements:

// A list of strings with at least 1 element
let friends: [string, ...string[]] = ['Sara', 'Tali', 'Chloe', 'Claire']

// A heterogeneous list
let list: [number, boolean, ...string[]] = [1, false, 'a', 'b', 'c']

Pro Tip: Tuples should be used often

They:

  • safely encode heterogeneous lists
  • capture the length of the list they type
  • significantly more safety than a plain array

Read-only Arrays & Tuples

Typescript has the ability to create an immutable array (I'm excited about this!) with the use of readonly. This prevents updating the array in place and forces non-mutating methods like .concat & .slice instead of mutating methods like .push & .splice

let as: readonly number[] = [1, 2, 3]     // readonly number[]
let bs: readonly number[] = as.concat(4)  // readonly number[]
let three = bs[2]                         // number
as[4] = 5            // Error TS2542: Index signature in type
                     // 'readonly number[]' only permits reading.
as.push(6)           // Error TS2339: Property 'push' does not
                     // exist on type 'readonly number[]'.

As with standard [[#Arrays]], readonly arrays can use the longer-form version

type A = readonly string[]           // readonly string[]
type B = ReadonlyArray<string>       // readonly string[]
type C = Readonly<string[]>          // readonly string[]

type D = readonly [number, string]   // readonly [number, string]
type E = Readonly<[number, string]>  // readonly [number, string]

null, undefined, void, never

Typescript has types for null & undefined. Each type allows only value

  • The null type allows the value null
  • The undefined type allows the value undefined

void is the return type of a function that doesn't explicitly return anything.

  • My comment:
    • I generally strive to avoid creating functions that don't return a value due to my preference for functional programming. I doubt I'll use it often, but one example that comes to mind is that Segment Functions (Source & Destination) generally return void
Type Meaning
null Absence of a Value
undefined Variable that has not been assigned a value yet
void Function that doesn't have a return statement
never Function that never returns

Enums

Enums are a way to enumerate the possible values for a type

enum Language {
	English,
	Spanish,
	Russian
}

As a convention, both enum names and keys are both uppercase & singular

Typescript will automatically infer a number as the value for each key if not set explicitly. The above set explicitly is as follows:

const enum Language {
	English = 0,
	Spanish = 1,
	Russian = 2
}

To retrieve the value of an enum, they are accessed in the same way that objects are used:

  • dot notation
  • bracket notation
const myFirstLanguage = Language.Russian // 2
const mySecondLanguage = Language['English'] // 0

String values can also be used for enums

const enum Color {
	Red = '#c10000',
	Blue = '#007ac1',
	Pink = 0xc10050,
	White = 255
}

IMO, Don't use enum unless absolutely necessary (and forced to do so)

Exercises

  1. For each of these values, what type will TypeScript infer?
let a = 1042                  // number
let b = 'apples and oranges'  // string
const c = 'pineapples'        // string
let d = [true, true, false]   // boolean[]
let e = {type: 'ficus'}       // { type: string }
let f = [1, false]            // (number, boolean)[]
const g = [3]                 // number[]
// (try this out in your code editor,
// then jump ahead to “Type Widening” if the result surprises you!)
let h = null // null
  1. Why does each of these throw the error it does?
let i: 3 = 3
i = 4 // Error TS2322: Type '4' is not assignable to type '3'.