This article was originally published on bmf-tech.com.
Overview
Revisiting the basics of TypeScript.
Review of JavaScript
Variable Scope
Global Scope
Defined as properties of the window object.
const a = "Hello";
console.log(window.a); // Hello
Local Scope
Function Scope
Variables defined within a function are only valid within that function.
function func() {
const a = "Hello";
console.log(a); // Hello
}
Lexical Scope
When a function is defined within another function, the inner function can access the variables of the outer function.
function outer() {
const a = "Hello";
function inner() {
console.log(a); // Hello
}
inner();
}
Block Scope
Variables defined within a block like an if statement or a for loop are only valid within that block.
if (true) {
const a = "Hello";
console.log(a); // Hello
}
const is Non-reassignable but Not Immutable
const obj = { key: "value"};
obj = { key: "newValue" }; // Error
obj.key = "newValue"; // OK
Issues with var
- Allows redeclaration of variables with the same name
- Can overwrite global variables
- Risk of bugs due to variable hoisting
- Wide scope
- Function scope, not block scope
Boxing
Converting primitive types to object types.
const a = "Hello";
const aobj = new String(a);
aobj.length; // 5
Primitive types do not have fields or methods, so boxing is necessary, but in JavaScript, it is done implicitly. This is called auto-boxing.
const a = "Hello";
a.length; // 5
The object resulting from auto-boxing is called a wrapper object. For example, Boolean is the wrapper object for boolean. There are no wrapper objects for undefined and null.
Objects
Everything Except Primitives is an Object
// Primitives
const num = 1;
const str = "Hello";
// etc...
// Objects
const obj = { key: "value" };
const arr = [1, 2, 3];
const func = function() { return "Hello"; };
// etc...
Generators
Generators can return values using yield within a function.
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
console.log(g.next()); // { value: 1, done: false }
Basics of TypeScript
Type Annotation for Variable Declaration
You can assign types to variables.
const a: string = "Hello";
You can also use wrapper objects, but wrapper object types cannot be assigned to primitive types.
const a: Number = 0;
const b: number = a; // Type 'Number' is not assignable to type 'number'.'number' is a primitive, but 'Number' is a wrapper object. Prefer using 'number' when possible.
Also, operators cannot be used with wrapper object types.
const a: Number = 0;
const b = a + 1; // Operator '+' cannot be applied to types 'Number' and '1'.
It is recommended to use primitive types instead of wrapper object types.
Type Inference for Variable Declaration
Types are inferred.
let a = "Hello"; // a: string
a = 1; // Type 'number' is not assignable to type 'string'
Type Coercion
Even if the types are different, it may not result in an error.
"10" - 1; // 9
Type coercion is the implicit conversion to another type.
Literal Types
Types that can only take specific values.
let a: "Hello" = "Hello";
a = "World"; // Type '"World"' is not assignable to type '"Hello"'.
Primitive types that can be used as literal types are as follows.
- string type
- number type
- boolean type
any Type
A type that can accept any type.
let a: any = "Hello";
a = 1; // OK
When type inference cannot be made from context (e.g., when type annotation is omitted), it is implicitly treated as any type.
Objects
Type Annotation for Objects
const obj: { key: string } = { key: "value" };
Method type annotation is also possible.
const obj: { key: () => string } = { key: () => "value" };
There is also an object type, but since it represents all objects except primitive types, it is not recommended to use the object type. Also, the object type does not guarantee type safety.
const obj: object = { key: "value" };
obj.key; // Property 'key' does not exist on type 'object'.
readonly for Object Types
A modifier to make properties read-only.
const obj: { readonly key: string } = { key: "value" };
obj.key = "newValue"; // Cannot assign to 'key' because it is a read-only property.
It can also be written in a consolidated way.
const obj: Readonly<{
foo: string;
bar: number;
};>
Optional Property for Object Types
A modifier to make object properties optional.
let obj: { key?: string } = {};
obj = {} // OK
never Type
A type that holds no values.
function error(message: string): never {
throw new Error(message);
}
unknown Type
Like any type, it can accept any type, but unknown type guarantees type safety. It is used when the type is unknown.
let a: unknown = "Hello";
a = 1; // OK
const b: string = a; // Type 'unknown' is not assignable to type 'string'.
When using unknown type, explicitly specify the type using type assertion, typeof, or instanceof.
const a: unknown = "hello";
const b: string = a as string;
if (typeof a === "string") {
const c: string = a;
}
if (a instanceof String) {
const d: string = a as string;
}
Functions
Type Annotation for Function Declarations
function func(a: string, b: number): string {
return a + b;
}
Type Annotation for Function Expressions
const sayHi = function(name: string): string {
return "Hi, " + name;
}
Type Annotation for Arrow Functions
const sayHi = (name: string): string => {
return "Hi, " + name;
}
Function Type Declaration
You can declare only the type of a function without implementing it.
type SayHi = (name: string) => string;
const sayHi: SayHi = (name) => {
return "Hi, " + name;
}
Method syntax is also possible.
type Obj = {
sayHi: (name: string) => string;
}
Type Guard Functions
Functions that determine the type when it is unknown.
// a is string part is called type predicate
function isString(a: unknown): a is string {
return typeof a === "string";
}
const a: unknown = "Hello";
if (isString(a)) {
const b: string = a;
}
Assertion Functions
Functions that perform type assertions.
function isString(a: unknown): asserts a is string {
if (typeof a !== "string") {
throw new Error("Type assertion failed.");
}
}
const a: unknown = "Hello";
isString(a);
Overload Functions
Defining multiple functions with the same name that take different types of arguments.
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
return a + b;
}
add(1, 2); // 3
add("Hello", "World"); // HelloWorld
Classes
Type Annotation for Classes
const animal: Animal = new Animal();
Type Annotation for Class Constructors
class Animal {
constructor(name: string) {
this.name = name;
}
}
Type Annotation for Class Methods
class Animal {
sayHi(name: string): string {
return "Hi, " + name;
}
}
Nominal Type
Treating types as different even if they have the same name.
TypeScript does not support nominal types, so they are treated as structural types.
type Animal = {
name: string;
}
type Person = {
name: string;
}
const animal: Animal = { name: "Taro" };
To achieve nominal types, you need to change the structure (e.g., add properties).
Open-ended and Declaration Merging
Open-ended means that defining multiple interfaces with the same name does not result in a duplication error.
Declaration merging means that defining multiple interfaces with the same name results in them being merged.
interface Animal {
name: string;
}
interface Animal {
age: number;
}
const animal: Animal = { name: "Taro", age: 3 };
These features are useful, for example, when extending library type definitions.
By splitting type definition files, you can import only the necessary types.
Reusing Types
typeof
Get the type of a variable.
const a = "Hello";
type A = typeof a; // type A = string;
keyof
Get property names as types from an object type.
type Animal = {
name: string;
age: number;
};
type AnimalKey = keyof Animal; // type AnimalKey = "name" | "age";
Utility Types
Required
Make all properties required (≒ remove optional).
type Animal = {
name?: string;
age?: number;
};
type RequiredAnimal = Required<Animal>; // type RequiredAnimal = { name: string; age: number; };
Readonly
Make all properties read-only.
type Animal = {
name: string;
age: number;
};
type ReadonlyAnimal = Readonly<Animal>; // type ReadonlyAnimal = { readonly name: string; readonly age: number; };
Partial
Make all properties optional.
type Animal = {
name: string;
age: number;
};
type PartialAnimal = Partial<Animal>; // type PartialAnimal = { name?: string; age?: number; };
Record
Generate an object type where the property keys and values are Keys and Type, respectively.
type Name = string
type Age = number
type AnimalRecord = Record<Name, Age>; // type AnimalRecord = { [key: string]: number; };
Pick
Extract properties from type T specified by Keys.
type Animal = {
name: string;
age: number;
};
type Name = Pick<Animal, "name">; // type Name = { name: string; };
Omit
Exclude properties from type T specified by Keys.
type Animal = {
name: string;
age: number;
};
type Name = Omit<Animal, "age">; // type Name = { name: string; };
Exclude
Generate a union type by excluding types specified by U from type T.
type Animal = "dog" | "cat" | "rabbit";
type ExcludeAnimal = Exclude<Animal, "dog">; // type ExcludeAnimal = "cat" | "rabbit";
Extract
Generate a union type by extracting types specified by U from type T.
type Animal = "dog" | "cat" | "rabbit";
type ExtractAnimal = Extract<Animal, "dog">; // type ExtractAnimal = "dog";
NoInfer
Prevent type inference for type T.
type Animal = {
name: string;
age: number;
};
function getAnimal<T>(animal: T): T {
return animal;
}
const animal = getAnimal<NoInfer<Animal>>({ name: "Taro", age: 3 });
Mapped Types
Generate a new type based on a specified type.
type Animal = {
name: string;
age: number;
};
type ReadonlyAnimal = {
readonly [K in keyof Animal]: Animal[K];
};
Indexed Access Types
Get the type of a property or array element.
type Animal = {
name: string;
age: number;
};
type Name = Animal["name"]; // type Name = string;
type ArrayType = string[];
type ElementType = ArrayType[number]; // type ElementType = string;
Conditional Types
Change types based on conditions.
type IsString<T> = T extends string ? "string" : "not string";
type A = IsString<string>; // type A = "string";
infer
A type operator used in conditional types to obtain a type variable.
// Utility type to extract the return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Example function
function exampleFunction(): string {
return "Hello, World!";
}
// Get the return type of the function
type ExampleFunctionReturnType = MyReturnType<typeof exampleFunction>;
// ExampleFunctionReturnType is of type string
const exampleReturnValue: ExampleFunctionReturnType = "This is a string";
console.log(exampleReturnValue); // This is a string
Union Distribution
Distribute a union type and apply it to each type.
type A = "a" | "b";
type B = A extends "a" ? "c" : "d"; // type B = "c" | "d";
Generics
Types that accept types as arguments.
// Generics
function identity<T>(arg: T): T {
return arg;
}
const a = identity<string>("Hello");
const b = identity<number>(1);
// Type arguments
type Identity<T> = T;
type A = Identity<string>; // type A = string;
Thoughts
I have studied JavaScript several times before, but I felt like JavaScript was this difficult even before TypeScript...
References
-
typescriptbook.jp - Survival TypeScript
- Very well organized
Top comments (0)