1.看道题先
type myObj = {
x: number,
y: number,
};
function getSum(obj:myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
const v = obj[n]; // 报错
sum += Math.abs(v);
}
return sum;
}
当浏览器编译到 const v = obj[n];就会飘红。要理解这个,我们就需要知道索引签名。
索引签名是ts中用于描述对象和类可以通过索引(类似于数组索引或对象属性名)访问对象的方法。索引签名允许我们定义对象或者类的索引类型以及对应索引值的类型
// 使用索引签名定义字符串索引类型和对应值的类型
interface StringDictionary {
[key: string]: string;
}
// 使用索引签名定义数字索引类型和对应值的类型
interface NumberDictionary {
[index: number]: number;
}
// 使用索引签名定义混合索引类型和对应值的类型
interface MixedDictionary {
[key: string]: string | number;
}
// 使用索引签名定义类的索引类型和对应值的类型
class MyArray {
[index: number]: string;
constructor() {
// 实现类的索引签名
}
}
而在我们的这段代码中,它的索引签名是这样的
type myObj = { x: number, y: number, };
//索引签名是这样的
{ [key: "x" | "y"]: number }
我们可以看到他的索引签名分别是字符串x和y,所以我们在通过对象属性名来访问属性值时,要满足两个条件,一是属性名是x和y,二是x和y都得是字符串类型。ok理解这个就好解释为什么我们代码会飘红了。
代码飘红是发生在ts编译成js的过程中的,此时代码并没有执行,因此我们通过obj[n]动态的形式去访问对象的属性值时,n的类型是any,因为并没有执行代码,n作为一个变量可以是任何类型。当我们通过类型是any的属性名去访问时,得到的自然是类型为any的属性值。因此此时v的类型就是any,而sum += Math.abs(v); 要求我们的v的类型是number,自然就会发生飘红了。
因此我们的解决方式就是通过类型断言,明确告诉ts,v的类型是number即可
type myObj = {
x: number,
y: number,
};
function getSum(obj: myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
// 使用类型断言告诉 TypeScript v 是 number 类型
const v = obj[n] as number;
sum += Math.abs(v);
}
return sum;
}
interface UserItem {
name: string
age: number
enjoyFoods: string[]
// 这个属性引用了本身的类型
friendList: UserItem[]
}
const petter: UserItem = {
name: 'Petter',
age: 18,
enjoyFoods: ['rice', 'noodle', 'pizza'],
friendList: [
{
name: 'Marry',
age: 16,
enjoyFoods: ['pizza', 'ice cream'],
friendList: [],
},
{
name: 'Tom',
age: 20,
enjoyFoods: ['chicken', 'cake'],
friendList: [],
}
],
}
第一反应就是在UserItem里引用了UserItem会类似对象一样产生嵌套,但是实际上不会,UserItem仅仅是代表类型,而空数组是任意数组类型的有效子类型。这是因为空数组不包含任何元素,因此它自然满足任何元素类型的要求(在这种情况下,元素类型是 UserItem)。
3.Qmit是在ts继承时可以删除掉的属性
type Omit<T, K extends string | number | symbol>
其中 T 代表已有的一个对象类型, K 代表要删除的属性名,如果只有一个属性就直接是一个字符串,如果有多个属性,用 | 来分隔开。
表示 K 是 string、number 或 symbol 类型之一的子集。这意味着 K 可以是任何字符串、数字或符号类型,这些类型是对象属性的键的类型。
interface UserItem {
name: string
age: number
enjoyFoods: string[]
friendList?: UserItem[]
}
// 这里在继承 UserItem 类型的时候,删除了两个多余的属性
interface Admin extends Omit<UserItem, 'enjoyFoods' | 'friendList'> {
permissionLevel: number
}
// 现在的 admin 就非常精简了
const admin: Admin = {
name: 'Petter',
age: 18,
permissionLevel: 1,
}
4.函数重载
// 对单人或者多人打招呼
function greet(name: string | string[]): string | string[] {
if (Array.isArray(name)) {
return name.map((n) => `Welcome, ${n}!`)
}
return `Welcome, ${name}!`
}
// 虽然已知此时应该是 string[]
// 但 TypeScript 还是会认为这是 string | string[]
const greetings = greet(['Petter', 'Tom', 'Jimmy'])
// 会导致无法使用 join 方法
const greetingSentence = greetings.join(' ')
console.log(greetingSentence)
可以看到我们本来的期望是如果参数是string类型,我们就输出string类型;如果是string[]那我们就返回string[],但是实际上无论我们的参数是什么类型,输出的类型都是联合类型string | string[]。我们可以通过类型断言来强制确定类型,但是这无疑会增加我们的心智负担。因此可以使用类型重载来解决:
// 这一次用了函数重载
function greet(name: string): string // TS 类型
function greet(name: string[]): string[] // TS 类型
function greet(name: string | string[]) {
if (Array.isArray(name)) {
return name.map((n) => `Welcome, ${n}!`)
}
return `Welcome, ${name}!`
}
// 单个问候语,此时只有一个类型 string
const greeting = greet('Petter')
console.log(greeting) // Welcome, Petter!
// 多个问候语,此时只有一个类型 string[]
const greetings = greet(['Petter', 'Tom', 'Jimmy'])
console.log(greetings)
// [ 'Welcome, Petter!', 'Welcome, Tom!', 'Welcome, Jimmy!' ]
上面是利用函数重载优化后的代码,可以看到一共写了 3 行 function greet … ,区别如下:
第 1 行是函数的 TS 类型,告知 TypeScript ,当入参为 string 类型时,返回值也是 string ;
第 2 行也是函数的 TS 类型,告知 TypeScript ,当入参为 string[] 类型时,返回值也是 string[] ;
第 3 行开始才是真正的函数体,这里的函数入参需要把可能涉及到的类型都写出来,用以匹配前两行的类型,并且这种情况下,函数的返回值类型可以省略,因为在第 1 、 2 行里已经定义过返回类型了。