Back Arrow
From the blog

Hidden Aspects of TypeScript and How to Resolve Them

We suggest using a special editor to immediately check each example while reading the article. This editor is convenient because you can switch the TypeScript version in it.

Andrey Stepanov

CTO at ByteMinds

We suggest using a special editor to immediately check each example while reading the article. This editor is convenient because you can switch the TypeScript version in it.

Getting “any” instead of “unknown”

When we use the “any” type, we lose typing - we can access any method or property of such an object, and the compiler will not warn us about possible errors. If we use 'unknown', the compiler will notify us of potential issues.

Some functions and operations return “any” by default - this is not entirely obvious, here are some examples:


// JSON.parse
const a = JSON.parse('{ a: 1 }'); // any
// Array.isArray
function parse(a: unknown) {
if (Array.isArray(a)) {
console.log(a); // a[any]
}
}
// fetch
fetch("/")
.then((res) => res.json())
.then((json) => {
console.log(json); // any
});
// localStorage, sessionStorage
const b = localStorage.a; // any
const c = sessionStorage.b // any

ts-reset can solve this problem.

ts-reset is a library that helps solve some non-obvious issues where we wish TypeScript worked differently by default.

Array methods are too strict for the “as const” construct

This issue is also found in the “has” methods of “Set” and “Map”.

Example: we create an array of users, assign the “as const” construct, then call the “includes” method and get an error because argument 4 does not exist in the “userIds” type.


const userIds = [1, 2, 3] as const;

userIds.includes(4);

ts-reset will also help get rid of this error.

Filtering an array from “undefined”

Let's say we have a numeric array that may contain “undefined”. To get rid of these “undefined”, we filter the array. But the “newArr” array will still contain the array type “number” or “undefined”.


const arr = [1, 2, undefined];
const newArr = arr.filter((item) => item !== undefined);

 We can solve the problem like this, and then “newArr2” will  have the type “number”:


const newArr2 = arr.filter((item): item is number => item !== undefined);

Also, ts-reset can help but only for the case when the “filter” function argument is “BooleanConstructor” type.


const filteredArray = [1, 2, undefined].filter(Boolean)

Narrowing a type using bracket notation

We create an object with a type key string, value string, or array of strings.

We then access the object's property using bracket notation and check that the object's return type is a string. In TypeScript versions below 4.7, the “queryCountry” type will be a string or an array of strings, i.e. automatic type narrowing does not work, even though we have already checked the condition.

However, if you use TypeScript version 4.7 and above, type narrowing will work as expected.


const query: Record<string, string | string[]> = {};

const COUNTRY_KEY = 'country';

if (typeof query[COUNTRY_KEY] === 'string') {
    const queryCountry: string = query[COUNTRY_KEY];
}

Link to documentation.

Enum problems

We create an “enum” and do not specify the values explicitly, so each key in order will have numerical values from 0 onwards.

Using this “enum”, we type the first argument of the “showMessage” function, expecting that we will be able to pass only those codes that are described in the “enum”:


enum LogLevel {
    Debug, // 0
    Log, // 1
    Warning, // 2
    Error // 3
}

const showMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

showMessage(0, 'debug message');
showMessage(2, 'warning message');

If we pass a value not contained in the “enum” as an argument, we should see the error "Argument of type '-100' is not assignable to parameter of type 'LogLevel'." But in TypeScript versions below 5.0, this error doesn’t occur, although logically it should:


showMessage(-100, 'any message')

We can also create an “enum” and explicitly specify numeric values. We indicate the “enum” type to the constant “a” and assign any non-existent number that is not in the “enum”, for example, 1. When using TypeScript versions below 5, there will be no error.


enum SomeEvenDigit {
    Zero = 0,
    Two = 2,
    Four = 4
}

const a: SomeEvenDigit = 1;

And one more thing: when using TypeScript below version 5, calculated values cannot be used in “enum”.


enum User {
  name = 'name',
    userName = `user${User.name}`
}

Link to documentation.

Functions that have an explicit return type of “undefined” must have an explicit return

In versions of TypeScript below 5.1, an error will appear in cases where a function has an explicit type of “undefined”, but no “return”.


function f4(): undefined {}

There will be no error in the following cases:


function f1() {}

function f2(): void {}

function f3(): any {}

 To summarize, if we explicitly assign the type “void” or “any” to a function, there will be no error. It will appear if we assign a function type “undefined”, and only when using TypeScript version below 5.1.

Link to documentation.

The behavior of “enums” follows nominative typing, not structural typing

 This is, even though TypeScript uses structural typing. 

Let's create an “enum” and a function whose argument we type with this “enum”. Then we try to call the function passing a string that is identical to one of the enum values as the argument. We get an error in “showMessage”: the argument type “Debug” cannot be assigned because the “enum” type “LogLevel” is expected.


enum LogLevel {
    Debug = 'Debug',
    Error = 'Error'
}

const showMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

showMessage('Debug', 'some text')

Even if we create a new “enum” with the same values, it won't work.


enum LogLevel2 {
    Debug = 'Debug',
    Error = 'Error'
}
showMessage(LogLevel2.Debug, 'some text')

The solution is to use objects with the value “as const”.


const LOG_LEVEL = {
    DEBUG: 'debug',
    ERROR: 'error'
} as const

type ObjectValues = T[keyof T]

type LogLevel = ObjectValues;

const logMessage = (logLevel: LogLevel, message: string) => {
    // code...
}

In this case, we can pass anything, and there will be no error because we are working with a simple value, and it does not matter where it is passed from.


logMessage('debug', 'some text')
logMessage(LOG_LEVEL.DEBUG, 'some text')

Possibility of returning the wrong data type in function with overloading

Suppose we want to return a string from a function if 2 of its arguments are strings. We create such functions and then check whether our arguments are strings. In this case, we can return any data type, even though a string was specified in the first step.


function add(x: string, y: string): string
function add(x: number, y: number): number
function add(x: unknown, y: unknown): unknown {

    if (typeof x === 'string' && typeof y === 'string') {
                return 100;
    }

    if (typeof x === 'number' && typeof y === 'number') {
        return x + y
    }

    throw new Error('invalid arguments passed');
}

Next, we expect that “const” will contain the type “string”, but we get a number.


const str = add("Hello", "World!");
const num = add(10, 20);

Passing an object as an argument to a function with an extra property

When typing the arguments of functions and classes, we cannot add extra properties that were not originally specified in the type or interface. After all, in this case, we are simply passing a different structure as an argument.

However, in TypeScript, it is possible to break this rule:

type Func = () => {
  id: string;
};

const func: Func = () => {
  return {
    id: "123",
    name: "Hello!",
  };
};

For greater clarity, let's create an object with the “formatAmountParams” settings, which we will pass to the “formatAmount” function. As you can see, an object with settings can contain extra properties and there will be no error.

type FormatAmount = {
  currencySymbol?: string,
  value: number
}

const formatAmount = ({ currencySymbol = '$', value }: FormatAmount) => {
  return `${currencySymbol} ${value}`;
}

const formatAmountParams = {
  currencySymbol: 'USD',
  value: 10,
  anotherValue: 20
}

Also, there is no error if we pass an object that contains extra properties:


formatAmount(formatAmountParams);

But we will get an error if we create an object as a function argument and pass it with an extra property.


formatAmount({ currencySymbol: '', value: 10, anotherValue: 12 });

In addition, we may face unexpected behavior if we want to rename “currencySymbol” to “currencySign”.

First, let's change the type, then TypeScript will prompt that we need to change the key in the object from “currencySymbol” to “currencySign”.

type FormatAmount = {
  currencySign?: string,
  value: number
}

const formatAmount = ({ currencySign = '$', value }: FormatAmount) => {
  return `${currencySign} ${value}`;
}

const formatAmountParams = {
  currencySymbol: 'USD',
  value: 10
}

formatAmount(formatAmountParams);

There are no errors -  so we might think that the refactoring went smoothly. But in “formatAmountParams” the old name “currencySymbol” remains, and instead of the expected result “USD 10” we will get “$10”.

Loss of typing when using “Object.keys”

Let's create an “obj” object. Using “Object.keys”, let's create an array with the object's keys and iterate through this array. If we access an object by key in a loop, TypeScript will say that we cannot do this because the generic type “string” cannot be used as a key for the “obj” object.

A possible solution is to cast the type using the “as” construct. But this can be unsafe because we are manually setting what type will be there. We need to ensure that [key] is not just a string, but a key, and indicate this explicitly.


const obj = {a: 1, b: 2}

Object.keys(obj).forEach((key) => {
  console.log(obj[key])
  console.log(key as keyof typeof obj)
});

TypeScript may not recognize data type changes

Let's create a “UserMetadata” type as a key-value “Map”. Based on this type, we create a “cache” and try to get the value for the key “foo” using the “get” method. Everything works as expected.

Next, we'll create a “cacheCopy” object based on “cache” and also call the “get” method. TypeScript won't indicate that anything is wrong, but there will be an error because the object doesn't have a “get” method.

type Metadata = {};

type UserMetadata = Map<string, Metadata>;

const cache: UserMetadata = new Map();

console.log(cache.get('foo'));

const cacheCopy: UserMetadata = { ...cache };

console.log(cacheCopy.get('foo'));

Merge interfaces

Interfaces, unlike types, can merge. If there are interfaces with the same names in one file, then when we assign this interface, it will contain properties from all interfaces with the same names.


interface User {
    id: number;
}

interface User {
    name: string;
}

// Error: Property 'id' is missing in type '{ name: string; }' but required in type 'User', because User interfaces merged
const user: User = {
    name: 'bar',
}

Moreover, if we have global interfaces, for example, predefined in TypeScript itself, they will also be merged. For example, if we create an interface named “comment”, we will get a merge of interfaces because “comment” already exists in “lib.dom.d.ts”.


interface Comment {
  id: number;
  text: string;
}

// Error: Type '{ id: number; text: string; }' is missing the following properties from type 'Comment': data, length, ownerDocument, appendData, and 59 more.
const comment: Comment = {
  id: 5,
  text: "good video!",
};

Link to documentation.

Conclusion

If you want to review the topic but don’t want to read the article again, you can watch a few videos on YouTube:

Enums considered harmful

Be Careful With Return Types In TypeScript

It's easy to start working with us. Just fill the brief or call us.

Find out more
White Arrow
From the blog
Related articles

How to properly measure code speed in .NET

Anton Vorotyncev

Imagine you have a solution to a problem or a task, and now you need to evaluate the optimality of this solution from a performance perspective.

Next.js

Formalizing API Workflow in .NET Microservices

Artyom Chernenko

Let's talk about how to organize the interaction of microservices in a large, long-lived product, both synchronously and asynchronously.

.NET

Troubleshooting tracking and personalisation in Sitecore XM Cloud

Anna Gevel

One of the first things I tested in Sitecore XM Cloud was embedded tracking and personalisation capabilities. It has been really interesting to see what is available out-of-the-box, how much flexibility XM Cloud offers to marketing teams and what is required from developers to set it up.

Sitecore

Mastering advanced tracking with Kentico Xperience

Dmitry Bastron

We will take you on a journey through a real-life scenario of implementing advanced tracking and analytics using Kentico Xperience 13 DXP.

Kentico
Devtools

Why is Kentico of such significance to us?

Anastasia Medvedeva

Kentico stands as one of our principal development tools, we believe it would be fitting to address why we opt to work with Kentico and why we allocate substantial time to cultivating our experts in this DXP.

Kentico

Where to start learning Sitecore - An interview with Sitecore MVP Anna Gevel

Anna Gevel

As a software development company, we at Byteminds truly believe that learning and sharing knowledge is one of the best ways of growing technical expertise.

Sitecore

Sitecore replatforming and upgrades

Anastasia Medvedeva

Our expertise spans full-scale builds and support to upgrades and replatforming.

Sitecore

How we improved page load speed for Next.js ecommerce website by 50%

Sergei Pestov

How to stop declining of the performance indicators of your ecommerce website and perform optimising page load performance.

Next.js

Sitecore integration with Azure Active Directory B2C

Dmitry Bastron

We would like to share our experience of integrating Sitecore 9.3 with the Azure AD B2C (Azure Active Directory Business to Consumer) user management system.

Sitecore
Azure

Activity logging with Xperience by Kentico

Dmitry Bastron

We'll dive into practical implementation in your Xperience by Kentico project. We'll guide you through setting up a custom activity type and show you how to log visitor activities effectively.

Kentico

Interesting features of devtools for QA

Egor Yaroslavcev

Chrome DevTools serves as a developer console, offering an array of in-browser tools for constructing and debugging websites and applications.

Devtools
QA

Kentico replatforming and upgrades

Anastasia Medvedeva

Since 2015, we've been harnessing Kentico's capabilities well beyond its core CMS functions.

Kentico

Umbraco replatforming and upgrades

Anastasia Medvedeva

Our team boasts several developers experienced in working with Umbraco, specialising in development, upgrading, and replatforming from other CMS to Umbraco.

Umbraco

Sitecore Personalize: tips & tricks for decision models and programmable nodes

Anna Gevel

We've collected various findings around decision models and programmable nodes working with Sitecore Personalize.

Sitecore

Fixed Price, Time & Materials, and Retainer: How to Choose the Right Agreement for Your Project with Us

Andrey Stepanov

We will explain how these agreements differ from one another and what projects they are suitable for.

Customer success

Enterprise projects: what does a developer need to know?

Fedor Kiselev

Let's talk about what enterprise development is, what nuance enterprise projects may have, and which skills you need to acquire to successfully work within the .NET stack.

Development

Headless CMS. Identifying Ideal Use Cases and Speeding Up Time-to-Market

Andrey Stepanov

All you need to know about Headless CMS. We also share the knowledge about benefits of Headless CMS, its pros and cons.

Headless CMS

Dynamic URL routing with Kontent.ai

We'll consider the top-to-bottom approach for modeling content relationships, as it is more user-friendly for content editors working in the Kontent.ai admin interface.

Kontent Ai
This website uses cookies. View Privacy Policy.