What do you know about functions of TS? Typescript series :(2) functions

Function plays an absolutely important role in our daily code, and it is very beneficial to understand the use of function in TS. If you are not familiar with generic functions, function signatures, and function overloading, reading this article will give you a more detailed understanding of the functions in TS.

[TOC]

First, return value

When we declare a function/method, we can put a type comment after the parentheses to constrain the type of its return value, or inferential it to type any if there is no explicit constraint on the type of the return value. All types except void and any should have a return value of the corresponding type.

  • Returns an error if the value is not of the constraint type, or if the constraint type does not return the corresponding type:
// type inference is made from the initial value 'cc' to indicate that _name is a string
let _name = 'cc'

Return () {return () {return () {return ();
function getName1() :string {}// Restrict the return value type to string
function getName1() :string {
  return _name
}

// Define a variable of type number _name2
let _name2 = 18
// Constraint function returns a value of type string
function getName2() :string {
  // The return value should be string and _name2 is number
  return _name2
}
Copy the code
  • It is also not true when our actual return value may not be the type of the constraint:
let _name3: string | number = 'cc'
function getName3() :string {
  // Invalid return value because _name3 may be number and the return value can only be string
  return _name3
}
Copy the code
  • This kind of situationThis is especially true of literal types:
// _name4 is a string inferred from type inference
let _name4 = 'cc'
/ / constraints, the return value can only be for the 'cc' | 'yy' type
function getName4() : 'cc'|'yy' {
  // An error is reported. Although _name4 is 'cc', it is a string
  // return _name4

  // This can be solved with type assertions, which we will cover later
  return _name4 as 'cc'
}
Copy the code
  • If the return value of a function is null, use void. In this case, return undefined, return null, or do not write return. Undefined is returned by default:
let _name = 'cc'

// return null value undefined
function setName(name) :void {
  _name = name
}

let a = setName('yy')  / / a is undefined
Copy the code

Second, the parameters

In TS, we often need to add a type comment to a function parameter. If we do not add a type comment, the parameter will be inferred to any by type. TS not only restricts the types of arguments passed, but also the types of parameters inside a function.

let _name = 'cc'
// Define a function that takes a string and returns no value
function setName2(name: string) :void {
  _name = name
}
Copy the code

Sometimes, we is more complex, the parameters such as various types of combination: string | number, then we need to shrink, the types of case in the case of return or parameter method is called.

let _name = 'cc'

// Error example
function setName3(name: string | number) :void {
  // The name argument may be number, so it cannot be assigned directly
  _name = name
}

// Correct example
function setName3(name: string | number) :void {
  // Name is a string
  if(typeof name === 'string'){
    _name = name
  }else{
    // Name is of the number type and can be forcibly converted
    _name = String(name)
  }
}
Copy the code

Sometimes, when a parameter is not required, you can add the question mark “?” after the parameter. To represent an optional parameter, which is undefined if not passed when the function is called. Therefore, inside the function body, the parameter may be undefined, which also requires type reduction.

let userInfo: {name:string.age:number.gender: 1 | 2 | 3};
function setUserInfo(name:string, age:number, gender? :1 | 2 | 3){
  if(gender === undefined){
    userInfo.gender = 3
  }else{
    userInfo.gender = gender
  }
}
Copy the code

Function type expressions

In TS you can define a function type using the arrow function form: (a: Type1, b: Type2…) => TypeN Indicates that the received parameter names are A, B… , the types are Type1, Type2… , returns a function of type TypeN.

// Fn1 is a function that takes a string name and a number age,
// Return a sttring value
type Fn1 = (name: string, age: number) = >string

// To add type Fn1 to fn1, both the parameter and the return value must meet the constraints of FN1
// The type is already constrained by Fn1, so there is no need to annotate the parameters and return values
const fn1: Fn1 = function(name, age){
  return 'I am' + name
}

// Arrow functions can also be used
const fn11: Fn1 = (name, age) = > 'I am' + name
Copy the code

When declaring methods on objects, we can easily use function-type expressions:

// Define a User interface that contains the interest method, passing in a string argument,
interface User {
  name: string.age: number.interest: (something: string) = >void
}

const user: User = {
  name: 'cc'.age: 18.interest(something){
    // ...}}Copy the code

Fourth, type reduction

In functions, we often encounter cases where the parameter is a composite type or an optional parameter. In this case, we need to reduce the type of the parameter, so that we can do the corresponding operation for each specific subtype to prevent type errors. As the process progresses, the range of possible types of this parameter gets smaller.

Control flow analysis: if-else or switch-case.

(I) Control flow analysis

Use if, else, and other control-flow statements to gradually narrow down the type range of parameters.

  • typeof type gurads

    In the following example, we use typeof, the Type GurADS type guard. Typeof returns a fixed list of strings that we reduce the type range based on.

type Fn = (name? :string | number) = > string

const fn: Fn = function(name){
  // Type reduction
  if(name === undefined) {return 'default name'
  // This can only be string or number
  }else if(typeof name === 'string') {return name
  // Can only be number
  }else{
    return String(name)
  }
}
Copy the code

Return value of typeof:

  1. “string”

  2. “numbrt”

  3. “bigint”

  4. “boolean”

  5. “symbol”

  6. “undefined”

  7. “object”

  8. “function”

As you can see, Typeof does not detect null. Typeof NULL returns “object”, so we can check with a “truthiness” check.

  • Truthiness is reflected for truth check

    Use true and false to determine the truth condition, so as to achieve the purpose of type reduction.

type Fn = (name? :string) = > string

const fn2: Fn = function(name){
  // True check
  if(name){
    return name
  }else{
    return 'default name'}}Copy the code

The following is a list of values that return false using if. According to the official Tyscript documentation, all values except those listed below return true.

  1. 0

  2. NaN

  3. “” Empty string

  4. 0n The number 0 + letter N is a bigint 0

  5. null

  6. undefined

If we want to convert any value to the corresponding Boolean type, we can use the Boolean negation character “!” , any value after double negation will be converted to the corresponding Boolean value.

!!!!!0;  // false!!!!!NaN  // false!!!!!""  // false!!!!!'name'  // true
Copy the code
  • Equality narrowing for equivalence check

    Using the known conditions for equivalence verification, TS can deduce the corresponding parameter type, to achieve the purpose of type reduction.

  • The in operator

    Use the expression “value” in x to determine whether an attribute exists in the object for type reduction.

type Fish = {
  swim: () = > void
}

type Dog = {
  bark: () = > void
}

function doSomething(obj: Fish | Dog){
  // Bark is Dog
  if('bark' in obj){
    console.log(Woof woof woof)}else{
    // Otherwise, Fish
    console.log('I am Fish')}}Copy the code
  • Use the instanceof

    Used for Array, Date, and other reference types.

(2) Type prediction

To define a custom type guard, we can usually use a function that returns a type prediction.

Type prediction format: param is Type, which we can then use for Type reduction.

type Fish = {
  swim: () = > void
}

type Dog = {
  bark: () = > void
}

// I may not be a person, but a real dog
function isDog(obj: Fish | Dog) :obj is Dog {
  return ('bark' in obj)
}

let animal: Fish | Dog = {
  swim: () = > console.log('I am Fish')}// Perform type reduction
if(isDog(animal)){
  animal.bark()
}else{
  animal.swim()
}
Copy the code

Note that if the animal’s method is not swim but bark, TS will infer that the animal is Dog, thus excluding the Fish type. Now, we have animal being Dog in the if branch, and animal being never in the else branch.

(3) parse the union type

In the example above, we examined some of the simpler types. But in reality, slightly more complex types are quite common. In the official documentation, an example is given: We define a Shape interface, Shape, that uses the kind attribute to indicate whether it is a circle or a square. The circle only needs a radius attribute, and the square only needs a side_length attribute. So we use the optional property, if it’s circle, we have the RADIUS property but not the side_length property, and vice versa.

interface Shape {
  kind: 'circle' | 'square', radius? :number, side? :number
}
Copy the code

Next we need a function to find the area. The parameter is of type Shape. Since both the radius and side parameters are optional, they can be null. Normally, we would use a different area formula to determine whether it is a circle or a square based on the value of the kind property:

function getArea(obj: Shape){
  if(obj.kind  === 'circle') {Obj. Radius may be empty
    return Math.PI * obj.radius ** 2
  }else{
    // Square area, error, obj.side may be empty
    return obj.side ** 2}}Copy the code

At this point, however, you will notice that under strict null checking, this code will report an error. Because radius and Side are both optional attributes, they can both have null values. Of course, we could use non-empty assertions here, but perhaps we could do it in a more sensible way: define different interfaces for circle and Square, since they are two completely different things. At this point, our getArea function will not have the above problems.

interface Circle {
  kind: 'circle'.radius: number
}

interface Square {
  kind: 'square'.side: number
}

type Shape = Circle | Square

function getArea(obj: Shape){
  if(obj.kind === 'circle') {// Circle must have radius
    return Math.PI * obj.radius ** 2
  }else{
    // if it is Square, it must have side
    return obj.side ** 2}}Copy the code

By designing interfaces reasonably, problems can be solved more elegantly.

(4) Never

When we do type reduction, once all possible types have been reduced, if we continue to reduce, for example by adding an else branch, we get a never type. TS uses the never type to tell us that the current situation is tan (math.pi / 2). The never type can be assigned to any type, but no other type (except never itself) can be assigned to never. This feature is often used for exhaustive checksums.

(5) Exhaustive check

When we do type reduction, sometimes we can’t consider all the cases. Therefore, you can use exhaustive checksums to avoid missing types. The exhaustive check takes advantage of the never feature above. In the last branch of the control flow (such as the default branch of the switch statement, or the else branch at the end of the if statement), an attempt is made to assign the type reduction argument to a variable of type never. Since only the never type can be assigned to the never type, if we are not thoughtful enough and the parameter type is missing, then in the last branch, the parameter type will not be never and cannot be assigned to a variable of the never type, and TS will report an error to inform us. If we consider all types, then the last branch of this parameter is never and can be assigned to a variable of never, TS will not report an error. Therefore, by exhaustive checking, we can see if we have considered all types of cases by simply looking at whether there are corresponding errors in the last branch.

interface Circle {
  kind: 'circle'.radius: number
}

interface Square {
  kind: 'square'.side: number
}

interface Triangle {
  kind: 'triangle'.side: number
}

type Shape = Circle | Square

function getArea(obj: Shape){
  if(obj.kind === 'circle') {// Circle must have radius
    return Math.PI * obj.radius ** 2
  }else if(obj.kind === 'square') {// if it is Square, it must have side
    return obj.side ** 2
  }else{
    // Perform exhaustive check on the last branch
    const _isExhaustive: never = shape
    return _isExhaustive
  }
}
Copy the code

Fifth, the order of function

Now that we’ve introduced function type expressions, let’s learn more about functions.

(1) Function signature

  1. Call sign

Functions are also objects that can have their own attributes. However, when using function-type expressions, you cannot simultaneously declare function attributes. A call signature describes a function type, including the properties of the function, the parameters to be passed when calling the function, and the return value. The use of call signatures is a convenient way to overcome the shortcomings of function-type expressions.

// Declare the call signature. The call signature is a type whose name can be arbitrarily chosen
type CallSignatureFn = {
  // Function attributes
  grade: string.// Function parameters and return values
  (arg1: number.arg2: string) :string
}

function logInfo(fn: CallSignatureFn) {
  console.log(fn.grade + " returned " + fn(6.'A'));
}
Copy the code

Call signature vs function type expression:

  • Function type expressions are very succinct

  • Calling signatures declare properties of functions

  • Calls are preceded by a colon “:” between the argument list and the return value, while function type expressions use the arrow “=>”

  1. Construct the signature

In addition to being called directly, functions can also be called using the new operator. The construction signature describes the parameters and return values of a function when called using the new operator.

type ConstructSignatureFn = {
  new (_type: string._num: number) :string[]}function fn(ctor: ConstructSignatureFn) {
  return new ctor("hello".2);
}
Copy the code
  1. Mixed signature

For some special functions, such as Date, the result of the call is the same as the result of the call using the new operator. This type of function can use mixed signatures, writing the calling signature and construction signature in one type object.

interface CallOrConstruct {
  new (s: string) :Date; (n? :number) :number;
}
Copy the code
  1. Reloading the signatureTo realize the signature

This is covered in the section on function overloading.

(2) generic functions

  1. basis

Previously, we would add type comments to parameters and return values directly when declaring functions, and pass in values of the corresponding type when calling. A function declared in this form has a fixed type of parameter and return value. Is there any way that we can be flexible about the types of parameters that we pass when we call a function? Generic functions are exactly what we want.

Generic functions: Highly abstract types. Abstract a type (which can be multiple types) when declaring a function: Enclose Angle brackets after the function name with the abstract type name (e.g.

, where T, K, and U are type parameters, each representing a type, which is determined by the type passed in when the function is called. When you call a function, you externalize it, pass in the actual type, and once you pass in the type, everywhere you see the generic type, you replace it with the type you passed in. If no explicit Type is passed in, TS does Type inference and automatically determines the Type of Type. (T, K, U, etc., can be substituted with any word you like, but these letters are simpler.)
,>

// 
      
        is generic, Tpye represents any Type,
      
// When a function is called, the actual Type needs to be passed in. Once the Type is passed in, all occurrences of Type are replaced
function firstElement<Type> (arr: Type[]) :Type | undefined {
  return arr[0];
}
Copy the code

A function can be called with any actual type passed in:

// Type inference determines that Type is string
const s = firstElement(["a"."b"."c"]);
// Type inference determines that Type is number
const n = firstElement([1.2.3]);
// Type inference determines that Type is undefined
const u = firstElement([]);
Copy the code

The concept of generics abstracts types so that functions can be called with the required type, thus increasing the versatility of functions. The name Type of a generic Type is optional. Note that the same generic Type represents the same Type.

  1. Generic constraint

As we know, generics can define multiple types, such as

, each generic type represents a type, which can be the same or different, depending on the type passed in when the function is called. However, the generics we have defined so far have been independent of other types. Many times, we want to constrain generics to be only one of a certain type. In this case, you can use the extends keyword to implement generic constraints.
,>

interface Person {
  name: string.age: number
}
// The generic T inherits the Person type, so T must have name and age attributes
function getInfo<T extends Person> (user: T) :string {
  return user.name
}

const user1 = {age: 16}
const user2 = {name: 'cc'.age: 18.gender: 1}
// Error: user1 does not have a name attribute
getInfo(user1)
// ok
getInfo(user2)
Copy the code
  1. Specify type parameters

In the previous examples, we did not manually pass in the type to specify the actual type of the generic. Instead, TS automatically inferred the type. For all we know, TS is smart. Sometimes, however, generics are so abstract that the type inference of TS alone may not yield the correct result. At this point, we can specify type parameters by manually passing in the type when calling the function. After all, we always know more than TS. Here’s an official example:

function combine<Type> (arr1: Type[], arr2: Type[]) :Type[] {
  return arr1.concat(arr2);
}

[1,2,3]; // error: Type = number;
// The second string array fails the Type check because Type[] should be number[]
combine([1.2.3], ['a'.'b'.'c'])
Copy the code

In this case, you need to specify the parameter type:

const arr = combine<string | number> ([1.2.3], ["hello"]);
Copy the code
  1. Three little details to write the generic function
  • Use generic constraints as little as possible and let TS do type inference

  • Use type parameters as little as possible

  • Do not take unreused types as type parameters

(3) function overload

  1. Optional arguments to the function

In the previous section on type reduction, we learned that functions can have optional arguments, and that if a function is called without passing a value to the optional argument, the value of that argument is undefined, which can cause unexpected errors. In functions, we can solve this problem by checking the truth value, or by giving the parameter a default value (same as JS). However, if a function has a callback function as an argument, and the callback function also has optional arguments, it is especially prone to errors. Be lazy and carry on with the official chestnuts:

function myForEach(arr: any[], callback: (arg: any, index? :number) = >void) {
  for (let i = 0; i < arr.length; i++) {
    // If no index argument is passed when callback is called, index is undefined
    callback(arr[i]);
  }
}

myForEach([1.2.3].(a, i) = > {
  // undefined, undefined has no toFixed method, so an error will be reported
  console.log(i.toFixed());
});
Copy the code

As you can see, using optional parameters is not only a bit cumbersome to handle, but also error-prone. Therefore, when a function has an indefinite number of or different types of arguments, it is better to overload the function.

  1. Function overloading

It is the overload signature that specifies the function’s shape to participate in the return value. There can be multiple overload signatures.

It is the implementation signature that is compatible with multiple overload signatures and performs logical processing. Since multiple sets of overload signatures are compatible, optional parameters will appear.

We can write multiple sets of overloaded signatures to specify how functions are called differently (with different numbers or types of arguments and different types of return values). Then implement the signature for compatible logical processing.

// Define two sets of overloaded signatures
// Allows a function to be called with only the name argument
function setUserInfo(name: string) :boolean;
// Allow the function to be called with name, age, gender
function setUserInfo(name:string, age:number, gender: 1 | 2) :string;
// Implement signature, unified processing logic
function setUserInfo(name:string, age? :number, gender? :1 | 2){
  // True check, because of two sets of overloaded signature rules, the function is called with either three arguments
  // Therefore, passing in age must also pass in gender
  if(age){
    return I call `${name}This year,${age}Years old! `
  }else{
    return false}}// Pass in an argument, correct
setUserInfo('cc')
// Pass in three arguments, correct
setUserInfo('cc'.18.2)
// An error was reported when two parameters were passed because an overloaded signature for both parameters was not defined
setUserInfo('cc'.18)
Copy the code

As you can see, the implementation of the signature is very similar to the usual optional arguments we used before, but the difference is obvious: although age and gender are both optional arguments, overloading the signature dictates that age and gender must be passed at the same time or not at the same time, which dictates that the call to the function can pass only one or three arguments. Without function overloading, there is one more case to deal with where only name and age are passed in. It can be seen that function overloading can make the logic and structure clearer and more elegant by specifying the different ways in which functions are called. When we do function overloading, we must make sure that the implementation signature is compatible with all overloaded signatures (both arguments and return values are handled in a compatible manner).

Declare this in a function

In general, TS will automatically infer the direction of this, just as JS does. JS does not allow this as an argument, but TS does allow us to declare the type of this in functions, especially in function callback arguments.

// filterUser method, followed by their calling signature
interface Data {
  filterUsers(filter: (this: User) = > boolean): User[];
}
Copy the code

At first I looked at the official example for a few minutes but found that its filterUsers was the call signature of a function, ੯ੁૂ · ̀ u\. This declares that this is of type User. If this is not of type User in the callback when the method is executed, TS will tell us that the code was written incorrectly. One thing to note when declaring this in a function is that, although callback uses the arrow form in the construction signature, when we actually call the method, callback cannot use the arrow function, only the function keyword. After all, as we all know, arrow functions don’t have their own scoped this; it uses the same this as the context in which the arrow function is defined.

(5) Other types

  • void

    The function returns a null value if the return value is set to void. Void is not the same as undefined.

    A function that returns a void does not have to be unable to write a return statement. If the function type is defined by function expression, function signature, etc., the instance function body of the type can have a return statement, and can be followed by any type of value, but its return value is ignored. If we assign the result of such a function call to a variable, the variable will still be of type void.

    type voidFunc = () = > void;
    
    const f1: voidFunc = () = > {
      // Can return any type of value, but is ignored
      return true;
    };
    
    // v1 is still of type void
    const v1 = f1();
    Copy the code

    However, if a function is declared to return void via a literal, there can be no return statement in the function body. Although it says so in the official documentation, and the chestnut below is taken from the official documentation, my VS Code editor does not report an error. .

    function f2() :void {
      // @ts-expect-error
      return true;
    }
     
    const f3 = function () :void {
      // @ts-expect-error
      return true;
    };
    Copy the code
  • object

    Lowercase object, not uppercase object. These are different things.

  • unknown

  • never

    Some functions never return a value, such as throwing an error before a return inside a function. The never type is also commonly used for exhaustive checking.

  • Funtion

    These types are already in the “# Typescript” series in 2022. – The Nuggets have already been introduced and will not be repeated here.

(6) Remaining parameters

  • Parameters denote parameters and arguments denote arguments.

  • The remaining parameters

The rest of the parameters are used in the same way as JS.

// The first argument is a multiple, which returns an array of all subsequent arguments multiplied by their respective multiples
function multiply(n: number. m:number[]) {
  return m.map((x) = > n * x);
}
/ / a value
const a = multiply(10.1.2.3.4);
Copy the code
  • The remaining arguments

Residual arguments are often used in function calls to expand passed arguments (arrays, objects, etc.), but this is easy to stomp on. Take arrays as an example:

const arr1 = [1.2.3];
const arr2 = [4.5.6]; arr1.push(... arr2);Copy the code

The array push can accept an unlimited number of arguments, so you can expand the argument arr2 directly. But some methods can only take a specified number of arguments, and in general, TS considers array values to be mutable. If you expand the array parameters of a method like this, an error will be reported, because TS will assume that the number of members in the array may be zero or more, which does not meet the requirement that the method only accept a specified number of parameters.

// Although the array now has only two members, its type is inferred to be number[],
// That is, the args array may change and may have zero or more parameters
The math.atan2 method accepts only two arguments, so an error is reported
const args = [8.5];
const angle = Math.atan2(... args); s);Copy the code

The solution is simply to assert the array type as immutable, using as const. The array is then inferred to be of tuple type. Tuple types will be covered in the next object Types article.

// The length of args is immutable and inferred to be of tuple type
const args = [8.5] as const;
// ok
const angle = Math.atan2(... args);Copy the code
  • Parameter structure

Nothing to say, go straight to the official example.

type NumberABC = { a: number; b: number; c: number };
function sum({ a, b, c }: NumberABC) {
  console.log(a + b + c);
}
Copy the code