[Typescript] Make Typescript Stick
Refer: https://www.cnblogs.com/Answer1215/p/15084496.html
A string is a primitive value, and all primitive values are immutable.
Q1:
const a = "Frontend Masters"
let b = "Frontend Masters"
const c = { learnAt: "Frontend Masters" }
let d = { learnAt: "Frontend Masters" }
const e = Object.freeze({ learnAt: "Frontend Masters" })
.
.
.
.
.
.
Answer:
All primitive values are immutable, therefore a & b are both hold immutable values.
Repeat "All primitive values are immutable" three times...
All primitive values are immutable
All primitive values are immutable
All primitive values are immutable
Const and Let differ in terms of whether variables can be reassigned, but that has nothing to do with whether the values they hold can be modified.
Therefore both c & d does NOT hod ummutable values
Object.freeze prevents properties of an object from being changed, and prevents new properties from being added. This effectively is a "Shallow immutability". Which means Object.freeze won't help you mutate a nested object.
const e = Object.freeze({ course: 'Typescript', learnAt: {place: "Frontend Masters"} })
e.course = "Reacct" // report error
e.learnAt.place = "Udmey" // works
Q2:
const str = "hello"
let val =
/* ??? */
console.log(val)
/**
* {
* '0': 'h',
* '1': 'e',
* '2': 'l',
* '3': 'l',
* '4': 'o'
* }
*/
.
.
.
.
.
.
Answer:
let val = { ...str.split("") }
console.log(val)
/**
* {
* '0': 'h',
* '1': 'e',
* '2': 'l',
* '3': 'l',
* '4': 'o'
* }
*/
Q3:
Look at the types of
first
andsecond
below, as well as the compile error messages. What does your mental model tell you about howstring
andString
are different?
let first: string & number
let second: String & Number
first = "abc"Type 'string' is not assignable to type 'never'.
second = "abc"Type 'string' is not assignable to type 'String & Number'. Type 'string' is not assignable to type 'Number'.
second = new String("abc")Type 'String' is not assignable to type 'String & Number'. Type 'String' is missing the following properties from type 'Number': toFixed, toExponential, toPrecision
for the first: both string and number are primtive types, they are not such a value is both string and number at the same time, so that it results in a `never`.
for the second: String and Number are interface types. The Union of String and Number are common methods and props of both, but it won't results in a never type.
Q4:
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")
})
.
.
.
.
.
.
.
Answer:
"dog" will come first
Then main() function will be invoked, so "cat" will come second.
Then getData() function will be invoked, so "elephant" will come third.
new Promise() constructor function will be invoked, 4th. "giraffe"
resolve() function will be invoked by the time we call .then()
so "zebra" comes 5th.
then "koala" comes 6th.
after await getData(), console.log(result) will run, so "lion" 7th.
"moose" will be last
- Are you surprised that
giraffe
andzebra
happen so early? Remember thatPromise
executors are invoked synchronously in thePromise
constructor - Are you surprised that
lion
happens so late? Remember that aresolve
is not areturn
. Just because aPromise
has resolved, doesn’t mean the corresponding.then
(orawait
is called immediately)
Does it compile?
let age = 38
age = Number.NaN
.
.
.
Answer: Yes
Number.NaN is still a number type
const vector3: [number, number, number] = [3, 4, 5]
vector3.push(6)
.
.
.
Answer: Yes
Unfortunately, because tuples are a specialized flavor of arrays (and at runtime, they actually are just regular arrays) they expose the entire array API. Look at the type signature of .push()
type Color = {
red: number
green: number
blue: number
}
interface Color {
alpha: number
}
.
.
.
Answer: No
Duplicate identifier 'Color'
But if both are interface type `Color`, then it will compile
class Person {
name: string
friends: Person[]
constructor(name: string) {
this.name = name
}
}
.
.
.
Answer: NO
Property 'friends' has no initializer and is not definitely assigned in the constructor.
If you give a default value, then it will
class Person {
name: string
friends: Person[] = [];
constructor(name: string) {
this.name = name
}
}
abstract class Person {
public abstract name: string
}
class Student extends Person {
public name: string | string[] = ["Mike North"]
}
.
.
.
Answer: No
Property 'name' in type 'Student' is not assignable to the same property in base type 'Person'. Type 'string | string[]' is not assignable to type 'string'. Type 'string[]' is not assignable to type 'string'.
Basicly you cannot loosen the types, from just `string` to `string | string[]`.
About abstract:
difference between abstract class and interface is interface only provide type information, but doesn't contain implementation
But abstract class can have some impelmentation as well.
interface Color {
red: number
green: number
blue: number
}
function printColor(color: Color) {
// ... //
}
printColor({
red: 255,
green: 0,
blue: 0,
alpha: 0.4,
})
.
.
.
Answer: NO
Object literal may only specify known properties, and 'alpha' does not exist in type 'Color'.
Because in printColor(color: Color), `color` won't be able to `alpha`, because TS has limit what you can get from Color type.
If you change the code to:
interface Color {
red: number
green: number
blue: number
}
function printColor(color: Color) {
// ... //
}
const c = {
red: 255,
green: 0,
blue: 0,
alpha: 0.4,
};
printColor(c)
console.log(c.alpha)
This will compile, because now we can access `alpha` from `c`.
type Color = {
red: number
green: number
blue: number
}
class ColorValue implements Color {
constructor(
public red: number,
public green: number,
public blue: number
) {}
}
.
.
.
Answer: YES
The same code as:
type Color = {
red: number
green: number
blue: number
}
class ColorValue implements Color {
public red;
public green;
public blue;
constructor(
red: number,
green: number,
blue: number
) {
this.red = red;
this.green = green;
this.blue = blue;
}
}
export class Person {
name: string = ""
}
interface Person {
age?: number
}
.
.
.
Answer: NO
Individual declarations in merged declaration 'Person' must be all exported or all local.
Which means:
class Person {
name: string = ""
}
interface Person {
age?: number
}
// or
export class Person {
name: string = ""
}
export interface Person {
age?: number
}
That will compile.
And class Person and interface Person will be merged.
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
})
}
}
.
.
.
Answer: NO
Property 'name' has no initializer and is not definitely assigned in the constructor.
The callback passed to .then
is not regarded as a “definite assignment”. In fact, all callbacks are treated this way.
enum Language {
TypeScript = "TS",
JavaScript,
}
enum Editor {
SublimeText,
VSCode = "vscode",
}
enum Linter {
ESLint,
TSLint = "tslint",
JSLint = 3,
JSHint,
}
.
.
.
Answer: NO
Once you provide a string initializer for an enum member, all following enum members need an explicit initializer of some sort, unless you go back to numeric enum values, at which point inference takes over again.
function handleClick(evt: Event) {
const $element = evt.target as HTMLInputElement
if (this.value !== "") {
this.value = this.value.toUpperCase()
}
}
.
.
.
Answer. NO
When you have a free-standing function like this, and refer to the this
value, we need to give it a type of some sort.
'this' implicitly has type 'any' because it does not have a type annotation.
In React, we often do `this.value.bind(this)`
or we can fix like this:
function handleClick(this: HTMLInputElement, evt: Event) {
const $element = evt.target as HTMLInputElement
if (this.value !== "") {
this.value = this.value.toUpperCase()
}
}
class Person {
#name: string
private age: number
constructor(name: string, age: number) {
this.#name = name
this.age = age
}
}
class Student extends Person {
#name: string | string[]
private age: number
constructor(name: string, age: number | null) {
super(name, age || 0)
this.#name = name
this.age = age
}
}
.
.
.
Answer: NO
Type 'number | null' is not assignable to type 'number'. Type 'null' is not assignable to type 'number'.
Because TS private
fields are just “checked for access at build time” and are totally accessible outside the class at runtime, there’s a collision between the two age
members.
As a result Student
is not a valid subclass of Person
Class 'Student' incorrectly extends base class 'Person'. Types have separate declarations of a private property 'age'.
So what if you change to:
class Person {
#name: string
#age: number
constructor(name: string, age: number) {
this.#name = name
this.#age = age
}
}
class Student extends Person {
#name: string | string[]
#age: number
constructor(name: string, age: number | null) {
super(name, age || 0)
this.#name = name
this.#age = age
}
}
For extends class, they cannot have the same private field, but can have both public or #private field
.
.
.
Answer: NO
class Person {
#name: string
constructor(name: string) {
this.#name = name
}
}
function makeName(name: string | string[]): string {
if (Array.isArray(name)) return name.join(" ")
else return name
}
class Student extends Person {
#name: string | string[]
constructor(name: string | string[]) {
super(makeName(name))
this.#name = name
}
}
.
.
.
Answer: YES
Because Ecma #private
fields are not visible, even at runtime, outside of the class, there’s no collision between the two #name
members.