Function definition
Function parameters could be defined using required or optional notations
function withOptionalArgs(a: string, b?: number): void {}
function withOptionalDefaultArgs(a: string, b = 10): void {}
Optional parameters should be in the end of parameters definitions.
Could be used spread operator to capture all parameters into an array
function withPassThruArgs(...other: string[]): void {}
Function can have another function as parameter
function convertToNumber(
data: string,
cb: (error: string | undefined, result?: number) => void
): void {}
Same rules applied to anonymous functions
const withOptionalArgs2 = (a: string, b?: number): void => {};
const withOptionalDefaultArgs2 = (a: string, b = 10): void => {};
const withPassThruArgs2 = (a: string, ...other: string[]): void => {};
Anonymous functions could be explicitly typed
type LoggerFunction = (...params: string[]) => void;
const logFunction: LoggerFunction = (...params: string[]) => {
console.log(...params);
};
const errorFunction: LoggerFunction = (...params: string[]) => {
console.error(...params);
};
Function type can also be defined as interface
This is not popular way, please don’t do this, exists for backwards compatibility with javascript
interface FunctionType {
(a: string): string;
}
const stringInterfaceFunction: FunctionType = (a: string) => a.toLowerCase();
Could be applied destructuring to the function parameters
interface FunctionParams {
a: string;
b: number;
}
const appInParams = (first: number, { a }: FunctionParams): void => {
console.log(first, a);
};
Destructuring with optional parameters and applied defaults
Parameters order doesn’t matter in this case. Optional default is not necessary at last position
interface PartialFunctionOptionalObject {
b: number;
c?: string;
d?: string;
}
const partialParamsOptional = (
a: number,
{ b, c = "defined", d }: PartialFunctionOptionalObject
): void => {
console.log(a, b, c, d); // d = string | undefined, c = string
};
Provide default value when it is possible.
const functionWithoutDefault = (name: string, data?: string[]) => {
if (data) {
// 'data' can be used now
data.map((x) => x);
}
// OR this way:
(data || []).map((x) => x);
};
const functionWithDefault = (name: string, data: string[] = []) => {
// 'data' can be used now
};
Async functions
As a good practice is to use await
inside of async
function otherwise it is just a function returning Promise
.
This is ensured by eslint
rules, typescript itself allows async
function without await
const mainAsync = async (a: number): Promise<number> => {
return await Promise.resolve(a);
};
const mainPromise = (): Promise<number> => {
return Promise.resolve(1);
};
Using any of these
async function myFunction() {
await mainAsync(1); // async function
await mainPromise(); // Promise function
}
In Node.js you could call async
function on top level and runtime will internally wait it to be resolved
myFunction();
Curried functions
Functions returning functions.
They used to keep some context till later time to execute action within that context
type LongCurriedFunction = (a: string) => (code: number) => number;
It is better to define all intermediate types to increase readability
type InternalFunction = (code: number) => number;
type WrapperFunction = (a: string) => InternalFunction;
const wrapperFunction: WrapperFunction = (a: string) => {
console.log("wrapper", a);
return (justCode: number) => {
console.log("internal function", a, justCode);
return justCode + 1;
};
};
Call one then second
const partialFunction = wrapperFunction("test"); // => InternalFunction
partialFunction(200); // => 201
Call both same time. Pretty useless scenario
wrapperFunction("test")(200); // => 201
Short notation
Can be used when function have only one operation which gives result and it could be immediately returned
Effectively it removes
{}
andreturn
const shortNotation = (a: string, b: number) => `${a} + ${b}`;
const shortCurried = (a: string) => (b: number) => (c: string) =>
`${a} -> ${b} -> ${c}`;
Return an object as only one operation in function
const addAndReturn = (a: string, b: string) => ({
sum: a + b,
});
Here, extra ()
is required because {}
in function definition used to define function body.
Here are same function definitions: add1, add2
function add1(a: number, b: number) {
return a + b;
}
const add2 = (a: number, b: number) => a + b;
add1(1, 2);
add2(1, 2);
“this” capturing
Main difference between function
and const
function definitions is the way how they works with this
In this sample set
and setClassical
doing the same thing, but are different
because of definitions used
function practiceThisWrapper() {
// Curried function type
type Setter = (a: string) => (a: string) => void;
// Object type definition in needed for `this` usage inside of it's functions
interface CapturingObject {
data: Record<string, string>;
set: Setter;
setClassical: Setter;
}
const thisCapturingObject: CapturingObject = {
data: {},
set: function (key: string) {
// Next arrow function is capturing `this` as current scope
// classical `function` will not work when using `this` inside
return (value: string) => {
this.data[key] = value;
};
},
setClassical: function (key: string) {
// keep `this` for the `function`
const self = this;
return function (value: string) {
self.data[key] = value;
};
},
// It is wrong definition:
// `this` will be referencing scope of `thisPracticeWrapper`
// setShort: (key: string) => (value: string) => {
// this.data[key] = value;
// },
};
thisCapturingObject.set("data")("value");
thisCapturingObject.setClassical("data2")("value2");
console.log(thisCapturingObject.data);
// { data: 'value', data2: 'value2' }
}
practiceThisWrapper();
As a recommendation here is to use builder to create the object.
This will not require to use this
and will be more transparent
function practiceThisBuilderWrapper() {
// Curried function type
type Setter = (a: string) => (a: string) => void;
// Object type definition in needed for `this` usage inside of it's functions
interface CapturingObject {
data: Record<string, string>;
set: Setter;
setClassical: Setter;
}
const buildCapturingObject = (): CapturingObject => {
const data: Record<string, string> = {};
// both are `arrow function` definitions
const set = (key: string) => {
return (value: string) => {
data[key] = value;
};
};
// both are `function` definitions
function setClassical(key: string) {
return function (value: string) {
data[key] = value;
};
}
return {
data,
set,
setClassical,
};
};
const thisCapturingObject = buildCapturingObject();
thisCapturingObject.set("data")("value");
thisCapturingObject.setClassical("data2")("value2");
console.log(thisCapturingObject.data);
// { data: 'value', data2: 'value2' }
}
practiceThisBuilderWrapper();