Please go to https://github.com/msd-code-academy/react-workshop and clone the project. Then inside the project run:
git pull
cd 09-typescript-in-react
npm ci
npm start
Development server should open on port 3000
.
- Open source programming language developed and maintained by Microsoft
- Superset of JavaScript: TS compiles to JS - try yourself on TS playground
- Aligned with ECMAScript for compatibility - what's available in JS is available in TS
- find problems before running the code - speed up the development and avoid surprising run time bugs
- better developer experience thanks to the intellisense
- easier maintainability and extendability in large code base (any change is visible throughout the whole project)
- better readability
- no need to use
propTypes
- allows additional features like e.g. function overloading that are not possible in JS
Even though there is some learning curve and at the beginning TS will slow you down, it is beneficial in the long term, especially with big and complex code base.
- replacement of tests - sometimes there are opinions that thanks to typescript we don't need to write (unit) tests anymore - that's not true. We still need to test the logic of our code!
- different programming language - TS is still JavaScript under the hood, it would be mistake to think that's it's a separate programming language like Java or C#
- always easy - TS can be sometimes confusing and especially error messages can be very hard to read
const isValid: boolean = true;
There is no distinction between integers, floats, etc. - there is just number
const count: number = 10;
const text: string = 'TypeScript is awesome 🌈';
Used to express that variable can have on of the predefined values.
enum Color {
Red,
Green,
Blue
}
const myColor: Color = Color.Green;
See the transpiled code in playground.
NOTE: Be careful with enum - the values will be changed to numbers in the runtime!
const myColor: Color = Color.Green;
console.log(myColor); // prints out 0
If we need the values, it is possible to assign them:
enum Color {
Red = 'red',
Green = 'green',
Blue = 'blue'
}
const myColor: Color = Color.Green;
console.log(myColor); // prints out 'green'
Magic type - by any
we express that the variable can have any type - no constraints are applied.
This type is not desirable - we could put any
everywhere, but then we would negate benefits of TS.
TIP: use strict mode ("strict": true
in tsconfig.json
) to prevent usage of any
.
These values have their own type in TypeScript.
We use void
to indicate that function doesn't return any type.
function logMessage(message: string): void {
console.log(message);
}
never
is a special type that indicates that the value 'never occurs'. For example here we say
by never
that function never returns anything and that the end of the function is not reachable.
function error(message: string): never {
throw new Error(message);
}
There are two ways to annotate list types. There is no real difference between them:
const firstArray: number[] = [];
const secondArray: Array<number> = [];
There is also a way in TypeScript how you can define tuple
:
const position: [number, number] = [0, 10];
object
is a type that represents any non-primitive type. It is not recommended to use, it's
better to define shape of an object using type
or interface
(see below) instead.
type Beverage = {
name: string;
isSparkling: boolean;
};
const beverage: Beverage = {
name: 'coke',
isSparkling: true
};
drink(beverage);
NOTE: Whenever we define our own type, it should start with a capital letter. For variable names we use snake case => that way we can easily distinguish between variable and type.
We can also achieve the same by using interface
instead of type
.
interface Beverage { //<= There is no '=' sign!
name: string;
isSparkling: boolean;
};
const beverage: Beverage = {
name: 'coke',
isSparkling: true
};
drink(beverage);
Nowadays interfaces and types are very similar and there are only small differences between them. The most noticeable differences:
- we can use
extends
for interface, but not for type (though we can achieve the same behavior using unions or intersections)
interface a {
name: string;
}
interface b extends a {
id: number;
}
- we can assign a primitive type to our own type alias, that is not possible in interface
type MyType = number;
- duplicate declaration for interfaces is valid (both are merged), while for type aliases it throws an error
interface Point { x: number; }
interface Point { y: number; }
const point: Point = { x: 1, y: 2 };
type TypePoint = { x: number; }
type TypePoint = { y: number; } //<= will throw a compilation error
In general, it is more frequent to use type aliases in React.
A type can be defined by combination of other types.
We use union (|
) to express that a variable is of a type A "OR" a type B.
type StringOrNumber = string | number;
// StringAndNumber is of the type 'string' or 'number'
const a: StringOrNumber = 7; // OK
const b: StringOrNumber = 'Hello'; // OK
For non-primitive types it means: common properties that have different type can have both types.
type Person = {
name: string;
id: string; // <=
};
type Student = {
name: string;
id: number; // <=
};
type StudentOrPerson = Student | Person;
// Result:
// {
// name: string;
// id: string | number; // <=
// };
Unions are very frequent. On the other hand, intersection is in practice used rarely, but it's good to understand the difference.
We use intersection (&
) to express that a variable is of a type A "AND" a type B.
For primitive types it doesn't make too much sense.
type StringAndNumber = string & number;
// StringAndNumber is of the type 'never'
For non-primitive types it means: types are merged and common properties will have the type that both properties have in common
type Person = {
name: string;
id: string | number; // <=
};
type Student = {
name: string;
id: number; // <=
grade: number;
};
type StudentAndPerson = Student & Person;
// Result:
// {
// name: string;
// id: number; // <=
// grade: number;
// };
TypeScript is a 'structural type system' - unlike 'nominal systems', it cares only about structure of types. This code is perfectly valid in TypeScript, but it would throw an error in Nominal type systems, like Java:
class Person {
name: string
}
class Customer {
name: string
}
const someone: Person = new Customer();
Even though TypeScript is good at inferring the return type of our function, it is a good practice to always state the return type explicitly:
// Functional Expression
const repeat = (text: string, repeat: number): string => {
//...
}
// Equivalent standard function
function repeat(text: string, repeat: number): string {
//...
}
Function that doesn't return anything should have the void
or never
return type:
function doSomething(): void {
//...
};
We use question mark to annotate optional parameters.
function handleCallback(callBack?: CallBack): void {
// callBack might be undefined here, we must handle that case otherwise TS will throw error:
if (!callBack) {
return;
}
//...
};
async
and await
are just a syntax sugar over the Promises, that's why we can type the async function like this:
type Data = {
id: number;
info: string;
};
const fetchData = async(query: string): Promise<Data[]> => { //<= Promise that will resolve to array of Data objects
const result = await fetch(`https://api.domain.com/data?query=${query}`);
const resultJSON = await result.json();
return resultJSON.result || [];
}
It is possible to use default parameters in the function declaration. Using default parameters will make them optional:
function sayMyName(name: string = 'Fantomas'): void {
// ...
};
There is no need to use question marks for parameters with default values.
Thanks to generics, it is possible to create functions and classes that work with a variety of types. For example instead of having:
function doSomethingWithString(str: string): string {
return str;
}
function doSomethingWithNumber(num: number): number {
return num;
}
we use generic type and define the doSomething
method like this:
function doSomething<T>(val: T): T {
return val;
}
Then we can call the function either with type in brackets as parameter:
const result = doSomething<string>('Hello');
Or we can even rely on TypeScript that it will infer the type and simply call:
const result = doSomething('Hello');
Variable result
will be a string in both cases.
We can define type alias for function with generics e.g. like this:
type MyGenericFunction = <T>(arg: T) => T;
Generics can be utilized also in classes:
class GenericClass<T> {
private myValue: T;
constructor (myValue: T) {
this.myValue = myValue;
}
public getValue(): T {
return this.myValue;
};
}
const MyClass = new GenericClass<number>(4);
const result = MyClass.getValue();
// type of result is number
Utility types are generic types that we can use out of the box and that transforms given type somehow.
type Car = {
color: string;
maxSpeed: number;
};
type PartialCar = Partial<Car>;
// type PartialCar = {
// color?: string;
// maxSpeed?: number;
// };
type Car = {
color: string;
maxSpeed: number;
};
type PickCar = Pick<Car, 'color'>;
// type PickCar = {
// color: string;
// };
type Car = {
color: string;
maxSpeed: number;
};
type OmitCar = Omit<Car, 'color'>;
// type OmitCar = {
// maxSpeed: number;
// };
More utility types can be found here.
To create a new React application with TypeScript run:
# Prerequisite: node version ^8.10.0 || ^10.13.0 || >=11.10.1
npx create-react-app app_name --typescript
Some packages comes with the type definitions available out of the box, but for others we have to add them. For example,
react
is not currently shipped with type definitions and we must add them manually from open source initiative DefinitelyTyped
npm install @types/react --save-dev
If you have issues that some package doesn't have types, try to install the types from DefinitelyTyped, usually they are there.
npm install @types/<package name> --save-dev
To use types with functional component, we can import the generic React.FC
(or React.FunctionComponent
) type from React:
type MyProps = {
//...
}
const MyComponent: React.FC<MyProps> = (props) => (
// React will automatically add 'children' prop to MyProps type
);
See the first exercise
Similarly we can add typing to a class component by extending the React.Component<Props, State>
or React.PureComponent<Props, State>
class FormattedContent extends React.Component<MyProps, MyState> {
state: MyState = {
// Default values for state
}
public render() {
//...
}
}
See the second exercise
Type inference works very well for this hook - defined by the type of the given (default) value:
const [isOpen, setIsOpen] = React.useState(false);
// type of isOpen: boolean
// type of setIsOpen: (value: boolean) => void
If we want to initialize the state with null
value, we can explicitly type it using generics:
type Student = {
id: number;
name: string;
}
const [student, setStudent] = React.useState<Student | null>(null);
// type of student: Student | null
// type of setStudent: (value: Student | null) => void
Typing the useEffect
is very easy, we should only take care not to return anything other than a function
or undefined
:
React.useEffect(
() => {
const intervalID = setInterval(() => {
console.log(new Date())
}, 1000);
return () => clearInterval(intervalID);
},
[]
);
See the third exercise
function TextInputWithFocusButton() {
// initialize with null, but tell TypeScript we are looking for an HTMLInputElement
const inputEl = React.useRef<HTMLInputElement>(null);
const onButtonClick = () => {
if (inputEl && inputEl.current) {
inputEl.current.focus();
}
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
There are currently two main linters for TypeScript: tslint
and eslint
.
Even though the tslint
is still supported and works, it is about to be deprecated,
use eslint
instead.
Palantir, tslint
creator and maintainer decided to contribute to eslint open source initiative instead of managing
separate tslint package in order to have unified developer experience across JavaScript and TypeScript languages.