Make TS Stick
March 13, 2024
Only suffering and pain stick, so we become proficient through them
#versusprivate- Make variable immutable
- Simple conversion from string to dictionary
stringvsString,numbervsNumber- A
Promise/resolveknowledge checker - use
...restin tuple type useUnknownInCatchVariables- Define a class using template literal
- Using tuple type can be unsafe
- Callback func is not definite in
.then - Type challenges
# versus private
#fieldNameis a JS private field, and it’s actually inaccessible outside of the class at runtimeprivate filedNameis a TypeScript private field, and while type-checking helps ensure we do not access it improperly, at runtime it’s accessible outside the class.
Make variable immutable
// "a" holds a immutable value and cannot be reassigned
const a = "AAAAAA";
// "b" holds a immutable value and can be reassigned
let b = "BBBBBB";
// "c" cannot be reassigned
const c = { learnAt: "web" };
// but the following is OK
c.learnAt = "xxxx";
// "d" is reassignable, and it holds a mutable value
let d = { learnAt: "cafe" };
// "e" is not reassignable, and holds an immutable value
const e = Object.freeze({ learnAt: "school" });
// the following is NOT OK
// e.learnAt="xxxx"
We can also use readOnly to make a property in an object not reassignable:
type ImmutableObject = {
readonly property: string;
};
const myImmutableObject: ImmutableObject = {
property: "This property cannot be changed",
};
// Trying to change these values will result in a TypeScript error
// myImmutableObject.property = "New value"; // Error
Simple conversion from string to dictionary
const str = "hello";
let val = { ...str.split("") };
console.log(val);
/**
* {
* '0': 'h',
* '1': 'e',
* '2': 'l',
* '3': 'l',
* '4': 'o'
* }
*/
string vs String, number vs Number
string and number are premitives, while String and Number are interfaces(defined in object-like fashion). Thus,
let a: string & number; // never
// a type contains properties shared by String & Number
let b: String & Number;
We can even extend a new interface from String and Number by resolving conflicting properties:
interface Bar extends String, Number {
valueOf(): never;
toString(): string;
}
A Promise/resolve knowledge checker
In what order will the animal names below be printed to the console?
function getData() {
console.log("elephant");
const p = new Promise((resolve) => {
console.log("giraffe");
resolve("lion");
console.log("zebra");
});
console.log("koala");
return p;
}
async function main() {
console.log("cat");
const result = await getData();
console.log(result);
}
console.log("dog");
main().then(() => {
console.log("moose");
});
The answer: dog, cat, elephant, giraffe, zebra, koala, lion, moose.
The explanation:
mainfunction runsconsole.log("cat")first,- jump into
getDatafunction - run
console.log("elephant") - executor function in
Promiseis called synchronously with thePromiseconstructor, hence,console.log("giraffe")runs firstresolve("lion")executes, but it does not return anyresultuntil the rest of the function is executedconsole.log("zebra")prints “zebra”console.log("koala")prints “koala”
- execute
console.log(result), printing “lion” - execute
console.log("moose")printing “moose”
use ...rest in tuple type
Define a tuple type whose first element is number, an enum the second which is followed by some other strings:
enum SecondElem = {
GroundBeef,
Lamb,
Pork
}
// ...string[] is the rest element here
type TupleType = [number, SecondElem, ...string[]];
const t: TupleType = [12, SecondElem.Pork, "str2"];
It’s a bit tricky when you create a function that manipulate tuple types like the one defined above. As it’s possible to lose some type information:
// exclude the first element in the passed tuple
function foo<T>(arg: readOnly [number, ...T[]]){
const [_ignored, ...rest] = arg;
return rest;
}
// tail has a type of (string|SecondElem)[]
const tail = foo([11,SecondElem.GroundBeef, "lettuce"])
The type of tail is (string|SecondElem)[], not [SecondElem, ...string[]]. To solve this issue, we can rewrite foo as:
function foo<T extends any[]>(arg: readOnly [number, ...T]){
// remain the same
}
const tail = foo([11,SecondElem.GroundBeef,"lettuce"])
// tail now has a type of [SecondElem, ...string[]]
When defining a tuple type, you can use multiple ... and they can be anywhere. But there can only be one ...rest[]:
type GoodType1 = [...[number, string], ...string[]];
// compiler error
type BadType = [...number[], ...string[]];
type GoodType2 = [string, ...number[], string];
useUnknownInCatchVariables
Turn useUnknownInCatchVariables in tsconfig.json to true, to formulate your catch clause as the following:
try{
somethingRisky()
} catch (error unknown){
if(err instanceof Error) throw err
else throw new Error(`${err}`);
}
Define a class using template literal
The DataStore class below gives some sort of type error that alerts you that you’ve broken the established pattern if you mis-name a method on the class(e.g., getSongs instead of getAllSong).
If you add a new entity like Comic (shown below) and make no other changes to your solution, you should get some sort of type error that alerts you to the absence of a clearComics, getAllComics and getAllSongs method.
export interface DataEntity {
id: string;
}
export interface Movie extends DataEntity {
director: string;
}
export interface Song extends DataEntity {
singer: string;
}
export type DataEntityMap = {
movie: Movie;
song: Song;
};
export type Setter = {
[K in keyof DataEntityMap as `add${Capitalize<K>}`]: (
arg: DataEntityMap[K],
) => void;
};
export type Getter = {
[K in keyof DataEntityMap as `get${Capitalize<K>}`]: (
arg: string,
) => DataEntityMap[K] | undefined;
};
export type GetAll = {
[K in keyof DataEntityMap as `getAll${Capitalize<K>}`]: () => DataEntityMap[K][];
};
export type Clearer = {
[K in keyof DataEntityMap as `clear${Capitalize<K>}`]: () => void;
};
export type StoreMethods = Setter & Getter & GetAll & Clearer;
export class DataStore implements StoreMethods {
// private field whose type is defined by using Record
#data: { [K in keyof DataEntityMap]: Record<string, DataEntityMap[K]> } = {
song: {},
movie: {},
};
constructor(
public movies: { [key: string]: Movie } = {},
public songs: { [key: string]: Song } = {},
) {
this.#data.song = this.songs;
this.#data.movie = this.movies;
}
addMovie(arg: Movie) {
this.movies[arg.id] = arg;
}
addSong(arg: Song) {
this.songs[arg.id] = arg;
}
getSong(arg: string) {
return this.songs[arg];
}
getMovie(arg: string) {
return this.movies[arg];
}
getAllSong() {
return Object.values(this.songs);
}
getAllMovie() {
return Object.values(this.movies);
}
clearSong() {
this.songs = {};
}
clearMovie() {
this.movies = {};
}
}
Using tuple type can be unsafe
The following snippet compiles because tuples are a specialized flavor of arrays, they expose the entire array API.
const vector3: [number, number, number] = [3, 4, 5];
vector3.push(6);
Callback func is not definite in .then
The callback passed to .then is not regarded as a “definite assignment”. In fact, all callbacks are treated this way. Because of this, the following won’t compile:
class Person {
name: string
constructor(userId: string) {
// Fetch user's name from an API endpoint
fetch(`/api/user-info/${userId}`)
.then((resp) => resp.json())
.then((info) => {
this.name = info.name // set the user's name
})
}
}
Type challenges
You can try to solve the type challenges at this github repo
You can find videos explaining solutions in the playlist here