TypeScript provides some useful primitive types that you’ll be using every day. In this article, I’ll explain them, what they’re for, and the TypeScript-specific primitives.

number

The number type works as you expect in TypeScript, covering all the ways JavaScript allows you to represent a number:

let n: number;
n = 3; //Integer
n = 3.14; //Floating point
n = 3.14e2; //exponent
n = 0b0011; //Binary
n = 0xc0ff33; //Hexadecimal
n = 0o1066; //Octal
n = 22 / 7;

You can also easily convert a number to a string in TypeScript, using the .toString() method of the number:

const myString: string = n.toString();

A cool feature of this function is that it accepts a number as the radix/base, so you can easily convert a number to a binary string, hexadecimal, or any other base up to 26.

string

The string type also acts as it does in JavaScript, storing text values.

let s: string;
s = 'Hi from Upweekly!';

Strings are also very useful when you combine them with type literals and type unions, letting you define a type as “only one of these possible strings”:

type AlertLevel = 'info' | 'warning' | 'critical';

function alert(level: AlertLevel, message: string) {}

alert('info', 'Rebooting the Upmostly quantum mainframe'); //✅
alert('hi', 'This doesnt work'); //❎ TS2345: Argument of type '"hi"' is not assignable to parameter of type 'AlertLevel'.

You can convert most types, e.g. numbers, and objects to a string by seeing if they have a .toString() method. You can convert a string to a number using the Number.

const myNumber = 3.141;

const myString: string = myNumber.toString();

const backToNumber: number = Number.parseFloat(myString); //3.141
const toInteger: number = Number.parseInt(myString); //3

parseInt() also accepts a base as a parameter, letting you convert things like binary and hexadecimal numbers to strings.

boolean

Booleans, very simply store true or false values. One pitfall you might run into is converting your boolean to a string. The easiest way to convert these to a string is using a ternary operator:

const bool: boolean = true;

const boolToString = bool ? 'true' : 'false';

You can of course pick your own representations for the strings.

BigInt

BigInts can be used to store numbers too large to be stored in the number primitive, anything bigger than Number.MAX_SAFE_INTEGER.

You can define a bigint by adding an “n” onto the end of your number, or by using the BigInt() constructor:

let myBigInt = 9007199254740992n;
let myBigInt2 = BigInt(9007199254740992);

The key differences you might deal with when working with Bigint values are:

  • The Math methods will not work with bigint values
  • You can’t mix operations with Bigint and number values without conversion, so be careful about losing precision going from a bigint to a number.

TypeScript will even prevent you from comparing bigints and numbers, and its safer to convert one to the other:

//TS2367: This condition will always return 'false' since the types 'bigint' and 'number' have no overlap.
if (myBigInt === myNumber) {} //❎

if (myBigInt === BigInt(myNumber)) {} //✅
if (Number(myBigInt) === myNumber) {} //✅

symbol

Symbol is a primitive data type that represents an immutable, unique value. You can create them using the Symbol constructor.

const mySymbol1 = Symbol('foo');
const mySymbol2 = Symbol('foo');

//TS2367: This condition will always return 'false' since the types 'typeof mySymbol1' and 'typeof mySymbol2' have no overlap.
if (mySymbol1 === mySymbol2) {
}

If you define your symbols as consts, TypeScript may be able to infer that they are unique.

To explicitly state this, TypeScript provides a special “unique” keyword.

In this example, TS doesn’t complain, since they are both of the type symbol.

const mySymbol1: symbol;
const mySymbol2: symbol;
if (mySymbol1 === mySymbol2) {};

We can change the type to “unique symbol” to tell TypeScript that we are confident that these are two different symbols:

const mySymbol1: unique symbol;
const mySymbol2: unique symbol;
//TS2367: This condition will always return 'false' since the types 'typeof mySymbol1' and 'typeof mySymbol2' have no overlap.
if (mySymbol1 === mySymbol2) {
}

And then TypeScript can be confident that they will never be the same, and throw an error when we try and compare them.

Undefined and Null

Undefined and null are two TypeScript primitive types used to represent nothing. They act similarly in a lot of cases, the main difference is the intent. Undefined pops up a lot when a variable hasn’t been defined, or for example when a function returns nothing. “null” on the other hand is often used intentfully. While undefined means “this thing doesn’t exist/we haven’t defined it”, null is usually used to mean “this thing exists, but I’ve specifically left it empty”.

One place you’ll see undefined pop up for example is with optional values. Making a function parameter optional in TypeScript actually sets its type to a type union, of whatever type you give and undefined.

function undefinedExample(maybeUndefined?: number) {
    //maybeUndefined: number | undefined
}

undefinedExample(10);
undefinedExample(undefined);
undefinedExample();

function nullExample(maybeNull: number | null) {}

nullExample(10);
nullExample(null);
nullExample() //❎ TS2554: Expected 1 arguments, but got 0.

You can see the difference here, not providing our variable “maybeUndefined” gives it a value of undefined. We can explicitly pass it undefined, but there’s no use.

With the null example, we can’t just omit the parameter, we have to explicitly pass in null.

Void and Never

TypeScript introduces two new ways to represent nothing, with Void and Never. If you’ve used any other type languages, you’re probably familiar with void. It represents a function that returns nothing.

function logInCaps(message: string): void {
    console.log(message.toUpperCase());
}

//vs

function toCaps(message: string): string {
    return message.toUpperCase();
}

Never is an interesting new type, it represents something that should never happen.

This could mean for example an infinite loop:

function infiniteLoop(): never {
    while (true) {
        console.log('Hi');
    }
}

Or for example if you use infer with a type union, if a type evaluates to never, it will get removed from the type union, since it should never exist.

Any and Unknown

In TypeScript, any represents any type. If you give a variable a type of any, you can assign anything to it, and do anything with it.

It’s generally frowned upon to use any, in favour of either giving it a more specific type, or using unknown. This is because there’s no type checking for any; you can do anything with it, so we lose a lot of the benefit of TypeScript.

Unknown on the other hand doesn’t let you do anything with it; you need to narrow the type down before you can do something.

function unknownExample(myUnknown: unknown) {
    myUnknown; //myUnknown: unknown
    myUnknown.toString(); //TS2571: Object is of type 'unknown'.
    if (typeof myUnknown === 'number') {
        myUnknown; //myUnknown: number
        myUnknown.toString();
    }
}

Any means “I don’t know what this is so I’ll just assume I can do x”, unknown means “I don’t know what this is, so I’ll make sure I check it can do x”.

Conclusion

Thanks for reading this article on the primitive types in TypeScript. Hopefully, you learnt a bit about the primitive types, any specific TypeScript quirks, and how to convert between them all. If you liked this article, feel free to leave a comment below!

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.