TypeScript Section 2 – Basic Types


TypeScript

Updated Aug 26th, 2022

Using Types

TS provides many “types” and allows you to write your own “types.”

function add(n1: number, n2: number) {
  return n1 + n2
}

const number1 = 23
const number2 = 2.8

const result = add(number1, number2)

// if number1 is "5" we get an error

Note: if you have an “app.ts” file and an “app.js” file you may get a VSCode warning because of duplicate function names.

TypeScript Types Versus JavaScript Types

The “typeof” operator is the regular way to check types in JavaScript but this has downsides. JavaScript uses “dynamic types” that are resolved at runtime. TS has “static types” and the values are set during development and do not change. Better to get the error during development.

function add(n1: number, n2: number) {

  //
  // The non-TS way
  //
  // if (typeof n1 !== "number" || typeof n2 !== "number") {
  //   throw new Error("Incorrect Output.)
  // }

  //

  return n1 + n2
}

const number1 = 23
const number2 = 2.8

const result = add(number1, number2)

Note: The core primitive “types” in TypeScript are all lowercase.

Working with Numbers, Strings, and Booleans

Core Types: number, string, boolean, object, array, tuple, enum, any

Numbers: In JS and TS there is no differentiation between integers or floats, (5.0 is the same as 5).

Strings: All text values in single quotes, double quotes, or backticks. If you try to use a “.map()” function on a string you will get an error.

Booleans: Produces either true or false, In TS there are no “truthy” or “falsy” values.

Don’t want to directly mathematically calculate numbers with strings because they will likely convert the numbers to the string. Instead store in a variable and reference the variable.

Type Assignment and Type Inference

Type inference means TS will guess what type we want. This is popular when we set a const. You want to let TS infer if given the choice.

TypeScript’s main job is to check types and yell at us when there’s an error. When files have errors you will see this in VSCode, with the filename highlighted in red and the number of errors to the right.

Object Types

Any JS object, more specific types (type of object) are possible.

const person = {
  name: "Max"
  age: 30
}

console.log(person.nickname) // TS throws error
const person: {
  name: string;
  age: number;
} = {
  name: "Max"
  age: 30
}

console.log(person.nickname) // TS throws error

Object types have key/type pairs instead of key value pairs. Typically better to let TS infer objects instead of explicitly setting a “key/type” object. This syntax is a TS representation that will be stripped out at build time.

If you’re looking inside an object for a property that isn’t there you’ll get an error. When defining an object we can put empty brackets instead of object keyword, or better yet put “key/type” pairs in this object. If you define an object and then add another property it will not make it through compilation as the property was not part of the original declaration.

Nested Objects and Types

Of course object types can also be created for nested objects.

Array Types

Any JS array, type can be flexible or strict, (regarding the element types). An example, “string[]” means an “array of strings.” The “any” keyword can help you have strings and numbers in an array, (mixed array).

We can explicitly set the type of a variable.

let favoriteActivities: string[]

Working with Tuples

Tuples are added by TS, fixed-length array and also a fixed-type array.

const person = {
  role: [2, "author"]
}

But this has some limitations. Let’s say we want this first element in the array to only be one of three numbers. And you only want two elements. A tuple can help you prevent adding a third element onto the end of this array or, say, changing the string to a number when you definitely need it to be a string. To tell TypeScript it’s a tuple you would put inside of an array the type declarations, [number, array]. This will compile down to a normal JavaScript array. the “push” method is an exception to the tuple so TypeScript will not prevent this.

const person: {
  name: string;
  age: number;
  hobbies: string[]
  role: [number, string]; // denotes tuple
} = {
  role: [2, "author"]
}

// person.role[1] = 10 throws an error since it needs to be a string
// .push() is an exception that is allowed with tuples 
// person.role.push("admin") doesn't throw an error, bypassing the number of elements error

Working with Enums

Added by TS, automatically enumerated global constant identifiers. Issues with non-human readable identifiers, instead of just 0 1 2, is that we forget what they signify and may add a 4 even though that is not supposed to be an option.

This style does work great for “if checks” because it’s simple. If we use human-readable labels, strings of text, now we have to remember in our “if check” the exact spelling instead of a simple number.

So the common pattern in JavaScript is the following:

const ADMIN = 0
const READ_ONLY = 1
const AUTHOR = 2

The downside of this pattern is that you could still add in a number you don’t want, and you have to define and manage all the constant variables.

The enum type makes this easier, as behind the scenes it sets these values for us. Can add an equal sign to change the starting value. Can also use your own numbers.

enum Role {ADMIN, READ_ONLY, AUTHOR}

You are not restricted to default values, you can set your own values as well.

enum Role {ADMIN = 5, READ_ONLY, AUTHOR}

This would map to 5, 6, and 7. You are not restricted to number values, and you can mitch and match with strings as well.

enum Role {ADMIN = "ADMIN", READ_ONLY = 100, AUTHOR = "AUTHOR"}

Note: Role is a custom-type, our first custom-type in the course.

Also Note: Often, you’ll see the names of “enums” with all-uppercase values but that’s not a must-do and you can go with any value names.

The “any” Type

The “any” type is the most flexible type and doesn’t tell TS anything. It is really flexible but avoid “any” wherever possible as it takes away all advantages of TS. Vanilla JS has “any” on everything. Use “any” as a fallback if you have some data where you really don’t know and you’re using runtime checks with “typeof” instead.

Note: Beyond the core types, other types include: Union, Literal, Custom, Function, “unknown”, and “never”

Union Types

Allow numbers and strings. Uses pipe symbol and can add as many types as you need.

function combine(input1: number, input2: number) {
  const result = input1 + input2
  return result
}

const combinedAges = combine(20,26)

console.log(combinedAges) // result is 56

const combinedAges = combine("Max", "Anna")
// throws an error but you could bring in Union type

So we can use the pipe operator to allow both numbers or string

function combine(input1: number | string, input2: number | string) {
  const result = input1 + input2
  return result
}

const combinedAges = combine(20,26)

console.log(combinedAges)

const combinedAges = combine("Max", "Anna")
// still throws an error so we need to refactor
// error related to TS seeing the union type and... 
// ...thinking it may not be able to use the plus sign

But you may need runtime type check (using typeof keyword or similar) and have additional logic in your function to have different behavior depending on the type of arguments you are getting.

function combine(input1: number | string, input2: number | string) {

  let result

  if (typeof input1 === "number" && typeof input2 === "number") {
    result = input1 + input2
  } else {
    result = input1.toString() + input2.toString()
  }
  return result
}

const combinedAges = combine(20,26)

console.log(combinedAges)

const combinedAges = combine("Max", "Anna")

Literal Types

Don’t just say the type but the exact value it should hold, and for strings this can be very useful. Example pass a 3rd parameter named “resultConversion: string” and when calling the function pass ‘as-number” or “as-text” as a third argument. Could now force a conversion based on the argument passed by bulking up the conditional logic.

function combine(
  input1: number | string,
  input2: number | string,
  resultConversion: "as-number" | "as-text" // get error if don't provide one of these two values
) {

  let result

  if (typeof input1 === "number && typeof input2 === "number || resultConversion === "as-number") {
    result = +input1 + +input2
  } else {
    result = input1.toString() + input2.toString()
  }

  return result
}

const combinedAges = combine(30, 26, "as-number")

console.log(combinedAges)

const combinedAges = combine("Max", "as-text")

The downside of literal types is that we have to memorize or remember these values.

Helps with typos.

Type Aliases / Custom Types:

Cumbersome to always repeat a “union type” over and over so define your own custom type with “alias” using the “type” keyword. You can also provide an “alias” to declare, say, a complex object type.

// above the combine code above

type Combinable = number | string
type ConversionDescriptor = "as-number" | "as-text"

// and then reference anywhere in your code

function combine(
  input1: Combinable,
  input2: Combinable,
  resultConversion: ConversionDescriptor
) {...}

Type Aliases & Object Types

Type aliases can be used to “create” your own types. You’re not limited to storing union types though – you can also provide an alias to a (possibly complex) object type.

type User = { name: string; age: number };
const u1: User = { name: 'Max', age: 30 }; // this works!

This allows you to avoid unnecessary repetition and manage types centrally.

For example, you can simplify this code:

function greet(user: { name: string; age: number }) {
  console.log('Hi, I am ' + user.name);
}
 
function isOlder(user: { name: string; age: number }, checkAge: number) {
  return checkAge > user.age;
}

To:

type User = { name: string; age: number };
 
function greet(user: User) {
  console.log('Hi, I am ' + user.name);
}
 
function isOlder(user: User, checkAge: number) {
  return checkAge > user.age;
}

Function Return Types & “void”

We’ve seen we can assign “types” to function parameters. But it also has a “return” type, that TS infers but you can explicitly set as well by adding a semicolon after the parameter list. It is a good idea to let TS inference do its thing, unless there is a specific reason.

You will notice that sometimes TS infers “void” as the “return” type. This “void” means the function does not have a “return” statement and does not “return” a value.

Note that “undefined” is an actual value and different than “void.” In TS, “undefined” is actually a type, although will be rarely used.

Note: The “void” type doesn’t force you to return anything if you don’t want to return something.

function printResult(num: number): void {
  console.log("Result " + num)
}

Functions as Types

Set type to “Function” but can take it a step further by describing parameters and “return” values. Accomplish this by dropping the “Function” keyword and using syntax similar to arrow function notation. You don’t use curly brackets, just the return type to the right of the arrow.

function add(n1: number, n2: number) {
  return n1 + n2
}

function printResult(num: number): void {
  console.log("Result " + num)
}

printResult(add(5, 12)) // output is 17

// let combineValues: Function;
let combineValues: (a: number, b:number) => number

combineValues = add
combineValues = printResult
//combineValues = 5 throws an error now

console.log(combineValues(8, 8)) // output is 16

// let someValue: undefined

Reminder: you can store a pointer to a function in a variable and execute that function through that variable. This is normal JS.

function add(n1: number, n2: number) {
  return n1 + n2
}

let combineValues
combineValues = add
console.log(combineValues(8, 8)) // outputs 16

Functions Types and Callbacks

They work pretty much the same way

function addAndHandle(n1: number, n2: number, cb) {
  const result = n1 + n2
  cb(result)
}

But makes sure to be clear that the callback is a function and is expecting a number as an argument.

function addAndHandle(n1: number, n2: number, cb: (num: number) => void) {
  const result = n1 + n2
  cb(result)
}


// call the function

addAndHandle(10, 20, (result) => {
  console.log(result)
})

Note: Seeing a function type definition passed as an argument. Not sure if this is a thing.

Callback functions can return something, even if the argument on which they’re passed does NOT expect a returned value.

The “unknown” Type

We don’t know what the user will eventually enter, we can store any value without getting any compilation errors. It is important to understand, “unknown” is different than “any.” This means if you are trying to set a variable defined as a “string” type, with something defined with “unknown,” we will get an error because it may not be “string” and we will need to add an extra check.

The “unknown” type is a bit more restrictive, and hence better, than the “any” type.

The “unknown” type is a niche type, and shouldn’t really be used.

let userInput: unknown
let username: string

userInput = 5
userInput = 'Max'

if (typeof userInput === "string") {
  userName = userInput
}

The “never” Type

The “never” type is a niche type, intended to never return anything, never produce a return value.

function generateError (message: string, code: number): never {
  throw {message: message, errorCode: code}
}

generateError('An error occurred', 500)

This code would likely be in a “try/catch” block since it stops execution, crashes or breaks script.