Skip to main content

TypeScript Essentials

What is a type?

A type is a set of possible values. For example, string type represents an infinite set of possible strings

  • & is an intersection (phép giao), e.g. false & true = never
  • | is a union (phép hợp), e.g. true | never = true
  • never is an empty set of values. never & T always results in never. never | T always results in T.
  • any is the set of all JS values as it ignore TS. unknown is the same (set of all JS values) but it's safe to use

Function

// Function Type expression
// Cách 1: Truyền trực tiếp
const greeter = (a: string): void => {
console.log(a)
}
function greeter1(a: string) {
console.log(a) // return type dc infer là void
}
// Cách 2: Khai báo qua `type`
type GreetFunction = (a: string, b?: number) => void
const greeter2: GreetFunction = (a) => {
console.log(a)
}

// Utility Types
type GreeterParams = Parameters<typeof greeter2>
type GreeterReturn = ReturnType<GreetFunction>

Index Signature

interface JD {
// An index signature property type must be either ‘string’ or ‘number’
[index: string]: any //* JD can have any number of properties with `any` type
1: string // `typeof jd[1] = string`
}

Use Interface until You Need Type

  • interface is more flexible as it allows adding new props to an existing interface.
  • Performance of intersections (Extended interface or type) in interface - & is higher than type - extends
type Identity = {
name: string
}
type Identity = {
age: number // ❌ Error: Duplicate identifier 'Identity'
}

interface Contact {
email: string
}
interface Contact {
phone: string // ✅: Có thể extend Interface
}

type Customer = Identity & Contact & { gender: string } // `type` can also be extended with `interfaces`...
interface Customer2 extends Identity, Contact {
gender: string // ...and vice versa
}

Keywords

keyof & Mapped Types (looping through object keys)

interface Person {
name: string
age: number
}

type PersonKey = keyof Person // "name" | "age"

type OptionsFlags<Type> = {
// Can understand `P` as "property" to remember
[P in keyof Type]: boolean
}
type FeatureFlags = {
f1: () => void
f2: () => void
}
type FeatureOptions = OptionsFlags<FeatureFlags> // f1: boolean, f2: boolean

// This removes all `readonly`` attributes since we used the `-` sign. If you removed the `-`, all fields would be read-only.
type Mutable<Type> = {
-readonly [P in keyof Type]: Type[P]
}

as (Type Assertion)

Assert the type-system that e can be assigned to Error. Only be used to more or less specific type (ko thể có ~ chuyển đổi vô lý như "hello" as number)

try {
failed && throw new Error('Failure!')
doSomething()
} catch (e) {
console.error((e as Error).message)
}

as const (Const Assertion)

  • No literal types in that expression should be widened (e.g. no going from "hello" to string)
  • Object literals get readonly properties (no assign to object props)
  • Array literals get readonly tuples (no assign to array value)
interface Home {
name: string
}
const noMutateHome = { name: 'home' } as const
noMutateHome.name = 'bro' // ❌: Cannot assign to 'home' because it is a read-only property.

in & is

  • in: Usage is the same with JS object
  • is: Type predicates, in this case, it is pet is Fish
type Fish = { swim: () => void }
type Bird = { fly: () => void }

// `type Fish | Bird` is called "Union Types"
function move(animal: Fish | Bird) {
'swim' in animal ? animal.swim() : animal.fly()
}

// Any time isFish is called with some variable, TS will narrow that variable
// to that specific type if the original type is compatible.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined
}

declare function getSmallPet(): Fish | Bird

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet()
if (isFish(pet)) {
pet.swim()
} else {
// TS knows that here pet must be `Bird` because it's not `Fish`
pet.fly()
}

Types

Tuple

Superset of Array - Knows exactly how many elements it contains, and exactly which types it contains at specific positions.

let tuple1: [string, number, number?] = ['John', 25] // tuple1.length = 2 | 3
let tupleArray: [string, number][] = [
['John', 25],
['Uyen', 23],
]
var tuple2: [string, ...boolean[], number] = ['John', true, false, 1] //* Rest elements must be array type

unknown

// `unknown`: Similar to `any`, but safer because u can't do anything with it
let anyFoo: any = 10
let unknownBar: unknown = 10 //* We can assign anything to unknown just like any
const upperCase = (x: string) => console.log(x.toUpperCase())

upperCase(anyFoo) // Ok, `any` can assign to anything
upperCase(unknownBar) //! Invalid; we can't assign `unknown` to any other type...
typeOf unknownBar === 'string' && upperCase(unknownBar) // ... We must do it with a type check
upperCase(unknownBar as string) //* Careful: Assert là string nên type-system ko báo lỗi, lúc run mới ra lỗi TypeError: number ko có method `toUpperCase`

anyFoo.method() //* Ok; anything goes with `any`
unknownBar.method() //! Not ok; we don't know anything about this variable

Enums

//2 Number enum
enum Status {
Pending, // 0
Approved, // 1
Rejected = 10,
}
console.log(Status.Rejected) // 10
console.log(Status[10]) // `Reverse mapping` -> Rejected

//2 String enum: KHÔNG REVERSE MAPPING như Number Enum dc
enum Status2 {
Pending = 'Đang chờ',
Approved = 'Thành công',
}
type ResponseTypes = keyof typeof Status2 // "Pending" | "Approved"
const ss = Status2.Pending // "Đang chờ"
const bb = Status2[0] // undefined

Utility Types

Record<Keys, Type>

An object type whose property keys are Keys and whose property values are Type

type GenericObject = Record<string, any>

type CatName = 'miffy' | 'boris' | 'mordred'
interface CatInfo {
age: number
breed: string
}
const cats: Record<CatName, CatInfo> = {
miffy: { age: 10, breed: 'Persian' },
boris: { age: 5, breed: 'Maine Coon' },
mordred: { age: 16, breed: 'British Shorthair' },
}

Promise & Awaited

interface Album {
id: number
userId: number
title: string
}
const fetchAlbum = async (): Promise<Album[]> => {
const data = await fetch('https://jsonplaceholder.typicode.com/albums').then(
(res) => res.json(),
)
return data
}

type ReturnedAlbums = Awaited<ReturnType<typeof fetchAlbum>> // Album[]

Omit & Pick

interface User {
id: string
firstName: string
lastName: string
}
type MyType = Pick<User, 'firstName' | 'lastName'>

Partial & Required

type User = {
id: number
name: string
email?: string
}

type PartialUser = Partial<User> // all properties are optional
type RequiredUser = Required<User> // all properties are required

const partialUser: PartialUser = {
id: 1,
}

const requiredUser: RequiredUser = {
id: 2,
name: 'John',
email: 'john@example.com',
}

Exclude

type T2 = Exclude<string | number | (() => void), Function> // string | number

type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; x: number }
| { kind: 'triangle'; x: number; y: number }

type T3 = Exclude<Shape, { kind: 'circle' }> // only square and triangle left

Generics

Generic Constraints

// `A extends B` means A is a subset of B, and more specific than `B`.
// Ràng buộc `T` phải ÍT NHẤT có `length` property
function minLength<T extends { length: number }>(obj: T, minimum: number): T {
if (obj.length >= minimum) {
return obj
}
return { length: minimum } // ❌: `T` ko nhất thiết phải là obj mà có thể là array hoặc string (đều có `length` property)
}

Default Generic value

type F1<T = string> = (a: T) => any
const Fn1: F1 = (a) => a.split(' ') // Ko khai báo thì default là `string` -> Dùng `split` dc

Multi Generics Function

Các Generic phải relate với nhau. Ex: Relate giữa input & output, hay giữa các input với nhau

function map<Input, Output>(
arr: Input[],
func: (arg: Input) => Output,
): Output[] {
return arr.map(func)
}
const parsed = map(['1', '2', '3'], (n) => parseInt(n)) // Infer `Input` = string; `Output` = number

Type Documentation

export type ProductVariant = {
/**
* The product variant’s price.
* @deprecated Use `priceV2` instead
*/
price: Scalars['Money']
/** The product variant’s price. */
priceV2: MoneyV2
}

Conditional Types

type Id = number
type Name = string
type NameOrId<T extends number | string> = T extends number ? Id : Name

How to check Type of Data

typeof

Works fine with number, string, boolean, undefined, function, symbol. Pitfalls:

typeof null // "object"

typeof [] // "object"
typeof {} // "object"
typeof new Date() // "object"
...

instanceof

In JS, the instanceof operator checks for the constructor of an object. In other words, it tests which class created a given value. Because of this, it can correctly determine types for objects, but NOT for primitive types. In TS, it can be also be used for narrowing types.

// JS
class Animal {}
class Dog extends Animal {}
const myDog = new Dog()
myDog instanceof Dog // true
;[] instanceof Array // ✅ true
;(() => {}) instanceof Function // ✅ true
new Map() instanceof Map // ✅ true

1 instanceof Number // ❌ false
'foo' instanceof String // ❌ false

// TS
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()) // x: Date
} else {
console.log(x.toUpperCase()) //x: string
}
}

Object.prototype.toString.call()

Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(new String('string')) // "[object String]"
Object.prototype.toString.call(function () {}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]

With a little bit of string processing using a regexp, we can come up with the following solution that can account for all cases:

function getType(obj) {
const lowerCaseTheFirstLetter = (str) => str[0].toLowerCase() + str.slice(1)
const type = typeof obj
if (type !== 'object') {
return type
}

return lowerCaseTheFirstLetter(
Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'),
)
}

getType([]) // "array"
getType('123') // "string"
getType(null) // "null"
getType(undefined) // "undefined"
getType() // "undefined"
getType(function () {}) // "function"
getType(/123/g) // "regExp"
getType(new Date()) // "date"
getType(new Map()) // "map"
getType(new Set()) // "set"

Type Narrowing by Downcasting

function toNumber(x: unknown): number {
if (typeof x === 'number') return x
if (typeof x === 'string') return parseInt(x, 10)

return NaN // Method 1
throw new Error('Error: x is neither a number nor a string') // Method 2
}

Type Assign Table

Type Assign Table

//* `undefined` ->(assign to) `void` ✅
const test1 = (x: void) => {}
test1(undefined) // Ok

//! object -> void ❌
const test2 = (x: void) => {}
test2({ id: 1 }) // Error