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 innever
.never | T
always results inT
.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 existinginterface
.- Performance of intersections (Extended
interface
ortype
) ininterface - &
is higher thantype - 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"
tostring
) - Object literals get
readonly
properties (no assign to objectprops
) - Array literals get
readonly
tuples (no assign to arrayvalue
)
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 JSobject
is
: Type predicates, in this case, it ispet 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
- Basic
- use case with `keyof`
// `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)
}
let x = { a: 1, b: 2, c: 3, d: 4 }
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
getProperty(x, 'a')
getProperty(x, 'm') //! Error
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
- Basic
- use case with `never`
type Id = number
type Name = string
type NameOrId<T extends number | string> = T extends number ? Id : Name
type MessageOf<T> = T extends { message: unknown } ? T['message'] : never
interface Email {
message: string
}
interface Dog {
bark(): void
}
type EmailMessageContents = MessageOf<Email> // string
type DogMessageContents = MessageOf<Dog> // never
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
//* `undefined` ->(assign to) `void` ✅
const test1 = (x: void) => {}
test1(undefined) // Ok
//! object -> void ❌
const test2 = (x: void) => {}
test2({ id: 1 }) // Error