1.1TypeScript的介绍
TS是JavaScript的超集(js有的TS都有)
TypeScript = type + JavaScript(在js的基础上,为js添加类型支持)
ts相比js的优势
1.更早的发现错误,减找少Bug,改Bug的时间,提升开发效率。
2.程序任何位置都有代码提示,增加开发体验
3.强大的类型系统提升的代码的可复用性,使得重构代码更加容易
4.支持最新的ECMAscript语法
5.TS的类型推断机制,不需要在代码的每一个地方都显示标注类型
1.2安装编译TS的工具包
node.js只认识js代码不认识ts代码,所以童工tsc命令,实现ts -> js的转换。
安装命令:npm i -g typescript
验证安装是否成功:tsc -v(查看typescript的版本)
- 编译运行ts代码
- 创建hello.ts文件,在终端输入 tsc hello.ts(会出现一个同名的js文件)
- 执行js文件,输入 node hello.js
- 优化ts运行步骤(内部还是将ts转换为js,在运行js代码)
- 使用ts-node包,直接在node当中运行ts代码
- 安装命令: npm -g ts-node
- 使用方法:ts-node hello.ts
1.3TypeScript常见的数据类型
ts新增数据类型:联合类型,自定义类型,接口,元组,字面量类型,枚举,void,any等
1.3.1类型注解
1.数字类型
let age:number = 18
:number就是类型注解,约定变量必须是number
2.数组类型的2种写法
let numbers:number[] = [1,2,3,4]
let strs:Array<string> = ['a','b']
如果数组当中既有数字也有字符串
let arr:(number | string)[] = [1,'a',2,'b']
| 叫联合类型 (当有多个类型组合的类型,表示可以是这些类型的任意一种)
1.3.2类型别名
let arr1: (number | string)[] = [1, 2, 3, 'one', 'two', 'three']
let arr2: (number | string)[] = [1, 2, 3, 'on', 'tw', 'three']
//上面2个类型重复了可以使用类型别名
// 类型别名
type CustArray = (number | string)[]
let arr3: CustArray = [1, 2, 3, 13, 'af', 'afa']
1.3.3函数类型
指的是函数参数和函数返回值的类型
1.单独指定参数,返回值的类型:
//1.单独指定参数,返回值类型
function add(number1:number,number2:number):number{
return number1+number2
}
console.log(add(1,2));
//函数表达式
const add2 = (number1:number,number2:number):number =>{
return number1-number2
}
console.log(add2(3,1));
2.同时指定参数,返回值类型
const add3:(number1:number,number2:number) => number = (number1,number2) => {
return number1+number2
}
3.函数没有返回值的时候 添加void
//函数没有返回值的时候 添加void
function greet(name: string): void {
console.log('hello', name)
}
greet('cui')
1.3.4函数的可选参数
不加问号就表示必须传递2个参数
// 必选参数必须在可选参数之前 在函数参数之后添加?
function mySlice(start?: number, end?: number): void {
console.log('开始', start, '结束', end)
}
mySlice()
mySlice(1)
mySlice(1, 2)
1.3.5对象类型
let person:{name:string;age:number;sayHi():void,greet(name:string):void} = {
name:'崔',
age:18,
sayHi() {
},
greet(name){}
}
// 多行可以省略;
let person: {
name: string
age: number
sayHi:()=>void
greet(name: string): void
} = {
name: '崔',
age: 18,
sayHi() {
},
greet(name) { }
}
1.3.6对象的可选属性
function myAxios(config: { url: string; method?: string }) { }
myAxios({
url: ''
})
1.3.7接口
当一个对象的类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的
步骤:
- 使用interface关键字来声明接口
- 接口名称是合法的变量名称
- 声明接口后直接使用接口名称作为变量的类型
- 因为每一行只有一个属性类型,因此属性后面没有;
接口 interface
interface IPerson {
name: string
age: number
sayHai(): void
}
类型别名
type IPerson = {
name: string
age: number
sayHai(): void
}
let person: IPerson = {
name: '崔',
age: 18,
sayHai() {
}
}
interface(接口) 和 type(类型别名)的区别对比
1.相同点:都可以为对象指定类型
2.不同点:
- 接口只可以为对象指定类型
- 类型别名,不仅可以为对象指定别名,实际还可以为任意类型指定别名
1.3.8接口继承
通过extends继承后会获取所有的属性跟方法
//接口继承 extends
interface Ponint2D { x: number; y: number }
// interface Ponint3D {x:number;y:number;z:number}
// 使用继承实现复用
interface Ponint3D extends Ponint2D { z: number }
let p3: Ponint3D = {
x: 1,
y: 2,
z: 3
}
1.3.9元组
// let position:number[] = [1,354,56,465,46,54,5]
// 使用元组 记录位置信息 中括号里面标记有多少个元素以及每个元素的类型
let position: [number, number] = [39, 55] // 必须是2个数字的数组
1.3.10类型推论
//1. 声明 变量并立即赋值的时候可以省略类型
let age = 18
// 如果声明变量没有立刻赋值,此时必须要手动添加类型注解
let a: number
a = 15
// 2.决定函数返回值的时候,可以省略函数返回值的类型(必须指定参数的类型)
function add(number1:number,number2:number){
return number1+number2
}
1.3.11类型断言
当你比ts更加明确一个值的类型,此时可以用类型断言来指定具体类型
比如 const alink = document.getElementById('link') 此时只能获取HTMLElement的公共属性,不能获取自己独有的属性比如a标签的href属性,所以as后面加上类型断言
// 写法1 const alink = <HTMLAnchorElement>document.getElementById('link')
// 写法2
const alink = document.getElementById('link') as HTMLAnchorElement;
alink.id
console.dir($0)来获取标签的类型 例如a标签(HTMLAnchorElement)
1.3.12字面量类型
str1的类型为string
str2的类型为'hello ts' 这个就是一个字面量类型
str3也是一个字面量类型 'cui'
字面量的值只能是字面量自己,字面量类型一般配合联合类型 | 使用
let str1 = 'hello ts'
const str2:'hello ts' = 'hello ts'
const str3 = 'cui'
let age:18 = 18
// 使用场景 贪吃蛇上下左右
function changeDir(direction:'up'|'down'|'left'|'right'){
}
changeDir('down')
1.3.13枚举类型(会被编译为js代码,其他类型不会,会被移除)
枚举:定义一组命名常量,它描述一个值,该值可以是这些命名常量当中的一个(会被编译成js代码)
enum
通过 . 语法来访问枚举当中的成员
数字枚举具有自增的情况
// 枚举
enum Directio{
up,
down,
left=18, // 数字枚举
right
}
function changeDire (direction:Directio) {
console.log(direction);
}
changeDire(Directio.left)//19 只能是枚举成员当中的一个
字符串枚举
所有枚举成员的值都必须初始化为字符串
注意:字符串枚举没有自增行为,所以枚举成员必须赋值
// 枚举
enum Directio{
up ="up",
down ="down",
left ="left",
right ="right"
}
function changeDire (direction:Directio) {
}
changeDire(Directio.left)//19 只能是枚举成员当中的一个
console.log(Directio); //{ up: 'up', down: 'down', left: 'left', right: 'right' }
1.3.14 any类型
原则:不推荐使用any这会让ts类型失去保护的优势,因为当值的类型为any时,可以对值镜像任何操作,并且不会有代码提示
其他隐试具有any类型的情况:1.声明变量时既不给类型也不付初始值 2.函数参数不加类型
// 不推荐使用any 这会让ts失去类型保护的优势
//隐试具有any类型的情况 1.声明变量不提供类型也不提供值 2.函数参数不加类型
let a
function add(num1, num2) { }
let obj: any = { x: 0 };
obj.abc = true
obj()
//赋值给其他类型的变量
const n : number = obj
1.3.15 typeof
使用typeof来获取变量p的类型,结果与对象字面量形式的类型相同,typeof出现在类型注解的位置,所处的环境就是类型的上下文,注意:typeof 只能查询变量或属性的类型,不能查询其他形式的类型(如函数调用的类型)
console.log(typeof 'hello world'); //string
let p = {x: 0, y: 1}
function test(point:typeof p) { }
// function test(point:{x:number;y:number}) { }
test(p)
let num: typeof p.x
function add(num1:number,num2:number) {
return num1 + num2
}
// let ret : typeof add(1,2) //typeof 只能查询变量或属性的值,不能查询其他形式的类型(如函数调用的类型)
1.4ts的高级类型
1.4.1class类
ts引入的class关键字,并为其添加类型注解和其他语法(比如可见修饰符等)
ts当中的class,不仅提供了class的语法功能,也作为一种类型存在
实例属性初识化:
class Person {
age: number
// gender:string ='男'
gender ='男'
}
const p = new Person()
p.age
p.gender
构造函数:
class Person {
age: number
gender: string
constructor(age: number, gender: string) {
this.age = age
this.gender = gender
}
}
//constructor() 方法是一种特殊的方法(构造方法),用于创建和初始化在类中创建的对象。
// 创建对象时会自动调用构造方法 constructor()。
// 如果没有显式指定构造方法,则会添加默认的 constructor 方法。
// 如果不指定一个构造函数 (constructor) 方法,则使用一个默认的构造函数 (constructor)。
// 在一个构造方法中可以使用 super 关键字来调用一个父类的构造方法。
const p = new Person(18, '男')
console.log(p.age,p.gender);//18 男
注意:成员初识化后才可以使用this.age来访问实例成员,需要为构造函数指定类型注解,否者会隐试推断为any,构造函数不需要返回值类型
实例方法:方法的类型注解与函数相同
class Point {
x = 0; y = 1;
scale(n: number):void{
this.x *= n;
this.y *= n;
}
}
const q = new Point()
q.scale(2)
console.log(q.x, q.y) //0 2
类的继承:
1.extends(继承父类) 2.implements (实现接口)
js只有extends ,implements是ts提供的
extends(继承父类)
class Animal {
age = 18
move() { console.log('不要狗叫');
}
}
// extends 继承父类所有的属性跟方法
class Dog extends Animal {
name = '二哈'
bark() { console.log('汪汪') }
}
const dog = new Dog()
dog.move()
dog.bark()
console.log(dog.age);
console.log(dog.name);
implements (实现接口)
interface Sing {
sing(): void
age:number
}
//implements 让class实现接口 Song 实现Sing接口 意味着Song提供Sing提供的属性跟方法
class Song implements Sing {
age = 18
sing() {
console.log('唱歌')
}
}
成员可见性(public)[ˈpʌblɪk]
//父类
//public 表示公开的 公共成员可以被任何成员访问,默认就是public
class Animal {
public move() {
console.log('跑步');
}
}
//子类
class Dog extends Animal {
bark() {
console.log("汪汪");
}
}
const d = new Dog()
d.move()
受保护的protected([prəˈtektɪd])
//protected 受保护的 仅在声明的类跟子类当中可见 (实例对象是不可见的)
// 子类可以通过this来访问父类中被保护的成员
class Animal {
protected move() {
console.log('gogo');
}
}
class Dog extends Animal {
back() {
console.log('王');
this.move();
}
}
const r = new Animal()
// r.move() 属性“move”受保护,只能在类“Animal”及其子类中访问。
const a = new Dog()
// a.move() 性“move”受保护,只能在类“Animal”及其子类中访问。
私有的privere
// private 只在当前的类当中可以使用 , 自己的实例对象,子类的类跟实例对象都不能访问
class Animal {
//私有的
private arun() {
console.log("跑步");
}
}
const a = new Animal();
// a.arun(); 属性“arun”为私有属性,只能在类“Animal”中访问
class Dog extends Animal {
//公有的
public bark() {
console.log("����");
}
// this.arun()
}
const dog = new Dog();
// dog.arun(); 属性“arun”为私有属性,只能在类“Animal”中访问
readonly(只读修饰符)防止构造函数之外的对属性进行赋值
class person {
//只读属性 readonly
readonly name: string = "John"; // 如果不加:number 那么name 就会变成一个字面量类型
constructor(name: string) {
this.name = name;
}
//readonly 不能修饰方法只能修饰属性
}
//接口 readonly
interface Person1 {
readonly age:number
}
let obj: Person1 = {
age:18
}
obj.age = 19 // 错误 因为age是只读属性
// 对象 readonly
let obj2: {age:number} = {
age:18
}
obj.age = 19 // 错误 因为age是只读属性
1.4.2类型兼容性
两种类型系统:1 Structural Type System(结构化类型系统) 2 Nominal Type System(标明类型系统)。
TS 采用的是结构化类型系统,也叫做 duck typing(鸭子类型),类型检查关注的是值所具有的形状。
也就是说,在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。
class Point { x: number; y: number }
class Point2 { x: number; y: number }
const p:Point = new Point2()
解释:
1. Point 和 Point2D 是两个名称不同的类。
2. 变量 p 的类型被显示标注为 Point 类型,但是,它的值却是 Point2D 的实例,并且没有类型错误。
3. 因为 TS 是结构化类型系统,只检查 Point 和 Point2D 的结构是否相同(相同,都具有 x 和 y 两个属性,属性类型也相同)。
4. 但是,如果在 Nominal Type System 中(比如,C#、Java 等),它们是不同的类,类型无法兼容。
注意:在结构化类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型,这种说法并不准确。
更准确的说法:对于对象类型来说,y 的成员至少与 x 相同,则 x 兼容 y(成员多的可以赋值给少的)。
class Point4 { x: number; y: number }
class Point3 { x: number; y: number; z: number}
const p1: Point4 = new Point3()
解释:
1. Point3的成员至少与 Point4 相同,则 Point4 兼容 Point3。
2. 所以,成员多的 Point3 可以赋值给成员少的 Point4。
除了 class 之外,TS 中的其他类型也存在相互兼容的情况,包括:1 接口兼容性 2 函数兼容性 等。
1 接口兼容性
接口之间的兼容性,类似于 class。并且,class 和 interface 之间也可以兼容。
// 接口的兼容性
interface Point5 { x: number; y: number }
interface Point6 { x: number; y: number }
interface Point7 { x: number; y: number; z: number }
class Point8 { x: number; y: number; z: number }
let p4: Point5
const p2: Point5 = new Point8()
let p3: Point6 = new Point8()
2 函数兼容性
1. 参数个数,参数多的兼容参数少的(或者说,参数少的可以赋值给多的)
解释:
1. 参数少的可以赋值给参数多的,所以,f1 可以赋值给 f2。
2. 数组 forEach 方法的第一个参数是回调函数,该示例中类型为:(value: string, index: number, array: string[]) => void。
3. 在 JS 中省略用不到的函数参数实际上是很常见的,这样的使用方式,促成了 TS 中函数类型之间的兼容性。
4. 并且因为回调函数是有类型的,所以,TS 会自动推导出参数 item、index、array 的类型。
2. 参数类型,相同位置的参数类型要相同(原始类型)或兼容(对象类型)。
解释:
1. 注意,此处与前面讲到的接口兼容性冲突。
2. 技巧:将对象拆开,把每个属性看做一个个参数,则,参数少的(f2)可以赋值给参数多的(f3)。
3. 返回值类型,只关注返回值类型本身即可:
解释:
1. 如果返回值类型是原始类型,此时两个类型要相同,比如,左侧类型 F5 和 F6。
2. 如果返回值类型是对象类型,此时成员多的可以赋值给成员少的,比如,右侧类型 F7 和 F8。
//函数兼容性(参数少的可以赋值给参数多的)
type F1 = (a: number) => void;
type F2 = (a: number,b:number) => void;
let f1:F1
let f2:F2
f2 = f1
1.4.3交叉类型
//交叉类型(&) :功能类似于接口继承(extends),用于组合多个类型为一个类型(常用于对象类型)
交叉类型与组合类型的对比
相同到哪: 都可以实现对象类型的组合
不同点 :两种方式实现类型组合时,对于同名属性之间,处理类型冲突的方式不同
接口继承会报错 交叉类型没有错误,可以简单理解为 fn:(value : string | number) => string
interface Person { name: string }
interface User { phone: string }
type PersonData = Person & User
let obj: PersonData = {
name: 'cui',
phone: '18888888888'
}
1.4.4泛型
泛型的应用场景:泛型在保护类型的同时,可以让函数等于多种不同类型一起工作,灵活可复用
// 泛型可以在保证类型安全的情况下,让函数与多种类型一起工作 ,从而实现复用,常用于:函数,接口,class 中
// 需求:创建一个id函数,传入什么数据就返回数据本省(也就是参数和返回值的类型相同)
function id(value: number): number {
return value
}
//这时候函数只能接受number类型的数据,
//为了让函数具备接收任何类型,可以将参数改成any,但是 这样就失去TS的类型保护,类型不安全
function add(value: any): any {
return value
}
//泛型在保护类型的同时,可以让函数等于多种不同类型一起工作,灵活可复用
1.泛型函数
//1.语法:在函数名称后面添加<>(尖括号),尖括号类指定具体的类型
// 2.当传入类型number后,这个类型会被声明的变量type捕获到
// 3.此时Type 的类型就是number ,所以函数id参数和返回值的类型都时number
//1.语法:在函数名称后面添加<>(尖括号),尖括号类指定具体的类型
// 2.当传入类型number后,这个类型会被声明的变量type捕获到
// 3.此时Type 的类型就是number ,所以函数id参数和返回值的类型都时number
function id<type>(value: type): type {
return value;
}
const num = id<number>(10)
const str = id<string>('string')
//函数调用是可以省略<>
const num = id(110)
这样通过泛型函数就就做到了让id函数与多种不同的类型一起工作,实现复用的同时保证的类型的安全
2.泛型约束
泛型约束:默认情况下,泛型函数的类型变量Type可以代表多个类型,这导致无法访问任何属性
比如 , id("q") 调用函数时就无法获取参数的长度
function id<type>(value: type) {
console.log(value.length);// 类型Type上不存在属性length
return value;
}
原因:因为Type表示任何属性,无法保证一定存在length属性,不如number就没有length属性
此时需要为泛型添加约束来收缩类型(缩窄类型的取值范围)
2.1 指定更加具体的类型Type[] (Type类型的数组),因为只要是数组就一个存在length属性,因此就可以访问了。
function id<Type>(value: Type[]) {
return value.length
}
2.2添加约束
1.创建描述约束的接口length,该接口要求提供length属性
2.通过extends关键字使用接口,为泛型添加约束
3.该约束表示:传入的类型必须具有length属性。
// 注意: 传入的实参只要有length属性即可,这也符合前面讲到的接口的类型兼容性。
interface ILength { length: number; }
function id1<Type extends ILength>(value: Type): Type {
return value
}
id1(['abc'])
id1("abc")
id1(123)//类型“number”的参数不能赋给类型“ILength”的参数
2.3类型变量之间的约束
// 解释:
/* 1.添加了第二个类型的变量,2个类型变量之间用(,)分隔
2.keyof关键字接收一个对象类型,生成一个键名称的联合类型。
3.keyof Type 实际上是获取的cui对象的所有键的联名类型,也就是 'name' | 'age'
4.类型变量Key收Type的约束只能是Type所有键当中的任意一个
*/
function getProp<Type, key extends keyof Type>(obj: Type, key: key) {
return obj[key]
}
let cui = {
name: 'cui',
age:18
}
getProp(cui, 'name',)
2.4泛型接口
泛型接口:接口也可以配合泛型来使用,以增加其灵活性,增加复用性
interface IdFunc<type> {
id: (value: type) => type
ids:()=> type[]
}
let obj: IdFunc<number> = {
id(value) { return value },
ids() { return [1,2,3] }
}
解释:
1. 在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口。
2. 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量。
3. 使用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc<nunber>)。
4. 此时,id 方法的参数和返回值类型都是 number;ids 方法的返回值类型是 number[]。
实际上js的数组在ts当中就是一个泛型接口
2.5泛型类
class Genng<Type> {
default: Type
add: (x: Type, y: Type) => Type
}
const newadd = new Genng<number>()
newadd.default = 12
2.6泛型工具类
1.Partial<Type> [ˈpɑːʃl]
//泛型工具类型 - Partial<Type> 用来构造(创建)一个类型,将 Type 的所有属性设置为可选。
//解释:构造出来的新类型 PartialProps 结构和 Props 相同,但所有属性都变为可选的。
interface Prage {
name: string,
age:18
}
type prage = Partial<Prage>
/* type prage = {
name?: string | undefined;
age?: 18 | undefined;
}
*/
2.Readonly<Type> [ˌriɑˈdɔnli]
泛型工具类型 - Readonly<Type> 用来构造一个类型,将 Type 的所有属性都设置为 readonly(只读)
interface Person1 {
id: string
titles: string
children: number[]
}
type obj = Readonly<Person1>
/*
type obj = {
readonly id: string;
readonly titles: string;
readonly children: number[];
}
*/
const a: obj = {
id: '123',
titles: '123',
children: [1,2,3]
}
a.children = [1,2,3] //无法为“children”赋值,因为它是只读属性
3.Record<Keys,Type> [ˈrekɔːd , rɪˈkɔːd]
泛型工具类型 - Record<Keys,Type> 构造一个对象类型,属性键为 Keys,属性类型为 Type。
// 泛型工具类型 - Record<Keys,Type> 构造一个对象类型,属性键为 Keys,属性类型为 Type。
type RecordObj = Record<'a' | 'b' | 'c', string[]>
let Obj: RecordObj = {
a: ['1', '2', '3'],
b: ['1', '2', '3'],
c: ['1', '2', '3']
}
解释:
1. Record 工具类型有两个类型变量:1 表示对象有哪些属性 2 表示对象属性的类型。
2. 构建的新对象类型 RecordObj 表示:这个对象有三个属性分别为a/b/c,属性值的类型都是 string[]。
1.4.5索引签名类型
绝大多数情况下,我们都可以在使用对象前就确定对象的结构,并为对象添加准确的类型。
使用场景:当无法确定对象中有哪些属性(或者说对象中可以出现任意多个属性),此时,就用到索引签名类型了。
interface AnyObject {
[key: string]:number
}
let OBJ: AnyObject = {
a: 1,
b: 2,
c: 3
}
解释:
1. 使用 [key: string] 来约束该接口中允许出现的属性名称。表示只要是 string 类型的属性名称,都可以出现在对象中。
2. 这样,对象 obj 中就可以出现任意多个属性(比如,a、b 等)。
3. key 只是一个占位符,可以换成任意合法的变量名称。
4. 隐藏的前置知识:JS 中对象({})的键是 string 类型的。
在 JS 中数组是一类特殊的对象,特殊在数组的键(索引)是数值类型。
并且,数组也可以出现任意多个元素。所以,在数组对应的泛型接口中,也用到了索引签名类型。
解释:
1. MyArray 接口模拟原生的数组接口,并使用 [n: number] 来作为索引签名类型。
2. 该索引签名类型表示:只要是 number 类型的键(索引)都可以出现在数组中,或者说数组中可以有任意多个元素。
3. 同时也符合数组索引是 number 类型这一前提。
interface MyArray<T> {
[index: number]: T
}
let arr: MyArray<number> = [1, 2, 3]
1.4.6映射类型
映射类型:基于旧类型创建新类型(对象类型),减少重复、提升开发效率。
type PropKey = 'a' | 'b' | 'c' | 'd'
type Type1 = { a: number, b: number, c: number }
type Type2 = { [key in PropKey]: number }
解释:
1. 映射类型是基于索引签名类型的,所以,该语法类似于索引签名类型,也使用了 []。
2. Key in PropKeys 表示 Key 可以是 PropKeys 联合类型中的任意一个,类似于 forin(let k in obj)。
3. 使用映射类型创建的新对象类型 Type2 和类型 Type1 结构完全相同。
4. 注意:映射类型只能在类型别名中使用,不能在接口中使用。
映射类型除了根据联合类型创建新类型外,还可以根据对象类型来创建:
type Props = { a: number, b: number, c: number, d: number }
type Props3 = { [key in keyof Props]: number }
解释:
1. 首先,先执行 keyof Props 获取到对象类型 Props 中所有键的联合类型即,'a' | 'b' | 'c'。
2. 然后,Key in ... 就表示 Key 可以是 Props 中所有的键名称中的任意一个。
T[P] 语法,在 TS 中叫做索引查询(访问)类型。
作用:用来查询属性的类型。
解释:Props['a'] 表示查询类型 Props 中属性 'a' 对应的类型 number。所以,TypeA 的类型为 number。
注意:[] 中的属性必须存在于被查询类型中,否则就会报错。
索引查询类型的其他使用方式:同时查询多个索引的类型
解释:使用字符串字面量的联合类型,获取属性 a 和 b 对应的类型,结果为: string | number。
解释:使用 keyof 操作符获取 Props 中所有键对应的类型,结果为: string | number | boolean。