In this chapter i will focus on just showing the pure basics of typescript, what you have/should know to get started creating your projects and some things to might be aware when you start to move to more advanced/complex types.
Typescript primitives are the same as the javascript primitives with some additions.
string
- A string is a sequence of characters contained inside" "
,' '
or` `
.
let str = "Any string";
number
- A number in javascript is a double-precision 64-bit floating point format (IEEE 754).
let num = 3;
bigint
- A bigint is a integer represented in an arbitrary precision format. In javascript is represented by a number followed byn
let bint = 3n;
boolean
- A boolean is a type that can be eithertrue
orfalse
.
let bool = true;
object
- The object primitive represents anything that has an index signature and isn't another primitive.
let a: object;
a = "";
//
// Type 'string' is not assignable to type 'object'.
a = 3;
//
// Type 'number' is not assignable to type 'object'.
a = {};
a.foo();
//^^^
// Property 'foo' does not exist on type 'object'.
a["bar"];
//^^^^^^
// Element implicitly has an 'any' type because expression of type '"bar"' can't be used to index type '{}'.
// Property 'bar' does not exist on type '{}'.
a();
//
// This expression is not callable.
// Type '{}' has no call signatures.
undefined
- Undefined is a primitive value automatically assigned to variables that have just been declared, or to formal arguments for which there are no actual arguments.
// In this case undefined has to be explicit declared because otherwise typescript will make it `any`, more on that in a moment
let u: undefined;
symbol
- A symbol can only be created using theSymbol
constructor and it is always1 unique.
const sym = Symbol();
unique symbol
- A unique symbol is a subtype of symbol and can only appear if explicit assigned and can only be assigned to a constant usingSymbol()
andSymbol.for()
.
const sym1: unique symbol = Symbol()
let sym2: unique symbol = Symbol()
// ^^^^
// A variable whose type is a 'unique symbol' type must be 'const'.
null
- A primitive type annotating that no memory address is assigned to this variable. It behaves similar to undefined however, it's considered an object.
const n1 = null
// For non constants you have to explicit say the type is null otherwise typescript will say its `any`
let n2: null = null;
any
- As the name sugests,any
can be assigned to any type and wont do any type checking assuming that you know the environment better than typescript.
let a: any;
a = "";
a = 3;
a = {};
a.foo();
a["bar"];
a();
const b: number = a;
unknown
- Basically the same asany
but doesnt allow dot notation, indexing and assigning to other variables. Usingunknown
overany
is highly recommended to prevent possible oversights.
let a: unknown;
a = "";
a = 3;
a = {};
a.foo();
//
// Object is of type 'unknown'.
a["bar"];
//
// Object is of type 'unknown'.
a();
//
// Object is of type 'unknown'.
const b: number = a
// ^
// Type 'unknown' is not assignable to type 'number'.
never
- The never type can usually appear when union types have nothing left and can be used as a return type for functions that throw exeptions. The most comun use ofnever
is on conditional and complex types but more on that later.
function fail(msg: string): never {
throw new Error(msg)
}
function fn(x: string | number): void {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x;
// ^? - (parameter) x: never
}
}
void
-void
is a typescript type that is used for functions that do not return anything or return functions that return void.
function returnVoid() {
// ^? - function returnVoid(): void
return;
}
An implicit type declaration is when you declare a variable without providing a type directly and leaving that work to the compiler.
let str = "A string";
// ^? - let str: string
An explicit type declaration is when you declare a variable with an assigned type.
let str: string = "A string";
// ^? - let str: string
Declaring array types can be done in 2 ways:
- Using the
Array
generic. - Using an array literal.
let stringArray: Array<string>;
let numberArray: number[];
let numberAndBooleanArray: Array<number | boolean>;
let stringAndBooleanArray: (string | boolean)[];
A tuple is a fixed length array that can have a unique type per element, there are however ways to define an infinite length tuple, however, is not recommended and you should use an array instead.
let tuple: [string, number, boolean] = ["hello", 3, true, "world"];
// ^^^^^
// Type '[string, number, true, string]' is not assignable to type '[string, number, boolean]'.
// Source has 4 element(s) but target allows only 3.
tuple = ["hello", true];
// ^^^^
// Type 'boolean' is not assignable to type 'number'.
let infiniteTuple: [string, ...Array<number>] = ["hello", 1, 2, 3];
infiniteTuple = ["hello", 1, 2, "world"];
//^^^^^^^^^^^
// Type '[string, number, number, string]' is not assignable to type '[string, ...number[]]'.
// Type at positions 1 through 3 in source is not compatible with type at position 1 in target.
// Type 'string | number' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.
There are 3 ways to declare object types, those being:
- Using
interfaces
ortype
aliases. - Using object literals.
- Using the
Record
generic.
interface IObj {
a: string;
b: number
}
type TObj = {
a: string,
b: number
}
let iObj: IObj;
let tObj: TObj;
let normalObj: { a: string, b: number };
let recordObj: Record<string, string | number>;
let recordObj2: Record<"a" | "b", string | number>
If you want to create a variable that can store any object, using Record
is highly recommended over the object
type.
let obj: object = {};
obj = [];
// More on 'Record' and 'PropertyKey' later
let record: Record<PropertyKey, unknown> = {};
record = [];
//^^^^
// Type 'never[]' is not assignable to type 'Record<PropertyKey, unknown>'.
// Index signature for type 'string' is missing in type 'never[]'.
Unit types are subsets of primitive types that can only contain one value.
let foo = "foo";
// ^? - let foo: string
let bar: "bar";
// ^? - let bar: "bar"
bar = foo
//^
// Type 'string' is not assignable to type '"bar"'.
In vanilla javascript the main difference between using const
and let
is that you cannot reassing nor redeclare a const
, however, typescript adds another main difference.
Values assigned to a const
will turn into unit types
when possible2, while let
will be assigned to its primitive
type.
const foo = "foo";
// ^? - const foo: "foo"
let bar = "bar";
// ^? - let bar: string
const num = 3;
// ^? - const num: 3
let lnum = 3;
// ^? - let lnum: number
const obj = { a: "hello", b: 3 }
// ^? - const obj: {
// a: string;
// b: number;
// }
The type
keyword is used to create and name any type but as it is only an alias, it cannot be used to create different or unique versions of the same type.
type Point = {
x: number,
y: number
};
type ConcatenatedString = string;
function concat(str1: string, str2: string): ConcatenatedString {
return str1 + str2;
}
// Create a sanitized input
let helloWorld = concat("hello ", "world");
// Can still be re-assigned with a string though
helloWorld = "Hello";
An interface
declaration is another way to declare a type and it's mostly used to declare object types. They can be extended by other interfaces and implemented in classes using the implements keyword.
interface Point {
x: number;
y: number;
}
The intersection operator (&
) can be used just like the extends
keyword but for type alises instead of classes.
interface Circle {
radius: number;
}
interface Colors {
r: number;
g: number;
b: number;
}
type ColorfullCircle = Circle & Colors;
// It can also be used inside function arguments and return types
function draw(circle: Circle & Colors): Circle & Colors {
return circle;
}
draw({ radius: 30 });
// ^^^^^^^^^^^^^^
// Argument of type '{ radius: number; }' is not assignable to parameter of type 'Circle & Colors'.
// Type '{ radius: number; }' is missing the following properties from type 'Colors': r, g, b
The union operator (|
) in typescript works like an or
.
When paired with never
a union type will ignore it and removei t from the union.
type StringOrNumber = string | number;
let a: StringOrNumber = "A string";
a = 3;
a = true;
//
// Type 'boolean' is not assignable to type 'StringOrNumber'.
a = {};
//
// Type '{}' is not assignable to type 'StringOrNumber'.
// As you can see the `never` is excluded
type BooleanOrNumber = boolean | never | number;
// ^? - type BooleanOrNumber = number | boolean
// It can also be used inside function arguments and return types
function stringOrNumber(o: string | number): string | number {
return o;
}
In typescript you can use ?
on a property to make it optional.
On a side note, making a type undefined
doesnt mean it is optional even tho optional types may show as possibly undefined
.
interface User {
name: string;
age?: number;
genre: string | undefined;
}
const user1: User = {
name: "Martin",
genre: undefined,
}
const user2: User = {
// ^^^^^
// Property 'genre' is missing in type '{ name: string; }' but required in type 'User'.
name: "Niek",
}
The extends
keyword can be used in 2 ways:
- Conditional Types that will be covered later on.
- Extend interfaces.
Extending interfaces works just like extending classes in vanilla javascript but with the difference that you can extend multiple interfaces.
interface Label {
labelName: string;
date: string;
}
interface Artist {
artistName: string;
}
interface Song extends Label, Artist {
songName: string;
realeseDate: Date;
}
The implements
keyword can be used to make sure than a class satisfies the definition of the given interface/type.
interface Foo {
foo: () => void;
}
type Bar = {
bar: () => void
}
class MyClass implements Foo, Bar {
// ^^^^^^^
// Class 'MyClass' incorrectly implements interface 'Bar'.
// Property 'bar' is missing in type 'MyClass' but required in type 'Bar'.
foo() {
return;
}
}
Type assertion can be used to convert one type to another. In very unique cases you might need to cast to unknown
first, however, it's not recommended.
Type assertion can be done in 2 ways:
- Using
<>
(only works on non JSX/TSX files). - Using the
as
keyword.
const a = <number>"hello";
// ^^^^^^^^^^^^^^^
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const b = <number><unknown>"hello";
const x = "hello" as number;
// ^^^^^^^^^^^^^^^^^
// Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
const y = "hello" as unknown as number;
The as const
assertion can be used to make a variable readonly and also preserve its types.
const obj = { a: "hello", b: 3 } as const
// ^? - const obj: {
// readonly a: "hello";
// readonly b: 3;
// }
let otherObj = { a: 3, b: "hello" } as const
// ^? - let otherObj: {
// readonly a: 3;
// readonly b: "hello";
// }
Type alias and interface are pretty simillar, the main differences are:
- A type cannot be extended (using the
extends
keyword). - A type cannot have duplicate identifiers.
- A type does not augment a class with the same name.
Interfaces | Types |
---|---|
interface Animal {
name: string;
}
interface Cat extends Animal {
race: string;
} |
type Animal = {
name: string
}
type Cat = Animal & {
race: string
} |
Interfaces | Types |
---|---|
interface Story {
title: string;
}
interface Story {
body: string;
} |
type Story = {
// ^^^^^
// Duplicate identifier 'Story'.
title: string
}
type Story = {
// ^^^^^
// Duplicate identifier 'Story'.
body: string
} |
Interfaces | Types |
---|---|
interface MyClass {
foo: () => void;
bar(): void;
}
class MyClass {
bazz() {
return;
}
}
const myClassInstance = new MyClass();
myClassInstance.foo();
myClassInstance.bar();
myClassInstance.bazz(); |
type MyClass = {
// ^^^^^^^
// Duplicate identifier 'MyClass'.
foo: () => void;
bar(): void;
}
class MyClass {
// ^^^^^^^
// Duplicate identifier 'MyClass'.
bazz() {
return;
}
}
const myClassInstance = new MyClass();
// ^^^^^^^
// 'MyClass' only refers to a type, but is being used as a value here. |
Utility types are types made to help commun manipulations.
The idea of this chapter is to show how they are made and explain how they work.
type PropertyKey = string | number | symbol;
// This is the same as:
type PropertyKey = keyof any;
// ^? - type PropertyKey = string | number | symbol
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F): any }
? F extends ((value: infer V, ...args: any) => any)
? Awaited<V>
: never
:T;
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Exclude<U, E> = U extends E ? never : U;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Extract<T, U> = T extends U ? T : never;
type NonNullable<T> = T & {};
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown;
type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;
interface ThisType<T> { }