banner
 Sayyiku

Sayyiku

Chaos is a ladder
telegram
twitter

TypeScript Keywords

Type Constraint extends#

Syntax: T extends K, where extends is not inheritance for classes or interfaces, but rather a way to judge and constrain types. It means to determine if T can be assigned to K.

Determine if T can be assigned to U, if so, return T, otherwise return never:

type Exclude<T, U> = T extends U ? T : never

Type Mapping in#

Iterate over the keys of a specified interface or a union type:

interface Person {
  name: string
  age: number
  gender: number
}

// Convert all properties of T to readonly type
type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// type ReadonlyPerson = {
//   readonly name: string
//   readonly age: number
//   readonly gender: number
// }
type ReadonlyPerson = Readonly<Person>

Type Predicate is#

TypeScript has a type guard mechanism. To define a type guard, we simply define a function that returns a type predicate:

function isString(test: any): test is string {
  return typeof test === 'string'
}

What is the difference between the above code and a function that returns a boolean value?

function isString(test: any): boolean {
  return typeof test === 'string'
}

When using the is type guard:

function isString(test: any): test is string {
  return typeof test === 'string'
}

function example(foo: any) {
  if (isString(foo)) {
    console.log('it is a string' + foo)
    console.log(foo.length) // string function
    // The following code will cause a compilation error and a runtime error because foo is a string and does not have the toExponential method
    console.log(foo.toExponential(2))
  }
  // Compilation will not fail, but a runtime error will occur
  console.log(foo.toExponential(2))
}
example('hello world')

When the return value is boolean:

function isString(test: any): boolean {
  return typeof test === 'string'
}

function example(foo: any) {
  if (isString(foo)) {
    console.log('it is a string' + foo)
    console.log(foo.length) // string function
    // foo is of type any, so it compiles fine. But a runtime error will occur because foo is a string and does not have the toExponential method
    console.log(foo.toExponential(2))
  }
}
example('hello world')

Summary:

  • When using type guards, TS further narrows down the type of the variable. In the example, the type is narrowed down from any to string;
  • The scope of a type guard is limited to the block scope after the if statement.

Practical example:

function isAxiosError(error: any): error is AxiosError {
  return error.isAxiosError
}

if (isAxiosError(err)) {
  code = `Axios-${err.code}`
}

Inferred Type infer#

You can use infer P to mark a generic type as an inferred type and use it directly.

Get the parameter type of a function:

type ParamType<T> = T extends (param: infer P) => any ? P : T

type FunctionType = (value: number) => boolean

type Param = ParamType<FunctionType> // type Param = number

type OtherParam = ParamType<symbol> // type Param = symbol

Determine if T can be assigned to (param: infer P) => any, and infer the parameter as the generic type P. If it can be assigned, return the parameter type P, otherwise return the input type.

Get the return type of a function:

type ReturnValueType<T> = T extends (param: any) => infer U ? U : T

type FunctionType = (value: number) => boolean

type Return = ReturnValueType<FunctionType> // type Return = boolean

type OtherReturn = ReturnValueType<number> // type OtherReturn = number

Determine if T can be assigned to (param: any) => infer U, and infer the return type as the generic type U. If it can be assigned, return the return type P, otherwise return the input type.

Primitive Type Guard typeof#

Syntax: typeof v === 'typename' or typeof v !== 'typename', used to determine if the type of data is a specific primitive type (number, string, boolean, symbol) and perform type guarding.

The typename must be number, string, boolean, or symbol. However, TypeScript does not prevent you from comparing it with other strings, as the language does not recognize those expressions as type guards.

Example: The print function will print different results based on the parameter type. How do we determine if the parameter is a string or a number?

function print(value: number | string) {
  // If it is a string type
  // console.log(value.split('').join(', '))
  // If it is a number type
  // console.log(value.toFixed(2))
}

There are two common ways to determine the type:

  1. Determine if it has the split property to check if it is a string type, and if it has the toFixed method to check if it is a number type. Drawback: Type conversion is required for both checking and calling.
  2. Use a type predicate is. Drawback: You have to write a utility function every time, which is too cumbersome.

In this case, we can use typeof:

function print(value: number | string) {
  if (typeof value === 'string') {
    console.log(value.split('').join(', '))
  } else {
    console.log(value.toFixed(2))
  }
}

After using typeof for type checking, TypeScript narrows down the variable to that specific type, as long as the type is compatible with the original type of the variable.

Type Guard instanceof#

Similar to typeof, but works differently. The instanceof type guard is a way to refine types based on their constructor functions.

The right-hand side of instanceof must be a constructor function. TypeScript refines it to:

  • The type of the prototype property of this constructor function, if its type is not any
  • The union of the types returned by the construct signatures
class Bird {
  fly() {
    console.log('Bird flying')
  }
  layEggs() {
    console.log('Bird layEggs')
  }
}

class Fish {
  swim() {
    console.log('Fish swimming')
  }
  layEggs() {
    console.log('Fish layEggs')
  }
}

const bird = new Bird()
const fish = new Fish()

function start(pet: Bird | Fish) {
  // Calling layEggs is fine because both Bird and Fish have the layEggs method
  pet.layEggs()

  if (pet instanceof Bird) {
    pet.fly()
  } else {
    pet.swim()
  }

  // Equivalent to the following
  // if ((pet as Bird).fly) {
  //   (pet as Bird).fly();
  // } else if ((pet as Fish).swim) {
  //   (pet as Fish).swim();
  // }
}

Index Type Query Operator keyof#

Syntax: keyof T, for any type T, the result of keyof T is the union of known public property names on T.

keyof is similar to Object.keys, but keyof takes the keys of an interface.

interface Point {
  x: number
  y: number
}

// type keys = "x" | "y"
type keys = keyof Point

Suppose there is an object as shown below, and we need to implement a get function using TypeScript to get its property value:

function get(o: object, name: string) {
  return o[name]
}

We might start by writing it like this, but it has many drawbacks:

  1. Unable to determine the return type: This will lose TypeScript's most powerful type checking feature;
  2. Unable to constrain the key: It may cause spelling errors.

In this case, we can use keyof to enhance the type functionality of the get function:

function get<T extends object, K extends keyof T>(o: T, name: K): T[K] {
  return o[name]
}

Note that keyof can only return known public property names on a type:

class Animal {
  type: string
  weight: number
  private speed: number
}

type AnimalProps = keyof Animal // 'type' | 'weight'

When you need to get the value of a specific property of an object, but you are not sure which property it is, you can use extends with typeof to restrict the parameter to be a property name of the object:

const person = {
  name: 'Jack',
  age: 20,
}

function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
  return person[fieldName]
}

const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.