Variables in TypeScript

Before we learn to use TypeScript in our React projects, let’s have an overview of TypeScript types and syntax.

Basic Variable and Type Declaration

ts
/**
* x is a string (inferred)
*/
let x = 'hello js';
let x: string
 
/**
* with type annotation, we know what we can do with the variable.
*/
console.log(x.toUpperCase()); // so this is fine
console.log(x.toFixed(2)); // this is not fine, because there is no `toFixed` method on string
Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?2551Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
 
/**
* reassignment is fine
*/
x = 'hello ts';
ts
/**
* x is a string (inferred)
*/
let x = 'hello js';
let x: string
 
/**
* with type annotation, we know what we can do with the variable.
*/
console.log(x.toUpperCase()); // so this is fine
console.log(x.toFixed(2)); // this is not fine, because there is no `toFixed` method on string
Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?2551Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
 
/**
* reassignment is fine
*/
x = 'hello ts';
ts
/**
* but it will error if we try to change type
*/
x = 42; // Error
Type 'number' is not assignable to type 'string'.2322Type 'number' is not assignable to type 'string'.
ts
/**
* but it will error if we try to change type
*/
x = 42; // Error
Type 'number' is not assignable to type 'string'.2322Type 'number' is not assignable to type 'string'.
ts
/**
* let's look at const. The type is literally 'hello'
*/
const y = 'hello';
const y: "hello"
 
/**
* This is called a 'literal type'. y can never be reassigned since it's a const,
* so its value will only be literally the string 'hello world' and no other possible value
*/
if (y === 'whurt') {
This comparison appears to be unintentional because the types '"hello"' and '"whurt"' have no overlap.2367This comparison appears to be unintentional because the types '"hello"' and '"whurt"' have no overlap.
} // this check is unnecessary
ts
/**
* let's look at const. The type is literally 'hello'
*/
const y = 'hello';
const y: "hello"
 
/**
* This is called a 'literal type'. y can never be reassigned since it's a const,
* so its value will only be literally the string 'hello world' and no other possible value
*/
if (y === 'whurt') {
This comparison appears to be unintentional because the types '"hello"' and '"whurt"' have no overlap.2367This comparison appears to be unintentional because the types '"hello"' and '"whurt"' have no overlap.
} // this check is unnecessary
ts
/**
* sometimes we need to declare a variable without initializing it
*/
let z;
z = 41;
z = 'abc'; // oh no! TypeScript didn't error
 
/**
* If we look at the type of z, it's `any`. This is the most flexible type
in TypeScript
*/
 
/**
* we could improve this situation by providing a type annotation
* when we declare our variable
*/
let zz: number;
zz = 41;
zz = 'abc'; // ERROR, yay!
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
ts
/**
* sometimes we need to declare a variable without initializing it
*/
let z;
z = 41;
z = 'abc'; // oh no! TypeScript didn't error
 
/**
* If we look at the type of z, it's `any`. This is the most flexible type
in TypeScript
*/
 
/**
* we could improve this situation by providing a type annotation
* when we declare our variable
*/
let zz: number;
zz = 41;
zz = 'abc'; // ERROR, yay!
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.

Takeaways:

  1. Having type for variable tells us and TypeScript what we can do with that variable.

  2. TypeScript will try to infer type of a variable for you. It will be as strict as possible based on how JavaScript works, but not so strict that it makes you waste time on fighting the it.

  3. But TypeScript could not read your mind, so you would need to annotate the type explicitly in some case.

  4. You can annotate the type of a variable by using the following syntax:

    ts
    let variableName: Type;
    ts
    let variableName: Type;

Array and Tuple

ts
/**
* simple array types can be expressed using []
*/
let luckyNumbers: number[] = [];
luckyNumbers.push(33);
luckyNumbers.push('abc'); // !ERROR
Argument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.
ts
/**
* simple array types can be expressed using []
*/
let luckyNumbers: number[] = [];
luckyNumbers.push(33);
luckyNumbers.push('abc'); // !ERROR
Argument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.
ts
/* array type will be inferred as well if you provide initial value */
let luckyPersons = ['Steve', 'Bill'];
let luckyPersons: string[]
luckyPersons.push('Elon');
luckyPersons.push(66); // ERROR
Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.
ts
/* array type will be inferred as well if you provide initial value */
let luckyPersons = ['Steve', 'Bill'];
let luckyPersons: string[]
luckyPersons.push('Elon');
luckyPersons.push(66); // ERROR
Argument of type 'number' is not assignable to parameter of type 'string'.2345Argument of type 'number' is not assignable to parameter of type 'string'.
ts
/**
* Array<> works too
*/
const frameworks: Array<string> = [];
frameworks.push('react');
frameworks.push(true); // !ERROR
Argument of type 'boolean' is not assignable to parameter of type 'string'.2345Argument of type 'boolean' is not assignable to parameter of type 'string'.
ts
/**
* Array<> works too
*/
const frameworks: Array<string> = [];
frameworks.push('react');
frameworks.push(true); // !ERROR
Argument of type 'boolean' is not assignable to parameter of type 'string'.2345Argument of type 'boolean' is not assignable to parameter of type 'string'.
ts
/**
* we can even define a tuple, a fixed length and specific type for each item
*/
let address: [number, string, string, number] = [123, 'Jalan Besar', 'Kuala Lumpur', 10110];
 
address = [1, 2, 3]; // !ERROR
Type 'number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
2322
2322
Type 'number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
 
/**
* Tuple values often require type annotations
*/
const xx = [32, 31]; // number[];
const xx: number[]
const yy: [number, number] = [32, 31];
ts
/**
* we can even define a tuple, a fixed length and specific type for each item
*/
let address: [number, string, string, number] = [123, 'Jalan Besar', 'Kuala Lumpur', 10110];
 
address = [1, 2, 3]; // !ERROR
Type 'number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
2322
2322
Type 'number' is not assignable to type 'string'.
Type 'number' is not assignable to type 'string'.
 
/**
* Tuple values often require type annotations
*/
const xx = [32, 31]; // number[];
const xx: number[]
const yy: [number, number] = [32, 31];

Takeaways:

  1. Two ways to declare array, Type[] or Array<Type> syntax. Personally I prefer the former as it is shorter.
  2. TypeScript inference will give preference to the most common usage pattern in JavaScript, e.g. prefer Array over Tuple.

Object

ts
/**
* object types can be expressed using {} and property names
*/
let address: { houseNumber: number; streetName: string };
address = {
streetName: 'Fake Street',
houseNumber: 123,
};
 
address = {
Property 'streetName' is missing in type '{ houseNumber: number; }' but required in type '{ houseNumber: number; streetName: string; }'.2741Property 'streetName' is missing in type '{ houseNumber: number; }' but required in type '{ houseNumber: number; streetName: string; }'.
houseNumber: 33,
}; // !Error: Property 'streetName' is missing
 
/**
* You can use the optional operator (?) to indicate that something may or may not be there
*/
// let add: { houseNumber: number; streetName?: string };
// add = {
// houseNumber: 33
// };
 
// Use `interface` to reuse object type
// interface Address {
// houseNumber: number;
// streetName?: string;
// }
// * and refer to it by name
// let ee: Address = { houseNumber: 33 };
ts
/**
* object types can be expressed using {} and property names
*/
let address: { houseNumber: number; streetName: string };
address = {
streetName: 'Fake Street',
houseNumber: 123,
};
 
address = {
Property 'streetName' is missing in type '{ houseNumber: number; }' but required in type '{ houseNumber: number; streetName: string; }'.2741Property 'streetName' is missing in type '{ houseNumber: number; }' but required in type '{ houseNumber: number; streetName: string; }'.
houseNumber: 33,
}; // !Error: Property 'streetName' is missing
 
/**
* You can use the optional operator (?) to indicate that something may or may not be there
*/
// let add: { houseNumber: number; streetName?: string };
// add = {
// houseNumber: 33
// };
 
// Use `interface` to reuse object type
// interface Address {
// houseNumber: number;
// streetName?: string;
// }
// * and refer to it by name
// let ee: Address = { houseNumber: 33 };

Interface

Interface can be used to describe object and function. Interface cannot be used to describe primitive, such as string or boolean.

ts
interface Job {
name: string;
salary: number;
}
const job: Job = {
name: 'programmer',
salary: 3000,
};
/* interface make sure the object fits the requirement */
// const anotherJob: Job = {
// name: undefined,
// salary: 7000
// }
/* interface can be extended, but don't do this more than a level */
interface AwesomeJob extends Job {
salary: 20_000;
benefits: string[];
}
const nonExistentJob: AwesomeJob = {
name: '@#$%^',
salary: 20000,
benefits: ['unlimited leaves', '1-year maternity leave'],
};
ts
interface Job {
name: string;
salary: number;
}
const job: Job = {
name: 'programmer',
salary: 3000,
};
/* interface make sure the object fits the requirement */
// const anotherJob: Job = {
// name: undefined,
// salary: 7000
// }
/* interface can be extended, but don't do this more than a level */
interface AwesomeJob extends Job {
salary: 20_000;
benefits: string[];
}
const nonExistentJob: AwesomeJob = {
name: '@#$%^',
salary: 20000,
benefits: ['unlimited leaves', '1-year maternity leave'],
};

Index signature

One of the common usage of JavaScript is to use object as a simple key-value map object that you can use to lookup value.

Consider the example below:

js
const pokemonCache = {};
const getPokemon = (id) => {
if (pokemonCache[id]) {
// if pokemon is already available in the cache, return it.
return Promise.resolve(pokemonCache[id]);
}
// else fetch the pokemon data from api, cache it and return it
return fetch(`https://pokemon-json.herokuapp.com/api/pokemons/${id}`)
.then((res) => res.json())
.then((pokemon) => {
pokemonCache[id] = pokemon;
return pokemon;
});
};
js
const pokemonCache = {};
const getPokemon = (id) => {
if (pokemonCache[id]) {
// if pokemon is already available in the cache, return it.
return Promise.resolve(pokemonCache[id]);
}
// else fetch the pokemon data from api, cache it and return it
return fetch(`https://pokemon-json.herokuapp.com/api/pokemons/${id}`)
.then((res) => res.json())
.then((pokemon) => {
pokemonCache[id] = pokemon;
return pokemon;
});
};

How can we declare typing of pokemonCache?

Index signature.

ts
interface Pokemon {
id: number;
name: string;
sprite: string;
thumbnail: string;
}
const pokemonCache: { [id: number]: Pokemon } = {};
const getPokemon = (id: number) => {
if (pokemonCache[id]) {
return Promise.resolve(pokemonCache[id]);
}
return fetch(`https://pokemon-json.herokuapp.com/api/pokemons/${id}`)
.then((res) => res.json())
.then((pokemon: Pokemon) => {
pokemonCache[id] = pokemon;
return pokemon;
});
};
ts
interface Pokemon {
id: number;
name: string;
sprite: string;
thumbnail: string;
}
const pokemonCache: { [id: number]: Pokemon } = {};
const getPokemon = (id: number) => {
if (pokemonCache[id]) {
return Promise.resolve(pokemonCache[id]);
}
return fetch(`https://pokemon-json.herokuapp.com/api/pokemons/${id}`)
.then((res) => res.json())
.then((pokemon: Pokemon) => {
pokemonCache[id] = pokemon;
return pokemon;
});
};

any

In some case where you want to declare a variable that can be anything, make it an any.

ts
let x: any;
x = 5;
x = true;
x = {
y: true,
};
ts
let x: any;
x = 5;
x = true;
x = {
y: true,
};

When you use any, what you tell TypeScript compiler is: “Hey this variable is so dynamic that you cannot figure out, leave me alone!“. And TypeScript will let you be wild.

Try to avoid any. If your code full of any, you could just don’t use TypeScript. Save yourself time to type those :any.

unknown

There are some cases where we can’t really know the type in advance. Some common examples are:

  • response of API calls
  • returned value of JSON.parse

When we writing wrapper for those common operation, the recommended type is unknown.

ts
const getStoredValue = (key: string): unknown => {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : null;
};
const storedUser = getStoredValue('user');
// uncomment below and see type error
// console.log(storedUser.toUpperCase());
if (typeof storedUser === 'string') {
console.log(storedUser.toUpperCase());
}
ts
const getStoredValue = (key: string): unknown => {
const storedValue = localStorage.getItem(key);
return storedValue ? JSON.parse(storedValue) : null;
};
const storedUser = getStoredValue('user');
// uncomment below and see type error
// console.log(storedUser.toUpperCase());
if (typeof storedUser === 'string') {
console.log(storedUser.toUpperCase());
}

The difference of unkown and any is any allows you to go wild and do any operation as you wish without giving you any type error, while for unknown value, you need to prove to TypeScript that it is really a specific type before you can use it.