Reference: The TypeScript Handbook
- The more statically typed our program is, the more validation and tooling we'll get, meaning less bugs.
npm install -g typescript
File name: hello.ts
// Greets the world.
console.log("Hello world!");
Run command:
tsc hello.ts
If the above command fails, try: tsc.cmd hello.ts
This command:
- does not executes the code but instead converts it into a
JavaScript
file. - does the compile time checking of the code, including arguments passed to the method.
However, even if there are mistakes (errors) that typechecker reports, the TypeScript would still go ahead and create .js
file for us.
In order to avoid this from happening, we can apply noEmitOnError
flag as shown:
tsc --noEmitOnError hello.ts
Adding type annotations on person
and date
to describe explicitly what greet
can be called with.
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
On compiling the above code, the TypeScript compiler converts to the following JavaScript code:
"using strict";
function greet(person, date) {
console.log("Hello ".concat(person, ", today is ").concat(date.toDateString(), "!"));
}
person
anddate
parameters no longer have type annotations.- The template string (within backticks character ` ) was converted to plain strings with annotations.
Typescript has the ability to rewrite code from newer versions of ECMAScript
to older versions such as ECMAScript3
or ECMAScript5
(a.k.a. ES3
and ES5
).
By default, TypeScript targets ES3
, an extremely old version of ECMAScript, we can use specific flag to target a specific version as shown:
By running the following command:
tsc --target es2015 hello.ts
We get the below JavaScript code:
function greet(person, date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
greet("Maddison", Date()); // Error, Date() returns a string
greet("Maddison", new Date()); // new Date() returns Date type
NOTE:
- Type annotations are not part of JavaScript (or ECMAScript), so browsers cannot understand those.
- It's best not to add annotations when the type system would end up inferring the same type anyway.
- While the default target is ES3, the great majority of current browsers support ES2015.
REMEMBER: Type annotations never change the runtime behavior of your program.
TS has serveral type-checking strictness flags that can be turned on or off. The strict flag in the CLI, or "strict": true
in a tsconfig.json file toggles them all simultaneously, but we can opt out of them indivisually
- TS, in some places, doesn't try to infer any type, instead it falls back to
any
. (Plain JS experience). - Turning this on, will give error on any variable whose type is implicitly referred as
any
.
- By default
null
andundefined
are assignable to any other type, but failure to handle this can cause countless bugs. - Turning this on, makes handling
null
andundefined
more explicit.
string
number
(general forint
orfloat
type)boolean
Use typeOf
operator to refer to the type.
number[]
(orArray<number>
)string[]
- Generic Type
T<U>
In case, we don't want a particular value to case typechecking errors.
On declaring a variable using const
, var
, or let
, we can optionally add a type annotation to explicitly specify the type of the variable:
let myName: string = "Alice";
However, not needed as TypeScript has automatic type inference.
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
Usually, not required because return type is automatically inferred based on return
statements.
function getFavoriteNumber(): number {
return 26;
}
const names = ["Alice", "Bob", "Eve"]
In the above code snippet, names
have been inferred as string[]
names.forEach(function (name) {
console.log(name.toUpperCase());
});
We can also re-write the same thing using arrow functions, as shown below:
names.forEach((name) => {
console.log(name.toUpperCase());
});
Even though, the parameter name
didn't have a type annotation, TypeScript used the types of the forEach
function, along with the inferred type of the array to determine the type name
will have.
This is called contextual typing because the context that the function occured within informs what type it should have.
To define an object type, simply list its properties and their types. For example, here's a function that takes a point-like object:
function printCoord(pt: {
x: number; // can use either ',' or ';' as separator, if we don't specify the type, `any` is assumed.
y: number
}) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
To call the method:
printCoord({ x: 3, y: 7 });
These have ?
appended after their name. But if we access such a property that may not exist, we'll get the value undefined
rather than runtime error. This is why we must add undefined
check before using it.
function printName(obj: {
first: string,
last?: string // optional property
}) {
if(obj.last !== undefined) {
// do something with obj.last
console.log(obj.last.toUpperCase());
}
// A safe alternative with modern JavaScript syntax
console.log(obj.last?.toUpperCase());
}
Another example:
type Foo: {
bar?: number // default type: number | undefined
}
function addOne(foo: Foo): number {
if (typeof foo.bar !== 'undefined') { // In this case, TS would have narrowed the type of bar to number.
return foo.bar + 1;
}
throw new Error('bar is undefined');
}
Alternatively:
function addOne(foo: Foo): number {
if(foo.bar) { // BEWARE! 0 is falsy.
return foo.bar + 1;
}
throw new Error('bar is undefined');
}
Further Reading:
- a union type is a type formed from two or more other types, representing values that may be any one of those types.
- referred to as union's members.
Here is a function that can operate on strings or numbers:
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
- TS will not allow any operation that is not valid for every member of the union.
function printId(id: number | string) {
if(typeof id === "string") {
// In this branch, id is of type 'string'
console.log(id.toUpperCase());
} else {
// Here, id is of type 'number'
console.log(id);
}
}
function welcomePeople(x: string[] | string) {
if(Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
If every member in a union has a property in common, you can use that property without narrowing:
function getFirstThree(x: number[] | string) {
// Both arrays and strings have a slice method
return x.slice(0, 3);
}
Almost all the features of an interface are available in type.
Key Difference: A type cannot be re-opened to add new properties vs an interface which is always extendable.
Specifying the type of value, to be returned, that TypeScript may not have any idea.
Example: TS only knows that document.getElementById
returns some kind of HTMLElement
, but if we know that our page will always return an HTMLCanvasElement
then apply type assertion
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
Writing same thing using angle-bracket syntax:
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");
NOTE:
Since, type assertions are removed at compile-time, there is no runtime checking associated with a type assertion. There won't be an exception or null
generated if the type assertion is wrong.
Example of invalid type assertion:
const x = "hello" as number;
Too conservative rules can be by-passed and asserted to any
(or unknown
) as follows:
const a = (expr as any) as T;
Ref: MDN: const
Ref: What does as const keyword mean in TypeScript?
Literal Types are themselves not helpful but when combined with unions, we can build a much more useful value.
function printText(
s: string,
alignment: "left" | "right" | "center") {
// ...
}
printText("Hello", "left"); // OK!
printText("Hi", "centre"); // Wrong.
function compare(
a: string,
b: string): -1 | 0 | 1 {
return a === b ? 0 : a > b ? 1 : -1;
}
types are used to determine both reading and writing behavior.
Consider the below code:
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = {
url: "https://example.com",
method: "GET"
};
// ERROR: Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'
handleRequest(req.url, req.method);
Two ways to work around this:
- Change the inference by adding a type assertion in either location:
/*
* Change 1
*
* It means, I intend for `req.method` to always have
* the literal type "GET" restricing any updates to
* req.method to have any other value.
*/
const req = {
url: "https://example.com",
method: "GET" as "GET"
}
/*
* Change 2:
*
* It means, I know for other reasons that
* `req.method` is supposed to have the value "GET"
*/
handleRequest(req.url, req.method as "GET");
- We can use
as const
to convert the entire object to betype
literals
const req = {
url: "https://example.com",
method: "GET"
} as const;
handleRequest(req.url, req.method); // OK
NOTE:
The as const
suffix acts like const
but for the type system, ensuring that all properties are assiged the literal type instead of a more general version like string
or number
.
In TS, the way these types behave depends whether we have the strictNullChecks
option on.
- This behavior is similar to other programming languages like Java, C#.
- The lack of checking for these values tends to be a major source of bugs thus we should always have this flag on.
In this case, we need to add nullability check and check for undefined
for any optional property and then proceed further. (Recall Narrowing)
- Removing
null
andundefined
from a type without doing any explicit checking. - Writing
!
after any expression is effectively a type assertion that the value isn'tnull
orundefined
.
function liveDangerously(x?: number | null) {
console.log(x!.toFixed()); // No error
}
NOTE:
Using this operator doesn't change the runtime behavior of your code, so it's important to only use !
when we know that the value can't be null
or undefined
.
Ref: /docs/handbook/enums.html
Code snippet showing fundamental usage of enums
enum UserResponse {
No = 0,
Yes = 1
}
function respond(
recipient: string,
message: UserResponse
): void {
// ...
}
respond("Tim Rogers", UserResponse.Yes);
Enums can also be mixed in computed and constant members but the only catch is that:
- either enums without initializers need to be first,
- or, have to come after numeric enums initialized with numeric constant or other constant enum members.
enum E {
A = getSomeValue(),
B // Error, enum member must have initializer.
}
Unlike numeric
enums, string
enums need to be initialized with a string literal.
- String enums do not have auto-incrementing behavior, but they serialize well.
- In debugging, string enums gives us more meaningful and readable value.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
NOTE:
Refrain using Heterogeneous Enums i.e. it is meaningless to mix numeric
and string
enums.
A constant enum expression is a subset of TypeScript
expressions that can be fully evaluated at compile time. A constant enum expression is the one which:
- A literal enum expression (basically a string literal or numeric literal)
- A reference to previously defined constant enum member (which can originate from different enum).
- A paranthesized constant enum expression.
- One of the
+
,-
,~
unary operators applied to constant enum expression. +
,-
,*
,/
,%
,<<
,>>
,>>>
,&
,|
,^
binary operators with constant expression as operands.
NOTE:
We get compile time error for constant enum expressions to be evaluated to NaN
or Infinity
.
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
Enum members also become types as well! For example, we can say that certain members can only have the value of an enum member.
enum Shape {
Circle,
Square
}
interface Circle {
kind: Shape.Circle;
radius: number;
}
interface Square {
kind: Shape.Square;
sideLength: number;
}
let c: Circle = {
kind: Shape.Square, // Error: Shape.Square cannot be assigned to type Shape.Circle
radius: 100
};
With union enums:
- the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself.
- TS can catch bugs where we might be comparing values incorrectly.
enum E {
Foo,
Bar
}
function f(x: E) {
if(x !== E.Foo || x !== E.Bar) { // Error, types have NO OVERLAP
}
}
enum E {
X,
Y,
Z
}
function f(obj: { X: number }) {
return obj.X;
}
f(E); // works since 'E' has a property X.
Use keyof typeof
to get the type that represents all Enum keys as strings.
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG
}
/**
* This is equivalent to:
* type logLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type logLevelStrings = keyOf typeOf LogLevel
function printImportant(key: logLevelStrings, message: string) {
// get the integer assigned to this enum
const num = LogLevel[key];
if(num <= LogLevel.WARN) {
// do somtething.
}
}
In this case, enum
is compiled into an object that stores both:
- forward mapping (
name
->value
) - reverse mapping (
value
->name
)
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
They are useful to avoid generating extra code (when TS is converted to JS).
const enum Direction {
Up,
Down,
Left,
Right
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right
];
The generated JS code will now be:
"use strict"
let directions = [
0 /* Direction.Up */,
1 /* Direction.Down */,
2 /* Direction.Left */,
3 /* Direction.Right */
];
Describe the shape of already existing enum types.
declare enum Enum {
A = 1,
B,
C = 2
}
In modern TS, you may not need an enum when an object with as const
could suffice:
const enum EDirection {
Up,
Down,
Left,
Right
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3
} as const;
function walk(dirIndex: EDirection) {
// ...
}
type DirectionIndexes = typeOf ODirection[keyof typeof ODirection];
function run(dirIndex: DirectionIndexes) {
// ...
}
Creating bigint via BigInt
function:
const oneHundred: bigint = BigInt(100);
Creating bigint via the literal syntax:
const anotherHundred: bigint = 100n;
const firstName = Symbol("name");
const secondName = Symbol("name");
if(firstName === secondName) { // No Overlap error
}
Take a look at the below method:
function padLeft(padding: number | string, input: string): string {
return input.repeat(padding);
}
We get compiler error at the invocation of repeat
method because padding is type number | string
and not number.
We'll now apply narrowing to tell the compiler know the exact type of padding:
function padLeft(padding: number | string, input: string): string {
if(typeof padding === "number") // type-guard
return input.repeat(padding);
return padding + input;
}
In TypeScript checking against a value returned by a typeof
is a type guard. Here is a list of values returned by typeof
:
- "string"
- "number"
- "bigint"
- "boolean"
- "symbol"
- "undefined"
- "object"
- "function"
NOTE:
"arrays"
are essentially"object"
types.typeof
doesn't returnnull
.
In JavaScript, typeof null
returns an "object"
, due to this Bug, we get compiler errors in the case whenever a value is expected to be null.
function printAll(strs: string | string[] | null) {
if(typeof strs === "object") {
// Error! strs is possibly null
for(const s of strs) {
console.log(s);
}
}
// ...
}
In order to handle such issues, we go for the next concept.
In JavaScript, if
statements don't expect their conditionals to always have the type boolean
. It uses the concept of "coerce"ing. Following expressions will "coerce" to false
:
0
NaN
""
(the empty string)0n
(thebigint
version of zero)null
undefined
Any other expression will "coerce" to true
.
- Using Boolean function:
Boolean("hello")
. - Using double-negation (
!!
) operator:!!"hello"
.
Both will return true
.
Now, let us fix our printAll
method:
function printAll(strs: string | string[] | null) {
if(strs && typeof strs === "object") {
for(const s of strs) {
console.log(s);
}
}
// ...
}
Using single-negation (!
) operator, we can filter out any expected falsy values:
function multiplyAll(
values: number[] | undefined,
factor: number): number[] | undefined {
if(!values)
return values;
return values.map(value => value * factor);
}
In the above code, if the variable values
belongs to either undefined
or null
, it will simply return, thereby safeguarding us from getting errors at run-time.
Determines if an object or its prototype chain has a property with the given name (or value). It automatically converts the given value to their corresponding types.
Basically used to check if a given value is an "instance" of any type. Any value constructed with new
keyword can be checked with instanceof
.
The type of any value is governed based on the expression at the right-hand side. In the below code, the type of variable x
will be number | string
:
let x = Math.random() < 0.5 ? 10 : "Hello World";
NOTE:
The only value we can assign to variable x
at any later point in the program is either of number | string
and compiler error will arrive if some other type is assigned. This means the type at the time of declaration of the variable is deemed final.
The analysis of code based on reachability is called Control Flow Analysis. This means that the exact type of any variable at any point in the program is governed with respect to the:
- value assignments
- type-guards
encountered in that flow.
Restricting the type
a variable can have in a particular method.
To define a user-defined type-guard, we simply need to define a function whose return type is a type predicate. In the below code snippet, animal is Fish
is our type predicate.
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim != undefined;
}
Any time, isFish
will be called with some variable, TypeScript will narrow that variable to that specific type.
function doAction(animal : Fish | Bird) {
if(isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
NOTE:
Not only does TypeScript know that within the if
block, animal is of type Fish
. It also knows that within the else block, animal is not of type Fish
, instead of type Bird
.
Another useful application of type predicate is to filter out values of type Fish
from a list of Fish | Bird
:
const zoo: (Fish | Bird)[] = [fish, fish, bird, fish, bird, bird];
const fishes: Fish[] = zoo.filter(isFish);
Lets us write type-safe TypeScript code.
Each type (object) should have a common property that should help TypeScript to distinguish amongs the given types.
interface Circle {
kind: "circle",
radius: number
}
interface Square {
kind: "square",
length: number
}
type Shape = Circle | Square
Now, consider the below code snippet:
function getArea(shape: Shape) {
// Error! radius does not exist on type 'Shape'
return Math.PI * Math.pow(shape.radius, 2);
}
Since, radius
is not a common property in the union type Shape
. TypeScript thinks that shape might be a Square
which does not have property radius
. In order to solve this issue, we narrow down the type using the common property kind
.
function getArea(shape: Shape) {
switch(shape.kind) {
case "circle":
return Math.PI * Math.pow(shape.radius, 2);
case "square":
return Math.pow(shape.length, 2);
}
}
Used to cut down the options of a union, when we know that we have exhausted all the possibilities.
Functions in TypeScript are both functions (as in any programming language) and can be passed as an expression to a method argument.
function greeter(fn: (a: string) => void) {
fn("Hello World");
}
function printToConsole(s: string) {
console.log(s);
}
greeter(printToConsole);
We can also use type aliasing for a function type:
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
In JavaScript, it is possible for functions to have properties as well. We can do so by writing a call signature in object type as shown:
type DescribableFunction = {
description: string,
(arg: number): boolean
}
function doSomething(fn: DescribableFunction) {
console.log(fn.description + " returned " + fn(6));
}
function isGreaterThan3(arg: number): boolean {
return arg > 3;
}
isGreaterThan3.description = "Is greater than 3";
doSomething(isGreaterThan3);
Such methods will initialize the object using the new
keyword and return the created object hence are called constructors.
In TypeScript, generics are used when we want to describe a correspondence between two values. We do this by declaring a type parameter in the function signature:
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
In such a case, the type was automatically inferred by TypeScript.
We can also use multiple type parameters as well as shown:
function map<K, V>(arr: K[], fn: (n: K) => V): V[] {
return arr.map(fn);
}
Limitting the kinds of types that a Generic type parameter can accept. We can do so by using extends
keyword:
function longestArray<T extends { length: number }>(a: T, b: T): T {
if(a.length > b.length)
return a;
return b;
}
The above method restricts that the type of the variable can only be the one which has the property called length
.
Suppose we have a method as shown below:
function combine<T>(a: T[], b: T[]): T[] {
return a.concat(b);
}
The method will work fine if both arguments are of same type. If we want to pass different type arguments, then we can do as follows:
const arr = combine<number | string>([1,2,3], ["ab", "cd"]);
Having too many type parameters or using constraints where they aren’t needed can make inference less successful, frustrating callers of your function.
When possible use the type parameter itself rather than constraining it.
Type parameters are for relating the types of multiple values. If a type parameter is only used once in the function signature, it’s not relating anything. Either as a parameter or as an inferred return type, but should be used twice.
Any parameter can be marked as optional using ?:
operator:
function fun(x?: number) {
//
}
NOTE:
Any unspecified (optional) parameters will by-default accept undefined
(or null
) as well.
This is why in the method fun
, x is actually of type number | undefined
.
Rule: When writing a function type for a callback, never write an optional parameter unless you intend to call the function without passing that argument.
Optional parameters should always be avoided wherever possible.
In TypeScript, we can specify a function that can be called in different ways by writing overload signatures. To do this, write some number of function signatures (usually two or more), followed by the body of the function:
// Declarations
function makeDate(timestamp: number): Date;
function makeDate(day: number, month: number, year: number): Date;
// Definition
function makeDate(dayOrTimestamp: number, month?: number, year?: number): Date {
if(month && year) {
return new Date(dayOrTimestamp, month, year);
} else {
return new Date(dayOrTimestamp);
}
}
const d1 = makeDate(Date.now());
const d2 = makeDate(13, 9, 2023);
// Error!
// Even though we wrote a function with
// two optional parameters after the required one,
// it can’t be called with two parameters!
const d3 = makeDate(Date.now(), 324324);
NOTE: The signature of the implementation is not visible from the outside.
1. When writing an overloaded function, always have two or more overloads above the implementation of the function.
Following code snippets are all invalid:
function fn(x: boolean): void;
function fn(x: string): void;
// Argument Type Incompatibility error
function fn(x: boolean) {
}
function fn(x: string): string;
function fn(x: number): boolean;
// Return Type Incompatibility error
function fn(x: string | number) {
return "oops";
}
Consider the below method:
function len(s: string): number;
function len(arr: any[]): number;
function len(x: any) {
return x.length;
}
len("") // OK
len([0]) // OK
len(Math.random() > 0.5 ? "hello" : [0]); // Error
The problem with this approach is that when len
is called with either a string
or any[]
, the TypeScript understands the argument type. But if the argument type is not known at compile time, i.e. it might either be a string
or an array
, then TypeScript starts complaining. This is because Typescript can only resolve a function call to a single overload.
For such a scenario, union types are always preferred:
// Can now be invoked with either array or string type.
function len(x: len[] | string) {
return x.length;
}
TypeScript automatically infers what this
should be in a function via Code Flow Analysis. Consider the below code snippet:
const user = {
id: 123,
admin: false;
becomeAdmin: function() {
this.admin = true;
}
}
In this case, TypeScript understands that the function user.becomeAdmin
has a corresponding this
which is the outer object user
.
Sometime, we want to want more control over what object this
represents. TypeScript lets us declare the type for this
in the function body.
Additionally, we need to use function
instead of arrow functions to achieve this behavior:
interface DB {
// Declaring the type for this.
filterUsers(filter: (this: User) => boolean): User[];
}
const db = getDB();
// OK!
const admins = db.filterUsers(function (this: User)) {
return this.admin;
}
// Arrow functions are not useful in this case
const admins = db.filterUser(() => this.admin);
- In TypeScript,
void
is the inferred type when a function doesn't have any return statements. - This will return
undefined
in JavaScript butvoid
andundefined
are different for TypeScript.
- Refers to any value that isn't a primitive. - Different from
{}
(empty object type). - Different from
Object
(global type). - In TypeScript, function types are considered to be
object
s.
Similar to but safer than any
.
- Represents values which are never observed.
- If used as a method's return type, would mean that the function throws an exception.
- It also appears when TypeScript infers that all expected
union
types are now exhausted. (if-else / switch case).
Generally best avoided.
A function can accept an unbounded number of arguments. This is done using Rest parameters as shown:
function multiply(n: number, ...m: number[]): number[] {
return m.map((x) => n * x);
}
A Rest Parameter should always be of array type: Array<T>
or T[]
.
We can provide a variable number of arguments from an iterable object. Since, the push
method of array can take unbounded arguments, we can do:
const arr1 = [1,2,3];
const arr2 = [4,5];
arr1.push(...arr2);
However, if a method does not accept a Rest Parameter, then we need to apply as const
suffix to our declared variable to convert it into a tuple
.
// Inferred as a 2-length tuple.
const args = [3,5] as const;
const angle = Math.atan2(...args);
NOTE:
The tuple (Array<T>
) is this case, should have exactly the same number of values as expected in the called method.
Conveniently unpack objects provided as an argument into one or more local objects in the function body. The type annotation for the object goes after the destructuring syntax:
function sum({a, b, c}: {a: number, b: number, c: number}) {
console.log(a + b + c);
}
We can also have a named type here as well:
type ABC = {a: number, b: number, c: number};
function sum({a, b, c}: ABC) {
console.log(a + b + c);
}
A void-returning callback type says "I'm not going to look at your return value, if one exists".
Ref: why-are-functions-returning-non-void-assignable-to-functions-returning-void
Contextual typing with a return type of void does not force functions to not return something. Another way to say this is a contextual function type with a void
return type (type voidFunc = () => void
), when implemented, can return any other value, but it will be ignored.
type voidFunction = () => void;
This contextual function can be implemented in any of the below ways and all of them are valid.
const f1: voidFunction = () => {
return true;
};
const f2: voidFunction = () => true;
const f3: voidFunction = function() {
return true;
};
Moreover, all of the variables f1
, f2
, f3
will have the return type of void
.
This behaviour exists to support situations like these:
const src = [1,2,3];
const des = [0];
src.forEach((item) => des.push(item));
Here, push
method returns a number
type while forEach
method expects a function of return type void
. But, the code works fine.
Additionally, if a literal function definition has a void
return type, then it must not return anything.
function f4(): void {
// Error!
return true;
}