The following text is a summary based on my understanding after reading "Effective TypeScript"
6. Search Type System using IDE
IDE means code editor, and vscode support Typescript
tooling especially. And vscode is 90% made up of Typescript
, if we check out the vscode repository.
It's really nice to understand how Type System works using code editor(vscode), and how TypeScript
infers types. Additionally, we have to check out d.ts
file to get to know how TypeScript
works.
7. Think of Type as set of values
JavaScript
is called as Dynamic Type Language which type is set on runtime dynamically. In a type-checking system with a type checker, the moment it checks for errors before compiling TypeScript
code, it has something called a "type".
It had better to say type as set of assignable values.
1. smallest set : the union
const x: never = 12;
It's impossible to assign the value on variable x
, because the range of value which is declared as never
type is the union.
2. Second smallest set: type that contains only one value - Unit Type or Literal Type
type A = "A";
type B = "B";
type Twelve = 12;
3. Third smallest set: Union Type
type AB12 = "A" | "B" | 12;
Like the type above, AB12
type can be the union using Union Type
.
💿 We may see the phrase "assignable" in
Typescript
errors. As a perspective of Collection, this phrase can be understood as subsets of ~ (types and their relationships) or elements of ~ (values - their relationships).
4. Fourth smallest set : Structural Typing with Interface
interface Person {
name: string;
}
Structural Typing Ruleset has not 'sealed' property which means some value can possess other values, but 'open/public' property
For example, &
operator means 'Intersection' of 2 types.
interface Person {
name: string;
}
interface LifeSpan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & LifeSpan;
Since the 2 interfaces above don't have properties in common, we might think of PersonSpan
type as the union such as never
type.
But, since a type declaration with Type Alias applies to a set of values rather than Interface property, values with additional properties can also be considered to be part of the set of the type. Thus, Intersection type that has the name, birth, and death properties would be part of the type Intersection.
const person: PersonSpan = {
name: "Kim",
birth: new Date("1912/06/23"),
death: new Date("1954/06/07"),
};
It would seem that applying the above features to unionize an inteface type would result in the result of an Intersection, but this is not the case.
type K = keyof (Person | LifeSpan);
A common way to declare the above PersonSpan
type is to utilize extends
keyword to create subtypes, which in the context of a set is to create a subset.
interface Person {
name: string;
}
interface LifeSpan extends Person {
birth: Date;
death?: Date;
}
If you think about the keyword "sub," you can create sub-classes of a class by inheriting from it using the extends
keyword. So linguistically, we can say that there is an inheritance relationship, but from a set perspective, we can say that Person
has a larger scope and that LifeSpan
type, which has the properties of Person
, is inside the scope of the Person type.
The extends
keyword is also used as a constraint method in Generic Types: it means a subset of some types.
const getValue = <K extends string>(obj: Person, key: K): boolean => {
const value = obj.name;
return value === key;
};
We can infer that K
is some type whose scope is a subset of string by the extends
keyword.
8. Distinguish between symbols in type and value spaces
Symbols in Typescript
exist in either type space or value space. It's important to note even if they have the same name, they can represent different things depending on which space they belong to.
interface Square {
width: number;
height: number;
}
const Square = (width: number, height: number): Square => ({ width, height });
It can be confused, because Square
Symbol is being used to define interface and constructor function.
The code below might be even more confusing.
interface Person {
firstName: string;
lastName: string;
}
const p : Person = { firstName: 'gildong', lastName: 'kim' }
const email = (p: Person, subject: string, body: string): Response => {
...
}
type T1 = typeof p; // Person
type T2 = typeof email; // (p: Person, subject: string, body: string) => Response
const person = typeof p; // 'object'
const e = typeof email; // 'function'
-
As a perspective of Type
typeof
reads a value and returns a type. Atypeof
in a type space can be used as part of a larger type, or it can be named in a type declaration syntax utilizing a type alias.
-
As a perspective of Value
-
typeof
is the typeof operator atJavaScript
runtime, so it returns the corresponding type as a string.
At the runtime,
JavaScript
returns the runtime type such asnumber, string, boolean, undefined, object, function
-
The property accessor []
behaves the same when used as a type. Always utilize []
to get the type attribute of a property value that corresponds to a property key of an object.
const firstName: Person["firstName"] = p["firstName"];
The contents below are examples of a value space and a type space serving as different purposes.
💿
This
as a value is the this keyword inJavaScript
. 🚀 this as a type is aTypeScript
type ofthis
called polymorphic this.
💿 The
&
and|
in the value are the bitwiseAND
andOR
operators. 🚀 types are called intersection and union types.
💿
const
in the value is used to declare constants. 🚀 asconst
changes in the type the inferred type of a literal or literal expression.
💿 As used in the value,
extends
can be used to define subclasses of a parent class. 🚀extends
on types can be used as a qualifier for subtypes or generic types.
💿 Additionally, keywords like
class
andenum
can be used as both types and values.
9. Using Type Declaration instead of Type Assertion
1. There are 2 options to set a type and assign a value to a variable
interface Flower {
name: string;
}
const rose: Flower = { name: "rose" };
const forsythia = { name: "forsythia" } as Flower;
- A type declaration checks that the value being assigned satisfies the interface.
- A type assertion is like telling the type checker to ignore an error because we force a type.
2. The type declaration of an arrow function can be ambiguous about the inferred type, sometimes.
const flowers = ["rose", "forsythia", "sunflower"].map((flower) => ({
flower,
}));
// Type '{ flower: string; }[]' is not assignable to type 'Flower[]'.
As a developer, you might infer from the above that the return value is an array type which has Flower
Interface value, but the type checker will not recognize it as an interface named Flower
.
Because of the above issues, using type assertions can cause problems at runtime.
So, we check the type by declaring the return type of the arrow function, rather than asserting the type.
const flowers: Flower[] = ["rose", "forsythia", "sunflower"].map(
(flower): Flower => ({ name: flower })
);
3. Helpful Moment using Type Assertion
Because Typescript
doesn't have access to the DOM, you need to actively utilize type assertions.
document.querySelector(".plus-button")!.addEventListener("click", (e) => {
console.log(e.currentTarget); // document.querySelector('.plus-button')
const button = e.currentTarget as HTMLButtonElement;
});
Because developers know something that TypeScript
doesn't, developers can write type assertions with the keyword as
.
Or, we can use !
to assert value is not possible null
, alternatively.
const $elem = document.getElementById("foo"); // HTMLElement | null
const $buttonElem = document.getElementById("button")!; // HTMLElement```
Reasons for using Type Assertion above the 3 examples
- #1 the
HTMLElement
type is a subtype ofHTMLElement | null
. - #2 the type
HTMLButtonElement
is a subtype ofEventTarget
. - #3 the type
Flower
is a subtype of{ }
.
interface Flower {
name: string;
}
const $body = document.body;
const $elem = $body as Flower;
// Conversion of type 'HTMLElement' to type 'Flower' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
As shown above, code editor is saying that it would be a mistake to convert the HTMLElement
type to the Flower
type. If a type assertion is intentional, utilizing the unknown
type might be useful.
We can say that every type is a subtype of
unknown
, so assertions withunknown
will always work.The
unknown
type assertion allows conversion between arbitrary types, but can introduce a dangerous situation where subsequent types are unpredictable.
10. Avoid Object Wrapper Type
JavaScript
has the character to freely convert between primitive and object types.
"hello".charAt(3);
The method called charAt
is not a method on a value of type string.
In JavaScript
, there's a lot of works going on internally when you use a string value in the code above. JavaScript
doesn't have methods on values that are the string primitive type, but it does have methods on String object types.
When we use a method like charAt
on string primitive, JavaScript
goes through the process of wrapping the value of string
type into a String
object, calling the method, and finally discarding the wrapped object.
Similar to how the string primitive is automatically converted to a String wrapper object, other object wrapper types exist.
number
↔️Number
boolean
↔️Boolean
symbol
↔️Symbol
bigint
↔️BigInt
It will be automatically converted as shown above so that you can declare the type as shown below.
const s: String = "hello";
const n: Number = 20;
In the code above, the type of the value Code eventually returns at JavaScript
runtime is a primitive. Typescript
allows declarations that assign a primitive type to an object wrapper.