Skip to main content
Most Practical TypeScript Features
11 min read

Build the internal tools you need(Dashboard, Workflow Management, CRM, Support and Billing management...) in minutes.

Most Practical TypeScript Features

The importance of type programming in projects goes without saying. This article will summarize some common features of TypeScript, helping everyone to familiarize and master its use.

Advanced Types

Intersection Types

Intersection types combine multiple types into one using the & symbol.

interface I1 {
name: string;
}

interface I2 {
age: number;
}

type T3 = I1 & I2

const a: T3 = {
name: "tj",
age: 11,
}

Union Types

Union types, indicated by the | symbol, mean a value can be one of several types.

const a: string | number = 1

String Literal Types

String literal types use a string type as the type of a variable.

const a: 'number' = 'number'

Numeric Literal Types

Numeric literal types use a number as the type of a variable.

const a: 1 = 1

Boolean Literal Types

Boolean literal types use a boolean value as the type of a variable.

const a: true = true

Template Literal Types

Template literal types use ES6 template string syntax to constrain types.

type https = `https://${string}`
const a:https = `https://jd.com`

Operators

keyof

keyof is used to get all keys of a type, returning a union type.

// const a: 'name' | 'age' = 'name'
const a: keyof {
name: string,
age: number
} = 'name'

typeof

typeof is used to get the structural type of an object or function.

const a2 = {
name: 'tj',
}

type T1 = typeof a2 // {name: string}

function fn1(x: number): number {
return x * 10
}

type T2 = typeof fn1 // (x: number) => number

in

in is used to iterate over union types.

const obj = {
name: "tj",
age: 11,
}

type T5 = {
[P in keyof typeof obj]: any
}

/*
{
name: any,
age: any
}
*/

T[K]

T[K] is used to access an index, obtaining the union type of the corresponding value.

interface I3 {
name: string,
age: number
}
type T6 = I3[keyof I3] // string | number

Operators

Non-null Assertion Operator

The non-null assertion operator ! is used to emphasize that an element is neither null nor undefined, informing TypeScript that the property will be explicitly assigned.

function Demo(): JSX.Element () {
const divRef = useRef<HTMLDivElement>()
useEffect(() => {
divRef.current!.scrollIntoView() // Asserting that divRef.current is not null or undefined
}, [])
return <div ref={divRef}>divDemo</div>
}

Optional Chaining Operator

The optional chaining operator ?. is used to check if the left-hand expression is null or undefined, stopping the expression if true.

const a = b?.a

Nullish Coalescing Operator

The nullish coalescing operator ?? is used to check if the left-hand expression is null or undefined, returning the right-hand value if not.

const a = b ?? 10

Numeric Separators

Numeric separators _ are used to split long numbers for easier reading. The compiled result will automatically remove _.

const num: number = 1_111_111_111

Type Aliases

Type aliases are used to give a type a new name.

type Message = string | string[]
let greet = (message: Message) => {
// ...
}

Type Assertions

Type assertions tell the browser what type I am very certain of.

// Angle bracket syntax
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

// as syntax
let someValue: any =

"this is a string";
let strLength: number = (someValue as string).length;

Type Guards

Type guards are expressions that check types at runtime to ensure the type within a certain scope.

interface A {
name: string;
age: number;
}

interface B {
sex: string;
home: string;
}

function doSomething(person: A | B): void {
if (person.name) {
// Error
// ...
}
}

// Using in type guard
function doSomething(person: A | B): void {
if ("name" in person) {
// OK
// ...
}
}

// Using typeof type guard
function A(a: string | number): string | number {
if (typeof a === "string") {
// OK
return a + ""
}
if (typeof a === "number") {
// OK
return a + 2
}
return ""
}

// instanceof type guard
class Foo {
}

class Bar {
}

function test(input: Foo | Bar) {
if (input instanceof Foo) {
// Here input type 'narrows' to Foo
} else {
// Here input type 'narrows' to Bar
}
}

Generics

Introduction to Generics

Generics are like passing parameters to types to get a more general type, just like passing parameters to functions.

As shown below, we get a general generic type T1, which can become T2 type string[] and T3 type number[] through passing parameters:

type T1<T> = T[]
type T2 = T1<string> // string[]
type T3 = T1<number> // number[]

As above, T is a variable, and we can replace it with any other variable name.

type T4<A> = A[]
type T5 = T4<string> // string[]
type T6 = T4<number> // number[]

Naming Convention

In the TypeScript generic variable naming convention, 4 common generic variable names are defaulted. To improve readability, it is not recommended to define them as other variable names.

T: Represents the first parameter K: Represents the key type of the object V: Represents the value type of the object E: Represents the element type

Generic Interface

Generic interfaces, similar to the examples above, pass parameters to interface types:

interface I1<T, U> {
name: T;
age: U;
}
type I2 = I1<string, number>

Generic Constraints (extends operator)

Sometimes, we need to constrain generic parameters, limiting each variable's type. TypeScript implements type constraints through extends.

Generic constraint syntax is as follows:

Generic name extends type T extends Length constraints T's type to include the length property, and the type of length must be number.

interface Length {
length: number
}

function fn1<T extends Length>(arg: T): number {
return arg.length
}

K is constrained by extends to be a key of T.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}

let tsInfo = {
name: "Typescript",
supersetOf: "Javascript",
difficulty: Difficulty.Intermediate,
}

let difficulty: Difficulty =
getProperty(tsInfo, "difficulty") // OK

let supersetOf: string =
getProperty(tsInfo, "superset_of") // Error

Default Generic Parameters

Generic parameters default values, like function parameter default values, are given default values when not passed.

interface I4<T = string> {
name: T;
}

const S1: I4 = { name: "123" } // By default name: string type
const S2: I4<number> = { name: 123 }

Conditional Generics

Conditional types mean the same as conditional judgments in Js, which is if the condition is met, then xx, otherwise xx.

Conditional type expression:

T extends U ? X : Y

If T can be assigned to U, then the type is X, otherwise the type is Y.

type T1<T> = T extends string ? 'string' : 'number'
type T2 = T1<string> // 'string'
type T3 = T1<number> // 'number

Generic Inference (infer Operator)

The keyword for generic inference is infer, and the syntax is infer Type.

It is generally used in combination with generic conditional types. To understand this, consider a practical example:

If the generic parameter T can be assigned to the type {t: infer Test}, then the type is the inferred type Test, otherwise, the type is string.

type Foo<T> = T extends {t: infer Test} ? Test : string

The generic parameter number does not have a t property, so the type is string.

type One = Foo<number> // string

The t property of the generic parameter is boolean, so the type is the inferred type boolean.

type Two = Foo<{ t: boolean }> // boolean

The t property of the generic parameter is () => void, so the type is the inferred type () => void.

type Three = Foo<{ a: number, t: () => void }> // () => void

Generic Utility Types

Mapped Types

Mapped types are a kind of generic type that can be used to map an existing object type into a new object type.

Common syntax for mapped types:

{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

For example, making all properties optional using mapped types:

type Partial<T> = {
[P in keyof T]?: T[P]
}

Partial

TypeScript has encapsulated some common mapped types, such as Partial, which is used to make all properties of a generic optional.

type Partial<T> = {
[P in keyof T]?: T[P]
}

type T1 = Partial<{
name: string,
}>

const a: T1 = {} // No error without the name property

Required

Required makes all properties of the generic required.

type Required<T> = {
[P in keyof T]-?: T[P]
}

type T2 = Required<{
name?: string,
}>

const b: T2 = {} // TypeScript error, the type "{}" lacks the property "name", but it is required in "Required<{ name?: string | undefined; }>". ts(2741)

The syntax -? means to remove the optional ? property.

Readonly

Readonly makes all properties of the generic read-only.

type T3 = Readonly<{
name: string,
}>

const c: T3 = {
name: "tj",
}

c.name = "tj1" // TypeScript error, cannot assign to "name" because it is a read-only property. ts(2540)

Pick

Pick selects certain properties from a type to create a new type.

type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}

type T4 = Pick<
{
name: string,
age: number,
},
"name"
>

/*
This is a new type, T4 = {name: string}
*/

const d: T4 = {
name: "tj",
}

Record

Record converts keys and values into type T.

type Record<K extends keyof any, T> = {
[key in K]: T
}
const e: Record<string, string> = {
name: 'tj',
}

const f: Record<string, number> = {
age: 11,
}

keyof any corresponds to types number | string | symbol, which are the collection of types that can be object keys.

ReturnType

ReturnType gets the return type corresponding to type T.

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

Exclude

Exclude removes types in one type that are part of another.

type Exclude<T, U> = T extends U ? never : T;
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>;

// string | number

Extract

Extract extracts type U from T.

type Extract<T, U> = T extends U ? T : never;
type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void

Omit

Omit constructs a new type using all properties from type T except those in type K.

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

interface Todo {
title: string;
completed: boolean;
description: string;
}

type TodoPreview = Omit<Todo, "description">;

/*
{
title: string;
completed: boolean;
}
*/

NonNullable

NonNullable is used to filter out null and undefined from a type.

type NonNullable<T> = T extends null | undefined ? never : T;
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Parameters

Parameters is used to get the tuple type composed of a function's parameter types.

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any
? P : never;

type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]

Conclusion

Bookmark this now!