Preface
The official documentation of TypeScript has long been updated, but the Chinese documents I can find are still in the older version. Therefore, some new and revised chapters have been translated and sorted out.
This article is organized from the " More on Functions " chapter in the TypeScript Handbook.
This article does not strictly follow the original translation, but also explains and supplements part of the content.
text
A function is a fundamental part of any application, whether it is a local function, a function imported from another module, or a method in a class. Of course, functions are also values, and like other values, TypeScript has many ways to describe how functions can be called. Let's learn how to write description function types (types).
Function Type Expressions
The simplest way to describe a function is to use function type expression. is written somewhat like an arrow function:
function greeter(fn: (a: string) => void) {
fn("Hello, World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
The syntax (a: string) => void
indicates that a function has a a
type is a string. This function does not return any value.
If the type of a function parameter is not explicitly given, it will be implicitly set to any
.
Note that the name of the function parameter is required. This function type description(string) => void
means that a function has a parameter ofany
namedstring
Of course, we can also use a type alias to define a function type:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
Call Signatures
In JavaScript, in addition to being called, functions can also have attribute values themselves. However, the function type expression mentioned in the previous section does not support declaring attributes. If we want to describe a function with attributes, we can write a call signature in an object type.
type DescribableFunction = {
description: string;
(someArg: number): boolean;
};
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
Note that this syntax is slightly different from function type expressions. Between the parameter list and the returned type, :
is used instead of =>
.
Construct Signatures
JavaScript functions can also be new
operator. When called, TypeScript will consider this to be a constructor because they will generate a new object. You can write a construction signature by adding a new
keyword in front of the call signature:
type SomeConstructor = {
new (s: string): SomeObject;
};
function fn(ctor: SomeConstructor) {
return new ctor("hello");
}
Some objects, such as the Date
object, can be called directly or by using the new
operator, and you can merge the call signature and the construction signature together:
interface CallOrConstruct {
new (s: string): Date;
(n?: number): number;
}
Generic Functions
We often need to write this kind of function, that is, the output type of the function depends on the input type of the function, or the types of two inputs are related to each other in some form. Let us consider a function that returns the first element of an array:
function firstElement(arr: any[]) {
return arr[0];
}
Note that the type of the return value of the function at this time is any
. It would be better if it could return the specific type of the first element.
In TypeScript, generics are used to describe the correspondence between two values. We need to declare the function signature in a type parameter (of the type the Parameter) :
function firstElement<Type>(arr: Type[]): Type | undefined {
return arr[0];
}
By adding a type parameter Type
to the function and using it in two places, we create an association between the input of the function (that is, the array) and the output of the function (that is, the return value). Now when we call it, a more specific type will be determined:
// s is of type 'string'
const s = firstElement(["a", "b", "c"]);
// n is of type 'number'
const n = firstElement([1, 2, 3]);
// u is of type undefined
const u = firstElement([]);
Inference
Note that in the above example, we did not explicitly specify Type
, the type is automatically inferred by TypeScript.
We can also use multiple type parameters, for example:
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
return arr.map(func);
}
// Parameter 'n' is of type 'string'
// 'parsed' is of type 'number[]'
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
Note that in this example, TypeScript can not only infer the type of Input (from the string
array passed in), but also infer the type Output
based on the return value of the function expression.
Constraints
Sometimes, we want to associate two values, but we can only manipulate some fixed fields of the value. In this case, we can use the constraint (constraint) to restrict the type parameters.
Let's write a function that returns the longer of the two values. To this end, we need to ensure that there is a value passed number
type of length
property. We use extends
syntax to constrain function parameters:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'.
TypeScript will infer longest
, so the type inference of the return value is also applicable in generic functions.
It is precisely because we have Type
a { length: number }
restriction on 061d45cdba7fb5, we can be allowed to obtain the .length
attribute of the a
b
Without this type constraint, we can't even get these attributes, because these values may be other types, and there is no length attribute.
Based on the parameters passed in, longerArray
and longerString
have been inferred. Remember, the so-called generic is to associate two or more values with the same type.
Working with Constrained Values
This is a common error when using generic constraints:
function minimumLength<Type extends { length: number }>(
obj: Type,
minimum: number
): Type {
if (obj.length >= minimum) {
return obj;
} else {
return { length: minimum };
// Type '{ length: number; }' is not assignable to type 'Type'.
// '{ length: number; }' is assignable to the constraint of type 'Type', but 'Type' could be instantiated with a different subtype of constraint '{ length: number; }'.
}
}
This function looks like there is no problem. Type
is { length: number}
, and the function returns Type
or a value that meets the constraint.
The problem is that the function should return an object of the same type as the passed parameter, not just an object that meets the constraints. We can write such a counterexample:
// 'arr' gets value { length: 6 }
const arr = minimumLength([1, 2, 3], 6);
// and crashes here because arrays have
// a 'slice' method, but not the returned object!
console.log(arr.slice(0));
Specifying Type Arguments
TypeScript can usually automatically infer the type parameters passed in in a generic call, but it can't always infer. For example, there is a function that merges two arrays:
function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
return arr1.concat(arr2);
}
If you call the function like this, an error will occur:
const arr = combine([1, 2, 3], ["hello"]);
// Type 'string' is not assignable to type 'number'.
And if you insist on doing this, you can manually specify Type
:
const arr = combine<string | number>([1, 2, 3], ["hello"]);
Some suggestions for writing a good generic function
Although writing generic functions is very interesting, it is also easy to overturn. If you use too many type parameters, or use some unneeded constraints, it may lead to incorrect type inference.
Push Type Parameters Down
The following two functions are written very similarly:
function firstElement1<Type>(arr: Type[]) {
return arr[0];
}
function firstElement2<Type extends any[]>(arr: Type) {
return arr[0];
}
// a: number (good)
const a = firstElement1([1, 2, 3]);
// b: any (bad)
const b = firstElement2([1, 2, 3]);
At first glance, the two functions are too similar, but the first function is written much better than the second function. The first function can infer that the return type is number
, but the second function infers the return type is any
, because TypeScript has to use the constraint type to infer the arr[0]
expression instead of waiting until the function is called to infer this element.
push down
in the original text of this section, in "Refactoring", there is an optimization method of Push Down Method, which means that if a function in the super class is only related to one or a few subclasses, Then it's best to remove it from the superclass and put it in the subclasses that really care about it. That is, only the shared behaviors are reserved in the super class. This method of copying the function ontology in the superclass to the subclass that is specifically needed can be called "push down", which is similar to push down
extend any[]
this section and giving its specific inference to Type
itself.
Rule: If possible, use the type parameter directly instead of constraining it
Use Fewer Type Parameters
Here is another pair of functions that look very similar:
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}
function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}
We created a type parameter Func
because it means that the caller has to manually specify an additional type parameter for no reason. Func
did nothing but made the function more difficult to read and infer.
Rule: Use as few type parameters as possible
Type parameters should appear twice (Type Parameters Should Appear Twice)
Sometimes we forget that a function does not need generics
function greet<Str extends string>(s: Str) {
console.log("Hello, " + s);
}
greet("world");
In fact, we can write this function so simply:
function greet(s: string) {
console.log("Hello, " + s);
}
Remember: Type parameters are used to associate types between multiple values. If a type parameter appears only once in the function signature, then it is not associated with anything.
Rule: If a type parameter only appears in one place, it is strongly recommended that you reconsider whether you really need it
Optional Parameters
Function in JavaScript is often passed a non-fixed number of parameters, for example: number
the toFixed
method to support an optional parameter passed:
function f(n: number) {
console.log(n.toFixed()); // 0 arguments
console.log(n.toFixed(3)); // 1 argument
}
We can use ?
indicate that this parameter is optional:
function f(x?: number) {
// ...
}
f(); // OK
f(10); // OK
Although this parameter is declared as number
type, x
actual type number | undefiend
, because this is not specified in the JavaScript function parameters will be assigned undefined
.
Of course, you can also provide a parameter default value:
function f(x = 10) {
// ...
}
Now f
function body, x
type of number
, because any undefined
parameters are replaced with 10
. Note that when a parameter is optional, you can still pass in undefined
when calling:
declare function f(x?: number): void;
// cut
// All OK
f();
f(10);
f(undefined);
Optional Parameters in Callbacks
After you have studied optional parameters and function type expressions, you can easily make the following mistakes in functions that include callback functions:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
With index?
as an optional parameter, the intention is to hope that the following calls are legal:
myForEach([1, 2, 3], (a) => console.log(a));
myForEach([1, 2, 3], (a, i) => console.log(a, i));
But TypeScript does not think so. TypeScript thinks that what it wants to express is that the callback function may only be passed in one parameter. In other words, the myForEach
function may also be like this:
function myForEach(arr: any[], callback: (arg: any, index?: number) => void) {
for (let i = 0; i < arr.length; i++) {
// I don't feel like providing the index today
callback(arr[i]);
}
}
TypeScript will understand this meaning and report an error, although this error is actually impossible:
myForEach([1, 2, 3], (a, i) => {
console.log(i.toFixed());
// Object is possibly 'undefined'.
});
How to modify it? In fact, it is possible to not set it as an optional parameter:
function myForEach(arr: any[], callback: (arg: any, index: number) => void) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
myForEach([1, 2, 3], (a, i) => {
console.log(a);
});
In JavaScript, if you call a function and pass in more parameters than needed, the extra parameters will be ignored. TypeScript does the same thing.
When you write the type of a callback function, do not write an optional parameter, unless you really plan to call the function without passing in the actual parameter
Function Overloads
Some JavaScript functions can be called with different numbers and types of parameters. for example. You can write a function that returns a date type Date
, this function accepts a timestamp (one parameter) or a month/day/year format (three parameters).
In TypeScript, we can describe different calling methods of a function by writing overlaod signatures. We need to write some function signatures (usually two or more), and then write the content of the function body:
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
if (d !== undefined && y !== undefined) {
return new Date(y, mOrTimestamp, d);
} else {
return new Date(mOrTimestamp);
}
}
const d1 = makeDate(12345678);
const d2 = makeDate(5, 5, 5);
const d3 = makeDate(1, 3);
// No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
In this example, we wrote two function overloads, one accepting one parameter and the other accepting three parameters. The first two function signatures are called overload signatures.
Then, we wrote a signature-compatible function implementation, which we call implementation signature, but this signature cannot be called directly. Although we declare two optional parameters after a mandatory parameter in the function declaration, it still cannot be called by passing in two parameters.
Overload Signatures and the Implementation Signature (Overload Signatures and the Implementation Signature)
This is a common confusion. People often write code like this, but don't understand why they report an error:
function fn(x: string): void;
function fn() {
// ...
}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.
Again, the signature written into the function body is "invisible" to the outside, which means that the outside world "cannot see" its signature, and naturally cannot be called in the way that the signature is implemented.
The implementation signature is invisible to the outside world. When writing an overloaded function, you should always need two or more signatures on top of the implementation signature.
And the implementation signature must be compatible with the overload signature. For example, the reason why these functions report an error is because their implementation signature does not match the overload signature correctly.
function fn(x: boolean): void;
// Argument type isn't right
function fn(x: string): void;
// This overload signature is not compatible with its implementation signature.
function fn(x: boolean) {}
function fn(x: string): string;
// Return type isn't right
function fn(x: number): boolean;
This overload signature is not compatible with its implementation signature.
function fn(x: string | number) {
return "oops";
}
Some suggestions for writing a good function overload
Just like generics, there are some suggestions for you. Following these principles can make your functions easier to call and understand.
Let us imagine a function that returns the length of an array or string:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
This function code function is implemented, and there is no error, but we can't pass in a value that may be a string or an array, because TypeScript can only handle one function call with one function overload at a time.
len(""); // OK
len([0]); // OK
len(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
Overload 1 of 2, '(s: string): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(arr: any[]): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'any[]'.
Type 'string' is not assignable to type 'any[]'.
Because both function overloads have the same number of parameters and the same return type, we can write a non-overloaded version of the function instead:
function len(x: any[] | string) {
return x.length;
}
In this way, the function can be passed in either of the two types.
Use union types as much as possible instead of overloading
Declare this in the function (Declaring this
in a Function)
this
in the function through the code flow, for example:
const user = {
id: 123,
admin: false,
becomeAdmin: function () {
this.admin = true;
},
};
TypeScript able to understand the function user.becomeAdmin
in this
points to the outer layer of the object user
, which has to cope with many situations, but there are some situations you need to explicitly tell TypeScript this
what is in the end it represents.
In JavaScript, this
is a reserved word, so it cannot be used as a parameter. But TypeScript allows you to declare the type of this
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(function (this: User) {
return this.admin;
});
This writing method is somewhat similar to the callback style API. Note that you need to use function
instead of arrow functions:
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
const admins = db.filterUsers(() => this.admin);
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.
Other Types to Know About
Here are some types that will often appear. Like other types, you can use them anywhere, but they are often used in conjunction with functions.
void
void
indicates that a function does not return any value. This type should be used when the function does not return any value or cannot return a clear value.
// The inferred return type is void
function noop() {
return;
}
In JavaScript, a function does not return any value, it implicitly returns undefined
, but void
and undefined
are different in TypeScript. There will be a more detailed introduction at the end of this article.
void is not the same as undefined
object
This special type object
can represent any value that is not primitive ( string
, number
, bigint
, boolean
, symbol
, null
, undefined
). object
different from the empty object type { }
, and also different from the global type Object
. Object
you won't use 061d45cdba87d9 either.
object is different fromObject
, always useobject
!
Note that in JavaScript, functions are objects, they can have attributes, and there are Object.prototype
and instanceof Object
in their prototype chain. You can use Object.keys
and so on for functions. For these reasons, the function is also considered object
in TypeScript.
unknown
unknown
type can represent any value. A bit similar to any
, but more secure, because it is illegal to do anything with a value of type unknown
function f1(a: any) {
a.b(); // OK
}
function f2(a: unknown) {
a.b();
// Object is of type 'unknown'.
}
Sometimes it's quite useful to describe the function type. You can describe a function that can accept any value, but the value of type any
You can describe a function that returns a value of unknown type, such as:
function safeParse(s: string): unknown {
return JSON.parse(s);
}
// Need to be careful with 'obj'!
const obj = safeParse(someRandomString);
never
Some functions never return a value:
function fail(msg: string): never {
throw new Error(msg);
}
never
type indicates that a value will no longer be observed (observed).
As a return type, it means that this function will throw an exception or end the execution of the program.
When TypeScript determines that there is no possible type in the union type, the never
type will also appear:
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
Function
In JavaScript, the global type Function
describes bind
, call
, apply
, and all other function values.
It also has a special property, that is Function
can always be called, and the result will return type any
function doSomething(f: Function) {
f(1, 2, 3);
}
This is an untyped function call, which is best avoided because it returns an unsafe type any
If you are going to accept a black box function, but don't plan to call it, () => void
will be safer.
Rest Parameters and Arguments
parameters
and arguments
arguments
and parameters
can represent the parameters of the function. Because of the specific distinction in this section, we define parameters
represent the name set when we define the function, which is the formal parameter, and arguments
represent the parameter that we actually pass in the function, which is the actual parameter.
Rest Parameters
In addition to using optional parameters and overloading to allow the function to receive a different number of function parameters, we can also define a function that can pass in an unlimited number of function parameters by using the rest parameters syntax (rest parameters):
The remaining parameters must be placed at the end of all parameters, and use the ...
syntax:
function multiply(n: number, ...m: number[]) {
return m.map((x) => n * x);
}
// 'a' gets value [10, 20, 30, 40]
const a = multiply(10, 1, 2, 3, 4);
In TypeScript, the type of the remaining parameters will be implicitly set to any[]
instead of any
. If you want to set a specific type, it must be in Array<T>
or T[]
, or the tuple type.
Rest Arguments
We can use an ...
syntax to provide a variable number of actual parameters to the function. For example, the push
method of the array can accept any number of arguments:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(...arr2);
Note that in general, TypeScript does not assume that arrays are immutable, which can lead to some unexpected behaviors:
// 类型被推断为 number[] -- "an array with zero or more numbers",
// not specifically two numbers
const args = [8, 5];
const angle = Math.atan2(...args);
// A spread argument must either have a tuple type or be passed to a rest parameter.
Fixing this problem requires you to write a little code. Generally speaking, using as const
is the most direct and effective solution:
// Inferred as 2-length tuple
const args = [8, 5] as const;
// OK
const angle = Math.atan2(...args);
as const
it into a read-only tuple through the 061d45cdba8abe syntax.
Note that when you want to run in an older environment, using the remaining parameter syntax may require you to turn on [downlevelIteration](https://www.typescriptlang.org/tsconfig#downlevelIteration)
to convert the code to the old version of JavaScript.
Parameter Destructuring
You can use parameter deconstruction to conveniently deconstruct the object provided as a parameter into one or more local variables in the function body. In JavaScript, it looks like this:
function sum({ a, b, c }) {
console.log(a + b + c);
}
sum({ a: 10, b: 3, c: 9 });
After deconstructing the grammar, write the type annotation of the object:
function sum({ a, b, c }: { a: number; b: number; c: number }) {
console.log(a + b + c);
}
This seems a bit cumbersome, you can also write like this:
// 跟上面是有一样的
type ABC = { a: number; b: number; c: number };
function sum({ a, b, c }: ABC) {
console.log(a + b + c);
}
Assignability of Functions (Assignability of Functions)
Returns void
The function has a void
, which will produce some unexpected and reasonable behavior.
When Contextual Typing deduces the return type as void
, it does not force the function to not return content. In other words, if such a return void
type of function type (type vf = () => void)
,
When applied, it can return any value, but the returned value will be ignored.
Therefore, the following () => void
type are all valid:
type voidFunc = () => void;
const f1: voidFunc = () => {
return true;
};
const f2: voidFunc = () => true;
const f3: voidFunc = function () {
return true;
};
And even if the return values of these functions are assigned to other variables, they will maintain the void
type:
const v1 = f1();
const v2 = f2();
const v3 = f3();
It is precisely because of this feature that the following code will be effective:
const src = [1, 2, 3];
const dst = [0];
src.forEach((el) => dst.push(el));
Although Array.prototype.push
returns a number, and the Array.prototype.forEach
method expects a void
, this code still reports no error. It is because based on the contextual deduction that the return type of the forEach function is deduced to be void. It is precisely because the function is not mandatory that the function must not return content, so the above return dst.push(el)
will not report an error.
In addition, there is a special example that needs to be noted. When a function literal definition returns a void
, the function must not return anything:
function f2(): void {
// @ts-expect-error
return true;
}
const f3 = function (): void {
// @ts-expect-error
return true;
};
TypeScript series
The TypeScript series of articles consists of three parts: official document translation, important and difficult analysis, and practical skills. It covers entry, advanced, and actual combat. It aims to provide you with a systematic learning TS tutorial. The entire series is expected to be about 40 articles. Click here to browse the full series of articles, and suggest to bookmark the site by the way.
WeChat: "mqyqingfeng", add me to the only reader group in Kongyu.
If there are mistakes or not rigorous, please correct me, thank you very much. If you like or have some inspiration, star is welcome, which is also an encouragement to the author.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。