官术网_书友最值得收藏!

Functions, lambdas, and execution flow

Functions are the processing machines we used to analyze input, digest information, and apply the necessary transformations to data that's provided either to transform the state of our application or to return an output that will be used to shape our application's business logic or user interactivity.

Functions in TypeScript are not that different from regular JavaScript, except for the fact that, just like everything else in TypeScript, they can be annotated with static types. Thus, they improve the compiler by providing it with the information it expects in their signature and the data type it aims to return, if any.

Annotating types in our functions

The following example showcases how a regular function is annotated in TypeScript:

function sayHello(name: string): string {

    return 'Hello, ' + name;

}

We can see two main differences from the usual function syntax in regular JavaScript. First, we annotate the parameters declared in the function signature, which makes sense since the compiler will want to check whether the data provided holds the correct type. In addition to this, we also annotate the returning value by adding the string type to the function declaration.

As mentioned in the previous section, the TypeScript compiler is smart enough to infer types when no annotation is provided. In this case, the compiler looks into the arguments provided and return statements to infer a returning type from it.

Functions in TypeScript can also be represented as expressions of anonymous functions, where we bind the function declaration to a variable:

const sayHello = function(name: string): string {

    return 'Hello, ' + name;

}  

However, there is a downside to this syntax. Although typing function expressions this way is allowed, thanks to type inference, the compiler is missing the type definition of the declared variable. We might assume that the inferred type of a variable that points to a function typed as a string is a string. Well, it's not. A variable that points to an anonymous function ought to be annotated with a function type:

const sayHello: (name: string) => string = function(name: string): string {

    return 'Hello, ' + name;

}

The function type informs us of the types expected in the function payload and the type returned by the execution of function, if any. This whole block, which is of the form (arguments: type) => returned type, becomes the type annotation our compiler expects.

Function parameters in TypeScript

Due to the type checking performed by the compiler, function parameters require special attention in TypeScript.

Optional parameters

Parameters are a core part of the type checking that's applied by the TypeScript compiler. TypeScript defines that a parameter is optional by adding the ? symbol as a postfix to the parameter name we want to make optional:

function greetMe(name: string, greeting?: string): string {

    if(!greeting) {

        greeting = 'Hello';

    }

    return greeting + ', ' + name;

}

Thus, we can omit the second parameter in the function call:

greetMe('John');

So, an optional parameter is set unless you explicitly do so. It is more of a construct so that you can get help with deciding what parameters are mandatory and which ones are optional. Let's exemplify this:

function add(mandatory: string, optional?: number) {}

You can invoke this function in the following ways:

add('some string');

add('some string', 3.14);

Both versions are valid. Be aware that optional parameters should be placed last in a function signature. Consider the following function:

function add(optional?: number, mandatory: string) {}

This creates a situation where both parameters would be considered mandatory. Let's say you call your function like so:

add(1);

Here, the compiler would complain that you have not provided a value for the mandatory argument. Remember, optional arguments are great, but place them last.

Default parameters

TypeScript gives us another feature to cope with default parameters, where we can set a default value that the parameter assumes when it's not explicitly passed upon executing the function. The syntax is pretty straightforward, as we can see when we refactor the previous example:

function greetMe(name: string, greeting: string = 'Hello'): string {

    return `${greeting}, ${name}`;

}

Just as with optional parameters, default parameters must be put right after the required parameters in the function signature.

Rest parameters

One of the big advantages of the flexibility of JavaScript when defining functions is its ability to accept an unlimited non-declared array of parameters in the form of the arguments object. In a statically typed context such as TypeScript, this might not be possible, but it is actually using the rest parameter's object. We can define, at the end of the arguments list, an additional parameter prefixed by ellipsis ... and typed as an array:

function greetPeople(greeting: string, ...names: string[]): string {

    return greeting + ', ' + names.join(' and ') + '!';

}

So, rest parameters are your friend when you don't know how many arguments you have.

Function overloading

Method and function overloading is a common pattern in other languages, such as C#. However, implementing this functionality in TypeScript clashes with the fact that JavaScript, which TypeScript is meant to compile to, does not implement any elegant way to integrate it out of the box. So, the only workaround possible requires writing function declarations for each of the overloads and then writing a general-purpose function that wraps the actual implementation, whose list of typed arguments and return types are compatible with all the others:

function hello(names: string): string {}

function hello(names: string[]): string {}

function hello(names: any, greeting?: string): string {

    let namesArray: string[];

    if (Array.isArray(names)) {

        namesArray = names;

    } else {

        namesArray = [names];

    }

    if (!greeting) {

        greeting = 'Hello';

    }

    return greeting + ', ' + namesArray.join(' and ') + '!';

}

In the preceding example, we are creating three different function signatures, and each of them features different type annotations. We could even define different return types if there was a case for that. To do so, we should have annotated the wrapping function with any type.

Arrow functions

ES6 introduced the concept of fat arrow functions (also called lambda functions in other languages such as Python, C#, Java, or C++) as a way to simplify the general function syntax and to provide a bulletproof way to handle the scope of the functions. This is something that is traditionally handled by the infamous scope issues of tackling with the this keyword. The first thing we notice is its minimalistic syntax, where, most of the time, we see arrow functions as single-line, anonymous expressions:

const double = x => x * 2;

The function computes the double of a given number x and returns the result, although we do not see any function or return statements in the expression. If the function signature contains more than one argument, we need to wrap them all between braces:

const add = (x, y) => x + y;

Arrow functions can also contain statements. In this case, we want to wrap the whole implementation in curly braces:

const addAndDouble = (x, y) => {

    const sum = x + y;

    return sum * 2;

}

Still, what does this have to do with scope handling? The value of this can point to a different context, depending on where we execute the function. This is a big deal for a language that prides itself on excellent flexibility for functional programming, where patterns such as callbacks are paramount. When referring to this inside a callback, we lose track of the upper context, which usually leads us to using conventions such as assigning its value to a variable named self or that. It is this variable that is used later on within the callback. Statements containing interval or timeout functions make for a perfect example of this:

function delayedGreeting(name): void {

    this.name = name;

    this.greet = function(){

        setTimeout(function() {

            console.log('Hello ' + this.name);

        }, 0);

    }

}

const greeting = new delayedGreeting('John');

greeting.greet();

Executing the preceding script won't give us the expected result of Hello John, but an incomplete string highlighting a pesky greeting to Mr. Undefined! This construction screws the lexical scoping of this when evaluating the function inside the timeout call. Porting this script to arrow functions will do the trick, though:

function delayedGreeting(name): void {

    this.name = name;

    this.greet = function() {

        setTimeout(() => 

            console.log('Hello ' + this.name)

        , 0);

    }

}

Even if we break down the statement contained in the arrow function into several lines of code wrapped by curly braces, the lexical scoping of this keeps pointing to the proper context outside the setTimeout call, allowing for more elegant and clean syntax.

主站蜘蛛池模板: 潢川县| 任丘市| 蕉岭县| 玉林市| 乌兰察布市| 北票市| 新营市| 上虞市| 道孚县| 曲阳县| 清丰县| 长顺县| 阜城县| 丰都县| 六枝特区| 闸北区| 宝兴县| 吉林市| 江口县| 永年县| 桦南县| 莱芜市| 抚顺市| 平阳县| 海南省| 治多县| 青铜峡市| 沭阳县| 义马市| 景德镇市| 江山市| 海安县| 盐山县| 西宁市| 汉阴县| 夹江县| 灵璧县| 德阳市| 宣城市| 安岳县| 廊坊市|