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
let let x: string
x = 'hello js'; //<- x is a string (by inference)
//=====
//<- With type, TypeScript knows what we are allowed to do with the variable.
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(let x: string
x.String.toUpperCase(): string
Converts all the alphabetic characters in a string to uppercase.toUpperCase()); //<- so this is fine
var console: Console
The `console` module provides a simple debugging console that is similar to the
JavaScript console mechanism provided by web browsers.
The module exports two specific components:
* A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and
[`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
_**Warning**_: The global console object's methods are neither consistently
synchronous like the browser APIs they resemble, nor are they consistently
asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for
more information.
Example using the global `console`:
```js
console.log('hello world');
// Prints: hello world, to stdout
console.log('hello %s', 'world');
// Prints: hello world, to stdout
console.error(new Error('Whoops, something bad happened'));
// Prints error message and stack trace to stderr:
// Error: Whoops, something bad happened
// at [eval]:5:15
// at Script.runInThisContext (node:vm:132:18)
// at Object.runInThisContext (node:vm:309:38)
// at node:internal/process/execution:77:19
// at [eval]-wrapper:6:22
// at evalScript (node:internal/process/execution:76:60)
// at node:internal/main/eval_string:23:3
const name = 'Will Robinson';
console.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to stderr
```
Example using the `Console` class:
```js
const out = getStreamSomehow();
const err = getStreamSomehow();
const myConsole = new console.Console(out, err);
myConsole.log('hello world');
// Prints: hello world, to out
myConsole.log('hello %s', 'world');
// Prints: hello world, to out
myConsole.error(new Error('Whoops, something bad happened'));
// Prints: [Error: Whoops, something bad happened], to err
const name = 'Will Robinson';
myConsole.warn(`Danger ${name}! Danger!`);
// Prints: Danger Will Robinson! Danger!, to err
```console.Console.log(message?: any, ...optionalParams: any[]): void (+2 overloads)
Prints to `stdout` with newline. Multiple arguments can be passed, with the
first used as the primary message and all additional used as substitution
values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
(the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)).
```js
const count = 5;
console.log('count: %d', count);
// Prints: count: 5, to stdout
console.log('count:', count);
// Prints: count: 5, to stdout
```
See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(let x: string
x.toFixed(2)); //<- this is not fine, because there is no `toFixed` method on string
//=====
let x: string
x = 'hello ts'; //<- reassignment is fine
//=====
x = 42; //<- but it will error if we try to change type
//=====
const const y: "hello"
y = 'hello'; //<- The type is literally 'hello' when it is declared with const. This is known as a 'literal type'.
//=====
if (y === 'whurt') { //<- TypeScript marks this check as error because it is always false
}
//=====
//<- Sometimes we need to declare a variable without initializing it
let let z: any
z;
let z: any
z = 41;
let z: any
z = 'abc'; //<- Oh no! TypeScript doesn't error
//=====
//<- If we look at the type of z, it's `any`. This is the most flexible (and dangerous) type in TypeScript
//=====
//<- We could improve this situation by providing a type annotation when we declare our variable
let let zz: number
zz: number;
let zz: number
zz = 41;
zz = 'abc'; //<- ERROR, yay!
Takeaways:
-
Having type for variable tells us and TypeScript what we can do with that variable.
-
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.
-
But TypeScript could not read your mind, so you would need to annotate the type explicitly in some case.
-
You can annotate the type of a variable by using the following syntax:
let variableName: Type;
Array and Tuple
/**
* simple array types can be expressed using []
*/
let let luckyNumbers: number[]
simple array types can be expressed using []luckyNumbers: number[] = [];
let luckyNumbers: number[]
simple array types can be expressed using []luckyNumbers.Array<number>.push(...items: number[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(33);
let luckyNumbers: number[]
simple array types can be expressed using []luckyNumbers.Array<number>.push(...items: number[]): number
Appends new elements to the end of an array, and returns the new length of the array.push('abc'); // !ERROR
/* array type will be inferred as well if you provide initial value */
let let luckyPersons: string[]
luckyPersons = ['Steve', 'Bill'];
let luckyPersons: string[]
luckyPersons.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push('Elon');
let luckyPersons: string[]
luckyPersons.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(66); // ERROR
const const frameworks: string[]
frameworks: interface Array<T>
Array<string> = []; // Array<> works too
const frameworks: string[]
frameworks.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push('react');
const frameworks: string[]
frameworks.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(true); // !ERROR
/**
* we can even define a tuple, a fixed length and specific type for each item
*/
let let address: [number, string, string, number]
we can even define a tuple, a fixed length and specific type for each itemaddress: [number, string, string, number] = [123, 'Jalan Besar', 'Kuala Lumpur', 10110];
let address: [number, string, string, number]
we can even define a tuple, a fixed length and specific type for each itemaddress = [1, 2, 3]; // !ERROR
/**
* Tuple values often require type annotations
*/
const const xx: number[]
Tuple values often require type annotationsxx = [32, 31]; // number[];
const const yy: [number, number]
yy: [number, number] = [32, 31];
Takeaways:
- Two ways to declare array,
Type[]
orArray<Type>
syntax. Personally I prefer the former as it is shorter. - TypeScript inference will give preference to the most common usage pattern in JavaScript, e.g. prefer Array over Tuple.
Object
/**
* object types can be expressed using {} and property names
*/
let let address: {
houseNumber: number;
streetName: string;
}
object types can be expressed using {} and property namesaddress: { houseNumber: number
houseNumber: number; streetName: string
streetName: string };
let address: {
houseNumber: number;
streetName: string;
}
object types can be expressed using {} and property namesaddress = {
streetName: string
streetName: 'Fake Street',
houseNumber: number
houseNumber: 123,
};
address = { houseNumber: number
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
.
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:
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.
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
.
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
.
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.