npm i -D typescript
tsc -w
: watch mode. Requires tsconfig.json
file. It can Work without any configuration.
If you don't set the input files in tsconfig.json
, Typescript will automatically find all the ts file in all folder, then compile ts
files to js
files in the same folder.
https://typescript.tv/glossary/index.html
There are 4 ways of assigning a type to a variable
- Type annotation (or colon annotation)
satisfies
as
annotation- Type inference: not annotating and letting TS infer it
A type annotation is when you explicitly define the type that a variable can take on:
let names: string = 'viet'; // String
let age: number = 3; // Number
We are not annotating the type and letting TS infer it.
With common primitive types(string, number, boolean), we can simply omit the type annotation
let name = 'viet'
let age = 3
In fact, most of the time you don't need to type your variables on declartion at all, just let TS infer it
- https://www.totaltypescript.com/clarifying-the-satisfies-operator
- https://typescript.tv/new-features/what-is-the-satisfies-operator-in-typescript/
- https://blog.logrocket.com/getting-started-typescript-satisfies-operator
It's like the combination of type inference and type annotation. It help us validate the type of variable and also infer the type of variable based on its value.
Normally, When you use a colon, the type BEATS the value. But with satisfies
, the value BEATS the type => use satisfies
when You want the EXACT type of the variable, not the WIDER type.
type TeamMembers = string | string[];
type TeamNames = 'Bulletproof' | 'Iconic';
type Teams = Record<TeamNames, TeamMembers>;
const AllTeams = {
Bulletproof: ['Alex', 'Lara', 'Sofia'],
Iconic: 'Benny',
}
Without satisfies
, if we use type inference like the above, we don't have restrictions on the TeamNames
and we can add whatever properties we want to AllTeams
const AllTeams = {
Bulletproof: ['Alex', 'Lara', 'Sofia'],
Iconic: 'Benny',
// Key is not part of "TeamNames"
IsNotAllowed: 'Bob',
};
Without satisfies
, if we use type annotation, we encounter a limitation in accessing specific methods based on the value types
const AllTeams: Teams = {
Bulletproof: ['Alex', 'Lara', 'Sofia'],
Iconic: 'Benny',
};
// The value of Teams can be string or array
// TS2339: Property 'join' does not exist on type 'TeamMembers'.
// We will need to use type guard to check the type of variable before using the methods
// like: if (typeof AllTeams.Bulletproof === 'string')
console.log(AllTeams.Bulletproof.join(', '));
// TS 2339: Property 'toUpperCase' does not exist on type 'TeamMembers'.
console.log(AllTeams.Iconic.toUpperCase());
With satisfies
, TS will both validate the type and infer the AllTeams
constant based on its value by automatically narrowing down TeamMembers
union
type TeamMembers = string | string[];
type TeamNames = 'Bulletproof' | 'Iconic';
type Teams = Record<TeamNames, TeamMembers>;
const AllTeams = {
Bulletproof: ['Alex', 'Lara', 'Sofia'],
Iconic: 'Benny',
// Ok, no other keys possible!
} satisfies Teams;
// ok!
console.log(AllTeams.Bulletproof.join(', '));
// ok!
console.log(AllTeams.Iconic.toUpperCase());
https://typescript.tv/new-features/how-to-use-const-assertions-in-typescript/
If you want to make your object and your array be read-only at design-time (not run time), add the keyword as const
after the declaration
const student = {
name: 'bob',
age: 20
} as const
const studentList = ['bob', 'joe', 'doe'] as const
Create a type based on two or more other type. Use |
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
It's a union of other types with discriminant properties (tag)
type LoadingState = {
state: "loading"; // tag
};
type FailedState = {
state: "failed"; // tag
status: number;
};
type SuccessState = {
state: "success"; // tag
response: {
isLoaded: boolean;
};
};
type State = LoadingState | FailedState | SuccessState;
// The first | is just for readability
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; x: number }
| { kind: "triangle"; x: number; y: number };
Create a type based on two or more interfaces with &
interface User {
id: string;
firstName: string;
lastName: string;
}
interface Engineer {
name: string;
age: number;
}
type Person = User & Engineer
const a: Person = {
id: 'string',
firstName: 'string',
lastName: 'string',
name: 'stirng',
age: 23
}
The type is narrowed down to specific value instead of generic type like 'string' or numer
const newValue = 'A' | 'B' | 'C';
const name = 'viet' // name has 'viet' type
Name a type Name convention: PascalCase
type StringOrNumber = string | number; // Union type
type Student = {
name: string;
id: StringOrNumber;
}
function( id: StringOrNumber, name: string ): void {}
Name an object type Name convention: PascalCase
interface Person {
name: string
age: number
speak?: (lang: string) => void
}
const viet: Person = {
name: 'viet',
age: 30
}
Difference between interface
and type
:
type
cannot be re-opened to add new properties
interface
is always extendable.
interface Student {
name: string
}
interface Student {
age: number
}
const student: Student = {
name: 'Bob',
age: 20
}
type Window = {
title: string
}
type Window = {
ts: TypeScriptAPI
}
// Error: Duplicate identifier 'Window'.
Never
type is a way of saying that this should never happen, this shouldn't be allowed . Often used for function that doesn't return
// Without Generic
interface List {
[index: number]: Number
}
type List1 = Number[]
// With Generic
interface List<T> {
[index: number]: T
}
type List1<T> = T[]
const a: List1<number> = [1,2,3]
- Define type for arguments: using ':'
function rect( width: number, height: number )
- Define type of return, default is
void
:
function square( side: number ) : number
- Function signature
let greet: ( a: string, b: number ) => void // function signature
greet = (name: string, age: number ) => console.log(`${name}`)
class Employee {
public name: string
private age: number
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// You can also write your constructor like this
constructor(
public name: string
private age: number
) {}
public get getAge() {
return this.age
}
public set setAge(v : number) {
this.age = v;
}
}
const viet = new Employee( 'viet', 30 )
viet.setAge = 32
console.log( viet.getAge );
A collection of constants that have the same category Easier to maintain and get the value Name convention: PascalCase
2 types: number enum and string enum
enum Status {
PENDING, // 0, default is number 0
IN_PROGRESS, // 1
DONE, // 2
CANCELLED, // 3
}
enum Status {
PENDING = 2,
IN_PROGRESS,
DONE,
CANCELLED,
}
// Get the value
console.log(Status['PENDING'])
console.log(Status.PENDING)
enum Type {
JSON: 'string',
HTML: 'html'
}
- Static data, not the data from API response. Use union type for data from API response.
Why:
Normally, if we use union type for elements in an array, the element can be one of those type defined in the union
let arr: Array<string | number>;
arr = ['viet', 1];
arr = [1, 'viet'];
There could be problem, when the type of the elements in array are matter, like the first one need to be string, the second one is number. That's why we need tuple.
Tuple defined fixed types in an array
const tuple: [string, number] = ['viet', 30];
const tupleFunction = (name: string, age: number): [string, number] => [name, age]
When to use: when we want to strictly define type for elements in array, like the returned value of React Hook
Must read: https://www.totaltypescript.com/mental-model-for-typescript-generics
- For types, interface, it turns them into a Javascript-alike function
- For functions, it connects the type of params you pass in and the code in your function body. If two params has connection to each other, you need to use generics, e.g one param is object and the other one is its key
- For object, you can pass value type of a property to another property's value type:
interface TableProps<T> {
rows: T[];
renderRow: (row: T) => ReactNode;
}
const useApi = <TData, TQueryKey extends QueryKey>(
queryKey: TQueryKey,
queryFn: (
key: QueryFunctionContext<TQueryKey>,
token: string,
) => Promise<TData>,
) => {
const token = useAuthToken()
return useQuery({
queryFn: (ctx) => queryFn(ctx, token),
queryKey,
});
};
When you don't know which type of input you pass into the function
Let's say you have a function like this:
const makeArr = (x: number) => [x]
But what if we didn't know if we wanted to passed in the number here, maybe say we wanted to pass in string or boolean. What will you do?
At first, you might want to change the type Number to any. However, this is basically going to remove all of your type safety and TS type functionality becomes useless.
This is where Generic comes in. You can use generic like this:
const makeArr = <T>(x: T) => [x]
T is shorthand for 'Types' and can be set manually or automatically
- Manually
makeArr<string>('3')
makeArr<string>(3) // error
- Automatically
makeArr(3) // T is number
makeArr('3') // T is string
- With function:
const foo = <T, >(x: T) => x;
function foo<T>(x: T): T {
return x;
}
- With type: When we pass generic to a type, it's like we are tranforming the type to a function and the generic is the param of that type
type foo<T> = {
};
- With interface:
interface Resource<T> {
uid: number,
data: T
}
const resourceOne: Resource<string> = {
uid: 1,
data: 'viet'
}
- With array
foo<T>[]
- In
.tsx
file
We need an additional comma.
const Component = <T,>() => {};
- When calling React functional component
interface User {
id: number;
name: string;
age: number;
}
<Table<User>
// @ts-expect-error rows should be User[]
rows={[1, 2, 3]}
renderRow={row => {
type test = Expect<Equal<typeof row, User>>;
return <td>{row.name}</td>;
}}
/>
The type can be restricted to a certain class family or interface, using the extends keyword:
const genericObject = <T extends {firstName: string, lastName: string}>( obj: T) => ({
..obj,
fullname: 'abc'
})
const args = {firstName: 'viet', lastName: 'nguyen', age: 30} // args must have firstName and lastName
const n3 = genericObject(args) // Now T has the type of args
type Fruit = "apple" | "banana" | "orange";
type GetAppleOrBanana<T> = T extends "apple" | "banana" ? T : never;
type AppleOrBanana = GetAppleOrBanana<Fruit> // 'apple' | 'banana'
When we pass an union type to a generic, it's like we are iterating it through the type function. We call this as distributive conditional type
type GetAppleOrBanana =
| ('apple' extends "apple" | "banana" ? 'apple' : never)
| ('banana' extends "apple" | "banana" ? 'banana' : never)
Enums are one great way to define named constant
enum Order {
First,
Second,
Third,
Fourth
}
You can assign values to the constant explicitly:
enum Order {
First = 0,
Second = 1,
Third = 2,
Fourth = 3
}
or also use strings:
enum Order {
First = 'FIRST',
Second = 'SECOND',
Third = 'THIRD',
Fourth = 'FOURTH'
}
Then you can access the constant like property of Object: Order.First
, Order.Second
Selectively construct type from other type
Partial<Type>
: return an object type that has all props ofType
to optional (Type is object type)Required<Type>
: return an object type that has all props ofType
to required (Type is object type)Pick<Type, Keys>
: return an object type by picking a set ofKeys
from Types (Type is object type)Omit<Type, Keys>
: return an object type by omitting a set ofKeys
from Types (Type is object type)Extract<Type, Keys>
: return type extracted fromType
: https://i.imgur.com/gVCRZQ6.png, it's kind of likePick
but Type inPick
is object andType
inExtract
is UnionExclude<Type, Keys>
: return type by excluding fromType
all union member fromKeys
, it's kind of likeOmit
Let's say we have multiple function and we don't know which returned type of each function => we can create a type helper function like this
type GetReturn<Fun> =
Fun extends (...args: any[]) => infer R ? R : never
With infer R we say that no matter the return type of this function, we store it in the type variable R. If we have a valid function, we return R as type
Helper function to check null
and undefined
declare function isAvailable<Obj>(obj: Obj): obj is NonNullable<Obj>
// Implementation
function isAvaialble<Obj>(obj: Obj): obj is NonNullable<Obj> {
return typeof obj !== 'undefined' && obj !== null
}
// Usage
// orders is Order[] | null
const orders = await fetchOrderList(customer)
if(isAvailable(orders)) {
//orders is Order[]
listOrders(orders)
}
- https://thewebdev.info/2020/08/12/typescript-any-and-unknown-types/
- https://dev.to/arafat4693/5-mistakes-that-every-typescript-deverloper-should-avoid-33b
When we use any
, we are telling TS to ignore type checking.
On the other hand, unknown
doesn't turn off type checking. It tells the compiler that the type of a value is not known yet, you need to check its type.
You can assign anything to an unknown type, but you have to do a type check to operate on unknown
function myFunction(fn: any) {
fn();
}
invokeAnything(1); // this line won't trigger type error but it will throws runtime errors
function myFunction(fn: unknown) {
fn(); // triggers a type error
}
invokeAnything(1);
So, to get rid of this problem you can perform type checking before using a variable of type unknown
. In the example, you would need to check if fn
is a function type:
function myFunction(fn: unknown) {
if (typeof fn === 'function') {
fn(); // no type error
}
}
invokeAnything(1);
https://dev.to/arafat4693/5-mistakes-that-every-typescript-deverloper-should-avoid-33b In TypeScript, a type guard is a way to narrow the type of a variable within a conditional block
https://thewebdev.info/2020/12/19/how-does-typescript-know-which-types-are-compatible-with-each-otherenums-and-more/ Functions overloads, where thereβre multiple signatures for a function with the same name, must have arguments that match at least one of the signatures for TypeScript to accept the function call as valid. For example, if we have the following function overload:
function fn(a: { b: number, c: string, d: boolean }): number
function fn(a: { b: number, c: string }): number
function fn(a: any): number {
return a.b;
};
Then we have to call the fn function by passing in at a number for the first argument and a string for the second argument like we do below:
fn({ b: 1, c: 'a' });
TypeScript will check against each signature to see if the property structure of the object we pass in as the argument matches any of the overloads. If it doesnβt match like if we have the following:
fn({ b: 1 });
Then we get the following error message:
No overload matches this call.
Overload 1 of 2, '(a: { b: number; c: string; d: boolean; })
Contains type definitions for libraries originally written in vanilla javascript
.d.ts
file is where we put all the custom type definitions and ambient type declaration
However, they are not available in other files yet. To make our types available, we also have to export them
declare const isDevelopment: boolean // Ambient type
export type StorageItem = {
weight: number
}
You can ignore the typescript error by adding the exclamation mark, if you know that it's safe for sure.
data.value!
const fruits = ["apple", "banana", "orange"] as const;
type AppleOrBanana = typeof fruits[0 | 1];
type Fruit = typeof fruits[number]; // 'apple' | 'banana' | 'orange'
const fruits = ["apple", "banana", "orange"] as const;
type Fruit = typeof fruits[number]; // 'apple' | 'banana' | 'orange'
// So, we can use this technique to handle every elements of array
type Falsy = 0 | '' | false | [] | {[P in any] : never}
type IsTruthy<T extends readonly unknown[]> = T extends Falsy ? false : true
type NonEmptyArray<T> = [T, ...Array<T>];
type Check = NonEmptyArray<[]> // false
type Check = NonEmptyArray<['a']> // true
type Method1<T> = T extends [] ? 'empty' : 'not empty';
type Method2<T extends unknown[]> = T[number] extends never ? 'empty' : 'not empty'
type Method3<T extends unknown[]> = T extends [infer P, ...infer Rest] ? : 'not empty and here you can do something with P and Rest' : 'empty'
type T = Array<unknown>
type Check = T extends [infer First, ...infer Rest] ? //...do something : //...do something
// Example
type Includes<T extends readonly any[], U> =
T extends [infer First, ...infer Rest]
? Equal<First, U> extends true
? true
: Includes<Rest, U>
: false
The technique below is called mapped type. K in keyof Values
block will iterate through the union keyof Value
. This is similar to distributive condition type.
interface Values {
email: string;
firstName: string;
lastName: string;
}
type ValuesAsUnionOfTuples = {
[K in keyof Values]: [K, Values[K]];
}[keyof Values];
// In case you want to extract specific keys from object, just use indexed type
type emailOrFirstName = Values['email' | 'firstName'];
There is also a technique called remap mapped type where you can modify the key of mapped type
interface User {
age: number;
firstName: string;
lastName: string;
}
type UserValidation = {
[P in keyof User as `has${Capitalize<P>}`]: boolean
};
type Route = "/" | "/about" | "/admin" | "/admin/users";
type RoutesObject = {
[K in Route]: K
}
type Route =
| {
route: "/";
search: {
page: string;
perPage: string;
};
}
| { route: "/about"; search: {} }
| { route: "/admin"; search: {} }
| { route: "/admin/users"; search: {} };
type RoutesObject = {
[K in Route as K['route']]: K['search']
};
- Using
&
type A = { propA: valueA }
type B = { propB: valueB }
type C = A & B // {propA: valueA, propB: valueB}
- Convert from Union
type AppendToObject<T, Prop extends string, Value> = {
[P in keyof T | Prop]: P extends keyof T ? T[P] : Value;
};
type Event =
| {
type: 'SIGN_IN';
payload: '123';
}
| {
type: 'SIGN_OUT';
};
const sendEvent = <Type extends Event['type']>(...args: Extract<Event, {type: Type}> extends {payload: infer Payload} ? [type: Type, payload: Payload] : [type: Type]) => {}
const groupBy = <
TObj extends Record<string, unknown>,
TKey extends keyof TObj
>(
arr: TObj[],
key: TKey
) => {
const result = {} as Record<
TObj[TKey] & PropertyKey,
TObj[]
>;
arr.forEach((item) => {
const resultKey = item[key] as TObj[TKey] &
PropertyKey;
if (result[resultKey]) {
result[resultKey].push(item);
} else {
result[resultKey] = [item];
}
});
return result;
};
const result = groupBy(array, "age");
result[20].forEach((item) => {
// No errors, no validation needed!
console.log(item.name, item.age);
});
Union is iterated by default.
When you put an union into a generics (distributed conditional type) or mapped type of object, it's kind of being iterated through every single condition of that union.
https://github.com/millsp/ts-toolbelt
// Method 1
const cache: Record<string, string> = {}
// Method 2
const cache: {[id: string]: string} = {}
type getUser = (id: string) => Promise<User>
You can use whatever you want. Type is more preferred because we can use it to define union type which is not possible with Interface
Just use == null
or != null
(not ===
and !==
)
function foo(a?: number | null) {
if (a == null) return;
// a is number now.
}
We can put this in catch
block
if (error instanceof Error) {
error
// ^? const error: Error
}
// Check axios error
if (axios.isAxiosError(error))
// You could return the boolean type, but it's not good if you use this function with `if` later
// function petIsCat(pet: Pet): boolean {
// return pet.species === "cat";
// }
function petIsCat(pet: Pet): pet is Cat {
return pet.species === "cat";
}
//Good
if (petIsCat(p)) {
p.meow(); // now compiler knows for sure that the variable is of type Cat and it has meow method
}
import { ReactNode } from 'react';
const Apple = () => <span>ππ</span>;
const Kiwi = () => <span>π₯</span>;
const Cherry = () => <span>π</span>;
const Grape = () => <span>π</span>;
const icon: Record<Fruit, ReactNode> = {
apple: <Apple />,
kiwi: <Kiwi />,
cherry: <Cherry />,
grape: <Grape />,
};
If you have a type with 2 props, both optional, but you want only one of them to exist at the same time. It's like when prop1 is null then prop2 is no longer optional and vice versa.
type Freecourse = {
youtube: string;
price?: never
}
type Paidcourse = {
youtube?: never;
price: number
}
type Course = Freecourse | Paidcourse;