Skip to content

Typescript: (2) Creating Basic Types

Mithi Sevilla edited this page Mar 30, 2021 · 1 revision

Array

const numbers:  Array<number>  =  []
const item:  string[]  =  []

function logScores(firstName:  string,  ...scores: number[]){
  console.log(firstName, scores)
}

logScores("Mike",  90,  60,  65)
// logScores("Mike", 90, 60, "65")
// Argument of type 'string' is not assignable to parameter of type 'number'.

Tuple

// you have to annotate this
const tomScore: [string, number] = ["tom", 70]
// because if you don't, it will infer it to be
// of type (string | number)[]

Open ended tuple - items have some structure but the number of items isn't fixed

const benScores: [string, ...number[]] = ["ben", 50, 75, 85]

Objects

const tomScore = { name: "tom", score: 70 }
// type will be infered as {name: string; score: number}

tomScore.score = 75 // this will not trigger an error

tomScore.passed = true // will trigger an error
// because passed doesnt exist on the inferred type

Explicit object type annotations

const tomScore: {name: string; score: number; } = {
  name: "Tom",
  score: 70
}

Type Aliases

type Score = {name: string; score: number;}
const tomScore: Score = { name: "Tom", score: 70 }
const bobScore: Score = { name: "Bob", score: 80 }

Interfaces

// onClick is optional
interface ButtonProps {
  text: string;
  onClick?: () => void;
}

const BuyButton: ButtonProps = {
  text: "Buy",
  onClick: () => console.log("Buy")
}

Make things read-only

interface ButtonProps {
  readonly text: string;
}

const buyButton: ButtonProps = {
  text: "Buy"
}

BuyButton.text = "Sell" // ERROR

interface Result {
  readonly name: string;
  readonly score: number[];
}

let billScores: Result = {
  name: "Bill",
  scores: [90, 65, 80
}

// Important! This will NOT trigger an ERROR
billScores.scores.push(70)

interface ImmutableResult {
  readonly name: string;
  readonly scores: readonly number[];
}

let tomScores: ImmutableResult = {
  name: "Tom",
  scores: [50, 95, 80]
}

tomScores.scores.push(70) // Will trigger an ERROR!

Extending interfaces

interface ColoredButtonProps extends ButtonProps {
  color: string;
}

Interfaces vs type aliases types and interfaces are similar and it is generally personal preferences as to which approach to use when creating types. before there where more differences, but now they're very similar.

Union Types

let age: number | null |undefined
let possibleFruits: "banana" | "apple" | "pear"

// might be useful for react reducers
type Actions = { type: "loading" } | { type: "loaded", data: { name: string } }

Intersection types

type Name = {
  firstName: string;
  lastName: string;
}

type PhoneNumber = {
  landline: string;
  mobile: string;
}

type Contact = Name & PhoneNumber

const fred: Contact {
  firstName: "Fred",
  lastName: "Smith",
  landline: "9425556"
  mobile: "091780513337"
}

type Email = { emailAddress: string }
type ExtendedContact = Name & PhoneNumber & Email

The code below will error

// IMPORTANT: This will create an error!
type BaseElement = {
  name: string
  kind: "text" | "number" | "email"
}

type TextInput = { kind: "text" }

type Field = BaseElement & TextInput

const age: Field = {
  name: "Age",
  kind: "number"
}
// Type '"number"' is not assignable to type '"text"'.
// when you intersect BaseElement & TextInput
// BaseElement has kind: "text" | "number" | "email"
// TextInput has kind: "text"
// THE COMMON MEMBER OF AN INTERSECTION TYPE 
// IS MATHEMATICALLY INTERSECTED

Another example

type A = {
  doIt: (a: string) => void
}

type B = {
  doIt: (a: string, b: string) => void
}

type A_and_B = A & B 
// can also be `B & A` it doesn't matter

// this will NOT error
const ab1: A_and_B = {
  doIt: (a: string ) => {} 
}

// this will error 
const ab2: A_and_B = {
  doIt: (a:string, b: string) => {}
}

interfaces can be extended, but type aliases can also extend existing types using the intersection operator

Type Compatibility

let firstName:  string  =  "fred"
let age =  30
firstName = age 
// ERROR: Type 'number' is not assignable to type 'string'.

const jones:  "Tom"  |  "Bob"  =  "Tom"
let jane:  string  =  "jane"
jane = jones // no error

Type names are not important It's the structure that is important

type Person = { name: string }
interface IPerson { name: string }

const bob: Person = { name: "bob" }
const fred: IPerson = { name: "fred" }
let person = bob;
person = fred // Okay
let bob = { name:  "bob" }
let oldFred = { name: "fred", age: 50 }
let person = bob
person = oldFred
// no error, even if oldFred has age, and person does not have that property

But this will error

let bob =  { name:  "bob"  }
let oldFred =  { name:  "fred", age:  50  }
let person = bob
person = oldFred
oldFred = bob // ERROR
// Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'.

Typescript uses structural typing which means variables with different types can be assigned to one another if the types are compatible

  • var a can be assigned to var b if b type is wider than a type
  • object a can be assigned to object b of a has at least the same members as b, (a can have properties than b don't have a can still be assigned to b)
  • function a can be assigned to b if each parameter in a has a corresponding parameter in b with a compatible type
let multiply = (a: number, b: number, c: number): number => a * b * c
let subtract = (x: number, y: number ) => x - y

// subtract = multiply  
// Type '(a: number, b: number, c: number) => number' 
// is not assignable to type '(x: number, y: number) => number'.

multiply = subtract // no error (`subtract` has less parameters than `multiply`)

IMPORTANT! Because a function with two parameters fits into a function with three parameters. If function parameters are a subset of the parameters of another function, it can be assigned to it WHY??

Clone this wiki locally