ts编译
浏览器无法识别ts,ts需要通过tsc编译为js,才可以被浏览器识别
// 安装ts就默认有tsc功能了
npm install typescript -g
// 查看版本
tsc --version
tsc体验
// 通过ts编译产生js文件代码
tsc xxx.ts
ts运行环境
不可能每次手动编译成js,再手动导入到html中,然后再通过html跑在浏览器,所以采用下面两种方案:
- 安装ts-node插件(可以帮助我们直接自动编译成js,再让js直接跑在node上)
- 使用webapck进行配置(项目中推荐)
1. ts-node
npm install ts-node -g
// 但是ts-node依赖两个包
npm install tslib @types/node -g
安装完毕之后,直接使用ts-node跑代码:
ts-node xxx.ts
2. webpack配置ts-node
需要安装ts-loader插件进行转换:
// 这里安装ts-loader,还需要安装typescript, 因为ts-loader是利用typescript对代码做编译
// 就像安装babel-loader时,需要安装babel-core一样;npm install babel-loader @babel/core; 因为babel-loader要利用babel-core对代码做转化
npm install ts-loader typescript -D
接着需要在webpack中进行ts-loader配置:
// webpack.config.js
rules: [
{
test: /\.ts$/,
loader: "ts-loader"
}
]
配置完成之后,直接运行会报错,因为配置了ts-loader之后,需要有一个tsconfig.json
文件,它是一个ts的配置文件;可以借助以下命令生成:
// 自动生成tsconfig.json文件
tsc --init
有时候可能报错,ts不支持import引入时的ts后缀名;所以可以在在extensions中配置,默认增加.ts
ts变量声明
var name = "alice"
let age = 18
const len = 1.88
// string是ts中的字符串类型
// String是js中字符串包装类型
const msg:string = "hello"
代码规范:
-
eslint:是让js代码规范
-
tslint:是让ts代码规范
可以全局安装npm install tslint -g
,然后使用tslint --init
自动生成tslint.json
配置文件
类型推导:
let msg = "hello"
msg = 10 // 这里会报错,初始时虽然没有指定string,但是类型推导会将string作为msg的默认指定类型
ts类型
1. string
2. number
3. boolean
4. Symbol
5. Array
// 方式1
const names: Array<string> = [] // 不推荐(react的JSX中是有冲突的)
// 方式2
const names: string[] = [] // 推荐
6. Enum
enum NameTypeEnum {
CITY_LEVEL = "城市等级",
COMPETING_PRODUCTS_LAYOUT = "布局",
MEMORABILIA_NODE = "节点"
}
7. any
//eg
let str: string ="hello"
str = 123 // 报错,因为指定了只能是string;如果想后期给str赋值任何类型的值,可以修改为any
// eg
let str: any ="hello"
str = 123
两种场景:
- 当进行一些类型断言
- 不想给js添加具体的类型时
8. unknow
不确定类型(ts3.x才有)
unknow类型只能给any和unknow赋值
any类型可以赋值给任意类型
推荐any,因为限制了乱赋值
// 下面不知道res接受的是number还是string
const foo = () => 1
const bar = () => "hello"
let flag = true
let res: unknow
res = flag ? foo() : bar()
let msg:string = res // 如果是res是unknow则报错,因为unknow只能赋值给any或者unknow,限制了乱用
9. tuple
弥补了数组的缺点,数组中的元素不知道每个元素的类型,调取某一个元素的length可能报错;元组可以指定每个元素的类型
元组应用场景:
10. void
11. null
let n: null = null
12. undefined
let u: undefined = undefined
13. never
14. 对象类型
定义对象类型只需要把对象的属性列举出来,并给属性增加对应的类型就可以
// z是可选的
function printPoint(point: {x: number, y: number, z?:number}){}
printPoint({x: 123, y:321})
printPoint({x: 123, y:321, z:111})
对象作为参数时,也可以针对interface或者type定义的对象类型给予默认值通过解构的方式
// z是可选的
function printPoint(point: {x: number, y: number, z?:number}){}
printPoint({x: 123, y:321})
printPoint({x: 123, y:321, z:111})
// 采用type定义类型别名:
type PointType = {x: number, y: number}:
function printPoint({x: 0, y = 0}: PointType){}
// 采用interface定义类型
interface PointType{
x: number;
y?: number // 可选属性
}
function printPoint(point: PointType){}
readonly属性只读,无法赋值,引用类型的内容仍然可以可变
// 示例1:非引用类型
interface SomeType {
readonly prop: string;
}
function doSomething(obj: SomeType) {
console.log(`prop has the value '${obj.prop}'.`);
// 报错 不可以赋值
obj.prop = "hello";
}
// 示例2:引用类型
interface Home {
readonly resident: { name: string; age: number };
}
function visitForBirthday(home: Home) {
console.log(`Happy birthday ${home.resident.name}!`);
// 内部内容可以修改
home.resident.age++;
}
function evict(home: Home) {
// 报错不能被重新赋值
home.resident = {
name: "Victor the Evictor",
age: 42,
};
}
15. 函数类型
- 常规函数表示
// 格式
function 函数名(参数名: 参数类型): 返回值类型 {
}
// 示例1:
function add(a: number, b:number):number{
return a+b
}
- 箭头函数
// 示例1:指定函数类型
const foo = () => {}
// 增加函数类型注解() => void
const foo:() => void = () => {}
// 看起来复杂借助type定义类型
type MyFun = () => void
const foo:MyFun = () => {}
// 示例2:
const add: (n:number, m:number) => number = (a: number, b:number) => a+b
type addFunType = (n:number, m:number) => number
const add: addFunType = (a: number, b:number) => a+b
// 示例3:函数的参数的可选参数,必须写在最后面
const add = (a: number, b?:number) => a+b
// 示例4:函数的剩余参数
function foo(...nums: number[]){}
foo(123, 333) // ...nums可以传入任意个参数,会自动放到nums数组中
- 函数作为参数
type GreetFunction = (a: string) => void;
function greeter(fn: GreetFunction) {
// ...
}
- 函数约束
让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个 number 类型的 length 属性。我们使用extends
语法来约束函数参数:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
函数重载:两种实现方式,一种是通过联合类型;另一种是函数重载方式
16. 联合类型
不确定变量是哪一种类型时使用;要么是string,要么是number,传入时只能是两种中的其中一种确定类型,所以内部需要做判断
// id可以是string, 也可以是number
function printPoint(id : string | number){}
注意:如果是联合类型,代码中使用时,必须做相应的判断
function printPoint(id : string | number){
// 必须判断是string的情况,再做string相应的操作
// 否则当直接使用string相关的操作时,会提示警告错误,因为有可能是number,然而number不具有string的某些操作
if(typeof id == "string"){
}else {
// id是number时的操作
}
}
printPoint()
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
17. 可选属性和联合类型的关系
可选属性类似于是联合类型和undefined的联合类型
// id?: string 相当于 id:string|undefined
function printPoint(id?: string){}
18. 字面量类型
18.1 字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值
// 示例1:
// 字符串也可以作为字面量类型
const msg: "hello" = "hello"
// 用途:是需要结合联合类型使用
let align: "left" | "top" | "bottom" | "right"
align = "top"
align = "haha" // 错误,只能赋值指定的字面量
// 示例2:
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
18.2 数字字面量类型
19. 交叉类型
交叉类型用于合并已经存在的对象类型
- 交叉类型
interface Colorful {
color: string;
}
interface Circle {
radius: number;
}
type ColorfulCircle = Colorful & Circle;
Colorful 和 Circle 产生了一个新的类型ColorfulCircle,新类型拥有 Colorful 和 Circle 的所有成员
- 接口继承和交叉类型
使用继承的方式,重写属性类型会报错:
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
使用交叉类型的方式,重写属性类型不会报错,并且会把对应的属性的类型进行合并
interface Colorful {
color: string;
}
type ColorfulSub = Colorful & {
color: number
}
// 虽然不会报错,那 color 属性的类型是什么呢,答案是 never,取得是 string 和 number 的交集
类型收窄
1. typeof判断
function printPoint(id : string | number){
// 必须判断是string的情况,再做string相应的操作
// 否则当直接使用string相关的操作时,会提示警告错误,因为有可能是number,然而number不具有string的某些操作
if(typeof id == "string"){
}else {
// id是number时的操作
}
}
2. in判断收窄
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; // (parameter) animal: Fish | Human
} else {
animal; // (parameter) animal: Bird | Human
}
}
3. instanceof收窄
4. 可辨别联合类型收窄
场景:计算面积(有可能是圆形、或者正方形)
interface Shape {
kind: "circle" | "square";
radius?: number; //圆形时传入
sideLength?: number; // 正方形时传入
}
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
// Object is possibly 'undefined'.
}
}
此时ts会报错,因为radius
是可选的,就算是circle
的情况,radius
也可能没传,所以直接访问shape.radius
则为undefined,此时可以加非空断言,shape!.radius
,可以解决ts报错,但并非完美。因为从接口的定义是可选的,但是实际操作中又认为是非空的,语义前后矛盾。
此时 Shape的问题在于类型检查器并没有方法根据 kind 属性判断 radius 和 sideLength 属性是否存在,而这点正是我们需要告诉类型检查器的,所以我们可以这样定义 Shape:
// 示例:完美解决方案
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
// 这里还是需要判断,否则万一传入的是Square类型,则没有radius属性
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
type类型别名
有时候变量或者属性的类型比较复杂,可以采用type定义对应的类型别名
// 示例1:
function printPoint(id: string|number|boolean){
}
// 采用type定义类型别名:
type Dtype = string|number|boolean:
function printPoint(id: Dtype){}
// 示例2:
function printPoint(point: {x: number, y: number, z?:number}){}
// 采用type定义类型别名:
type PointType = {x: number, y: number, z?:number}:
function printPoint(id: PointType){}
注意:可以通过联合类型或者交叉类型来扩展type定义的类型
断言
1. 类型断言as
有的时候,你知道一个值的类型,但 TypeScript 不知道。
举个例子,如果你使用 document.getElementById,TypeScript 仅仅知道它会返回一个 HTMLElement,但是你却知道,你要获取的是一个 HTMLCanvasElement。
这时,你可以使用类型断言将其指定为一个更具体的类型:
const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
比如(不建议):想把string转number类型;可以先把string通过as转为any类型,再把any通过as转成number类型
const msg = "hello"
const num: number = (msg as any/unknow) as number
2. 非空断言!
funtion foo(msg?:string){
console.log(msg.length)
}
foo("hello") // 报错,因为msg是可选的,万一不传呢,就有可能是空,那么空的length所以就会报错,
// 两种方式: 一种是直接在foo函数中增加逻辑代码if判断msg有值再打印;
// 另一种是将 msg.length 变为 msg!.length !强制指定msg不为空
3. 确定赋值断言
类
1. 类定义
class Person {
// 可以直接name,也可以name:string,也可以name:string = "why"
name?: string // 这里必须写否则报错 类型不写默认都是any
age: number // 这里必须写否则报错
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
eating() {
console.log(`${this.name} eating...`);
}
}
const p = new Person("why", 18);
p.eating();
2. 继承
// 示例1:基本继承
class Person {
name: string = "";
age: number = 0;
eating() {
console.log(`eating...`);
}
}
class Student extends Person {
id: number = 0;
studying() {
console.log("studying...");
}
}
class Teacher extends Person {
title: string = "";
teaching() {
console.log("teaching...");
}
}
const stu = new Student();
stu.name = "why";
stu.age = 18;
stu.eating();
// 示例2:constructor
class Person {
name: string
age: number
constructor(name: string, age: number){
this.name = name
this.age = age
}
eating() {
console.log(`eating...`);
}
}
class Student extends Person {
id: number
constructor(name: string, age: number, id: number){
super(name, age)
this.id = id
}
// 重写父类方法
eating() {
// 如果希望子类调用自己的eating时也调用父类的eating;可以使用super.eating()
console.log(`eating...`);
}
studying() {
console.log("studying...");
}
}
class Teacher extends Person {
title: string
constructor(name: string, age: number, title: string){
super(name, age)
this.title = title
}
teaching() {
console.log("teaching...");
}
}
const stu = new Student("why", 18, 111);
stu.eating();
console.log(stu.name)
console.log(stu.age)
3. 多态
多态可以写出更加通用的代码
多态:父类引用指向子类对象
class Animal {
action() {
console.log("animal");
}
}
class Dog {
action() {
console.log("Dog runing");
}
}
class Fish {
action() {
console.log("Fish swiming");
}
}
// 这里使用Animal类型数组接受
function mutiAction(animals: Animal[]){
animals.forEach(animal => {
animal.action() // runing、 swiming
})
}
// 传递的时子类的对象,相当于父类引用指向了子类对象 形成多态
mutiAction([new Dog(), new Fish()])
// let animal:Animal = new Dog() // 父类引用指向子类对象
4. 类修饰符
类属性和方法支持3种修饰符:
- public(默认):任何地方可见可用
- protected:仅在当前类自身内部和子类中可用
- private:仅在当前类内部可用
class Person{
public name: string = ""
}
// protected示例:
class Person{
protected name: string = "123" // protected在子类Student的getName中访问了
}
class Student extends Person{
getName(){
return this.name
}
}
const stu = new Student()
console.log(stu.getName()) // 123
5. readonly
- 只能在构造器中可以被赋值,赋值之后就不能修改
- readonly属性本身不能修改,但是如果他是对象,对象中的属性是可以修改的
class Person{
// readonly name: string = "123" // 如果在这里初始化赋值了,再外面就不能再修改了
readonly name: string
age?: number
readonly friends?: Person
constrcutor(name: string, friends?: Person){
this.name = name
this.friends = friends
}
}
const p = new Person("kobe", new Person("why"))
p.name
p.friends
// p.friends = new Person("james") 不能直接修改friends
p.friends.age = 19 // 可以修改
// p.friends.name = "xxx" // 不可以修改 name是readonly
6. setter和getter访问器(推荐)
比如想读取私有变量name,一般提供setName和getName对应的方法,通过调用对应方法进行访问;但是在ts中推荐使用setter和getter访问器进行操作
// 示例1:
class Person{
private _name: string
constructor(name: string){
this._name = name
}
setName(name: string){
this._name = name
}
getName(){
return this._name
}
}
const p = new Person("why")
p.setName("kobe")
p.getName()
// 示例2:setter和getter
class Person{
private _name: string
constructor(name: string){
this._name = name
}
// setter: setter 访问器名
set name(newName: string){
this._name = newName
}
// getter
get name(){
return this._name
}
}
const p = new Person("why")
// setter方式直接通过setter访问器访问
p.name = "code"
// getter方式直接通过getter访问器访问
console.log(p.name)
7. 静态属性
class Person{
static name: string
static fun(){
console.log("static方法")
}
}
Person.name
Person.fun()
8. 抽象类
一般使用类多态继承就可以,为什么还需要抽象类呢?因为大多时候在父类中公用方法,都会被子类重写,所以父类中的公用方法的方法体没有意义,此时我们可以将父类的方法定义为抽象方法,父类也会成为抽象类
c抽象类特点:
- 不能new
- 抽象方法必须被子类实现,否则子类也是抽象类
- 抽象方法必须包含在抽象类中
// 如果没有抽象类,没有抽象方法:
function makeArea(shape: any) {
return shape.getArea();
}
class Rectangle {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Circle {
private r: number;
constructor(r: number) {
this.r = r;
}
getArea() {
return this.r * this.r * 3.14;
}
}
console.log(makeArea(new Rectangle(10, 20)));
console.log(makeArea(new Circle(10)));
console.log(makeArea(10)); // 运行时会报错 10没有getArea()
以上10没有getArea()就会报错,我们可以通过抽象类限制makeArea的输入:
// 这里参数类型限制
function makeArea(shape: Shape) {
return shape.getArea();
}
// 抽象类
abstract class Shape {
// 抽象方法不能有方法体
abstract getArea()
}
class Rectangle extends Shape{
private width: number;
private height: number;
constructor(width: number, height: number) {
super()
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Circle extends Shape {
private r: number;
constructor(r: number) {
super()
this.r = r;
}
getArea() {
return this.r * this.r * 3.14;
}
}
console.log(makeArea(new Rectangle(10, 20)));
console.log(makeArea(new Circle(10)));
console.log(makeArea(10)); // 编写代码时会提示错误
接口
接口的主要作用是:1. 实现类型约束;2. 接口可以实现多继承,类只能单继承类,但是可以实现多个接口
0. 接口使用场景
场景一:类型约束
interface IEat {
eating: () => void
}
// IEat接口是info对象的类型,那么info中就必须包含eating函数
const info: IEat = {
eating(){
log
}
}
// foo函数的参数eat类型是IEat接口类型,那么传入的eat对象必须带有eating函数,符合IEat接口对eating函数的类型约束
function foo(eat: IEat){
}
场景二: 接口继承
场景三: 类实现多接口
interface IEat {
eating: () => void
}
interface ISwim {
swimming: () => void
}
class Animal {
}
// 单继承类,实现多接口
class Fish extends Animal implements IEat, ISwim {
eating(){}
swimming(){}
}
编写公共API:面向接口编程;(面向接口编程的前提是 类实现对应接口)
interface IEat {
eating: () => void
}
interface ISwim {
swimming: () => void
}
class Animal {}
class Fish extends Animal implements IEat, ISwim {
eating(){}
swimming(){}
}
class Person implements ISwim {
swimming(){}
}
// 编写公共API:面向接口编程
function swimAction(swimable: ISwim){
swimable.swimming()
}
// 所有实现了ISwim接口的类都可以传入
swimAction(new Fish())
swimAction(new Person())
1. 接口声明
除了通过type声明类型别名之外,也可以通过interface声明类型
// 方式一:type声明对象的类型别名
type InfoType = {
name: string,
age: number
}
const info: InfoType = {
name: "why",
age: 18
}
// 方式二:interface声明对象的类型别名
interface IInfoType {
readonly name: string,
age: number,
friends?: {
name: string
}
}
const info: IInfoType = {
name: "why",
age: 18,
friends: {
name: "kobe"
}
}
2. 接口索引类型
// 索引类型
interface IndexLanguage {
// 定义key必须是number类型(采用index),value是string
[index: number]: string
}
const front: IndexLanguage= {
0: 'html',
1: 'css'
}
// 定义key是字符串(采用name) value是number
interface IndexLanguage {
[name: string]: number
}
const front: IndexLanguage= {
'html': 999
}
所以类型可以让interface实现对对象和数组的约束:
// 对数组的约束
interface IndexLanguage {
[index: number]: string
}
const frontArr: IndexLanguage= ['c++', 'c']
// 对对象的约束
interface IndexLanguage {
[index: string]: number
}
const frontObj: IndexLanguage= {
'html': 999
}
3. 接口和type的区别
- 如果是定义非对象类型,推荐type
- 如果是对象类型
- interface可以重复定义的接口,接口中的不同属性,接口的属性会合并
- type不支持重复定义别名
// 示例1:
interface Foo{
name:string
}
interface Foo{
age:number
}
// 最终的Foo接口中会有两个存在两个属性,分别是name和age
// 示例2:
type Foo = {
name: string
}
type Foo = {
age: number
}
// type重复定义Foo报错
备注:
1、interface:表示用于描述对象的结构和属性;(interface可以被实现(implements)或者扩展(extends))
2、type: 表示类型的别名;(允许为任何类型创建别名——典型的是进行类型的复杂操作)
3、注意两者的区别:type是类型的别名,为了进行类型的交叉联合产生新类型的等;但是interface不是作用于类型,而是为了描述对象的结构和属性
4、interface重名会合并属性,type不能重名;
5、interface可以被类实现,type不能
6、type支持联合类型和交叉类型,interface不支持
3.1 对象和函数
// 示例1:对象描述
type InfoType = {
name: string,
age: number
}
// 这里和type不一样,这里没有赋值等号
interface InfoType {
name: string,
age: number,
}
const info: InfoType = {
name: "why",
age: 18
}
// 示例2:函数描述
type SetPointType = (x: number, y: number) => number; // 这里是箭头符号
interface SetPointType {
(x: number, y: number) : number; // 这里不是箭头符号 => 而是冒号
}
const SetPoint: SetPointType = (x: number, y: number) => x+y
4. 接口继承
接口可以实现多继承
interface ISwim {
swimming: () => void
}
interface IFly {
flying: () => void
}
interface IAction extends ISwim, IFly {
}
const action: IAction = {
swimming(){},
flying(){}
}
注意:多继承除了interface之外,还可以采用交叉类型实现
5. 接口的实现
泛型
函数调用可以自动类型推导;但是对象就不行,比如第三点
1.认识泛型
泛型本质:类型参数化(就是具体的类型通过参数的方式在调用时传入进来)
function sum<T>(num: T): T{
return num
}
// 1. 调用方式1:明确传入类型
sum<number>(20)
sum<{name : string}>({name: "why"})
sum<any[]>(["abc"])
// 2. 调用方式2:类型推导
sum(30)
2. 泛型多类型
function sum<T, K>(num: T, age: K): T{
return num
}
sum<number, string>(20, "why")
3. 泛型接口使用
// interface IPerson<T1 = string, T2 = number>
interface IPerson<T1, T2> {
name: T1
age: T2
}
const p: IPerson<string, number> = {
name: "why",
age: 18
}
4. 泛型类的使用
class Point<T> {
x: T
y: T
constructor(x: T, y: T) {
this.x = x
this.y = y
}
}
// 类调用的类型传入位置和接口泛型传入的位置
const p = new Point<string>("11", "22") // 推荐
// 也可以像下面一样指定类型传入位置
const p: Point<string> = new Point("11", "22")
5. 泛型的类型约束
// 这种联合类型穷举太繁琐
function foo(arg: string | number | {length: number} | any[]){
return arg.length
}
以上联合类型限制参数类型传入的方式太繁琐;我们可以通过泛型,但是泛型T过于宽泛,可以传入任意类型,如何限制联合类型穷举出来的类型呢?借助泛型的类型继承进行类型的限制约束
// 示例1: 泛型T过于宽泛,什么都可以传入,达不到类型约束(只需要部分指定类型)
function foo<T>(arg: T){
return arg.length
}
// 示例2:
interface ILength {
length: number
}
// T extends ILength: 表示T是ILength的子类,所以外界传入的T类型必须有length属性;否则报错
function foo<T extends ILength>(arg: T){
return arg.length
}
foo(123) // 会报错
foo([1,2,3]) // 正确
foo("why") // 正确
foo({length : 100}) // 正确
8. 泛型应用理解
8.1 常规函数泛型
// 示例1:常规返回泛型
// identity<T,R> 函数后的T和R必须传递,不然参数和返回值包括函数内部都不能使用T和R
// 函数后接受的T和R,会传递给参数的T和R;接着再传递给返回值的T
function identity<T,R>(arg: T): T {
let age:R
log(age,arg)
return arg;
}
8.2 类中使用
类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
// 示例1: 通过类传入T,可以在属性和方法的参数和返回值上直接使用
class Person<T>{
name:T
constructor(name: T){
this.name = name
}
setName(name: T){
this.name = name
}
}
const p = new Person<string>("why")
// 示例2:直接在类方法中传入T,方法中 setName<T>(name: T)的T只作用该方法的参数和返回值以及方法内部(和普通函数传入T效果一样),调用setName时在调用处传递T
class Person{
name: any
constructor(name: any){
this.name = name
}
setName<T>(name: T){
this.name = name
}
}
const p = new Person("why")
p.setName<number>(110)
// 示例3: 当类和方法都传入泛型T时,方法的T会生效
8.3 在接口中使用T
// 示例2:接口中定义函数: 留意接口中定义的函数和类方法的T一样,也不一定需要接口传入T
interface GenericIdentityFn {
name: T // 也可以写 但是没有意义
<T>(arg: T): T;
}
function identity<T>(arg: T): T { return arg; }
let myIdentity: GenericIdentityFn = identity;
myIdentity<string>("why")
// 示例3:接口定义函数传入T (留意如果接口传入了T,方法类型前要传入的T就没有了)
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T { return arg; }
let myIdentity: GenericIdentityFn<string> = identity;
identity("why") 或者 identity<string>("why")
identity<number>(111) // 也可以;以方法的为准,打印111, 并且是number类型
8.4 泛型定义类型以及应用场景
// 示例1:函数类型简写
function identity<T>(arg: T): T { return arg; }
方式1:let myIdentity: <U>(arg: U) => U = identity;
方式2:let myIdentity: {<T>(arg: T): T} = identity;
<U>(arg: U) => U // 表示的是identity函数的函数类型简写;到时候传入的U
// {<T>(arg: T): T} 和 <U>(arg: U) => U 的写法一样
// 示例2:接口中定义函数: 留意接口中定义的函数和类方法的T一样,也不一定需要接口传入T
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T { return arg; }
let myIdentity: GenericIdentityFn = identity;
myIdentity<string>("why")
// 示例3:接口定义函数传入T (留意如果接口传入了T,方法类型前要传入的T就没有了)
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T { return arg; }
let myIdentity: GenericIdentityFn<string> = identity;
identity("why") 或者 identity<string>("why")
identity<number>(111) // 也可以;以方法的为准,打印111, 并且是number类型
应用场景:
对原有的类型支持的属性做进一步扩展,可以让类型支持的属性约束更强大:
function loggin<T>(arg: T): T
{ console.log(arg.length); // Error: T doesn't have .length return arg; }
由于可能传入number,number没有length属性,所以我们可以扩展原有T类型,再不改变的T类型的前提下,让T通过extends其他包含length属性接口R,已达到最终传入的T类型必须包含除了T接口本身的属性之外,还必须包含R类型中的属性。
interface R {
length:number
}
// 最终这里的T包含了原本的T和R所有的属性
function loggin<T extends R>(arg: T): T
{
console.log(arg.length);
}
logging(3) // 会报错
logging({length: 10, name:"why"})
模块化和命名空间
模块化: 支持esModule和commonJS
命名空间(namespace): 最早称之为内部模块,主要目的是将一个模块内部再进行作用域的划分,防止一些命名冲突的问题(比如在一个文件中两个函数名冲突的场景)
// bar.ts: 在一个文件中两个函数名冲突
function format(){
return 10
}
function format(){
return 99
}
使用namespace可以解决一个文件中两个函数名冲突的场景
// 示例1:bar.ts
namespace time{
// 要想在time的namespace外部访问到,则需要export导出
export function format(){
return 10
}
}
namespace price{
export function format(){
return 10
}
}
// 示例2:bar.ts文件内部使用
time.format()
price.format()
// 示例3:bar.ts文件外部使用,则需要把namespace导出
export namespace time{
// 要想在time的namespace外部访问到,则需要export导出
export function format(){
return 10
}
}
import { time, price } './bar.ts'
time.format()
price.format()
类型的查找规则以及声明
为什么在文件中axios可以使用,原生的domAPI可以使用,但是lodash不能用
ts中有两种文件:
- .ts文件: 写代码的地方,最终这个ts文件会输出js文件
- .d.ts文件:用来做类型的声明(declare),仅仅用做类型检测,告知typescript我们有哪些类型
ts在3个地方找类型声明:
- 内置类型声明
- 外部定义类型声明
- 自己定义类型声明
内置类型声明:
比如在ts文件中可以直接使用document.getElementById, 也能识别到HTMLElement类型等,是因为typescript内置了DOM的类型;在node_modules/typescript/lib/lib.dom.d.ts中声明。
内置类型是ts内置的, 比如Math、Data、DOM API、Window、Document等
外部类型声明:
axios库:在node_modules/axios/index.d.ts中批量声明了类型,所以在业务文件中,可以直接import导入后直接使用axios
lodash库:在业务文件中导入后不能使用,因为没有提供xxx.d.ts声明文件
外部类型声明有两种方式:
- 一种是在自己库里面集成了.d.ts类型声明(axios)
- 一种是通过社区的一个公有库
DefinitelyTyped
存放类型声明文件(这种方式是库和类型声明文件不放在一起;类型声明文件查找网址:www.typescriptlang.org/dt/search?s… )
自定义声明类型
如果库没有自己集成d.ts声明文件,并且在typescript提供types(DefinitelyTyped)平台也没有提供对应的类型声明文件,则需要自己开发手动编写.
// 创建lodash.d.ts文件,并在文件中手写声明
declare module 'lodash' {
export function join(arr: any[]): void
}
// main.ts
import lodash from 'lodash'
lodash.join(["a", "b"]) // 编译成功
类型声明文件:可以声明变量/函数/类
在index.html中的script标签中写ts代码,在main.ts中使用,但是编译报错,为什么呢?平时编写的ts代码经过编译打包之后变成js代码,最终也会被导入到index.html中,那为什么在index.html的script标签中编写的ts代码在main.ts中不能用呢?编译会报错,这是ts的规范,需要有类型声明,此时可以在.d.ts文件中进行声明,然后再编译打包就可以了
// index.html
<scrpit>
let whyName = "why"
let whyAge = 18
function whyFoo(){
console.log("whyFoo")
}
</scrpit>
// main.ts
log(whyName)
log(whyAge)
whyFoo()
// xxx.d.ts
declare let whyName: string
declare let whyAge: number
declare function whyFoo(): void
// 然后编译打包就可以了
// xxx.d.ts
// 1. 声明模块
declare module 'lodash' {
export function join(arr: any[]): void
}
// 2. 声明变量/函数/类
declare let whyName: string
declare let whyAge: number
declare function whyFoo(): void
declare class Person {
name: string
constructor(name: string)
}
// 3. 声明一个文件:比如说import图片
declare module "*.jpg" // 表达的意思是可以将jpg图片以文件的形式import导入
declare module "*.png"
关键字
1. is运算符
语法:parameterName is T:parameterName的类型如果是T类型则返回true,否则返回false;(parameterName必须是当前函数的参数名)
如果像下面这样写已经比较简洁了,但是调用方法的时候,还是要进行类型转换才可以,否则还是会报错
function isBird(bird: Bird | Fish): boolean {
return !!(bird as Bird).fly;
}
function isFish(fish: Bird | Fish): boolean {
return !!(fish as Fish).swim;
}
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if (isBird(pet)) {
(pet as Bird).fly(); // 这里在调用方法时仍然需要类型转换,否则报错
} else if (isFish(pet)) {
(pet as Fish).swim();
}
}
is此时可以派上用场,可以让我们判断完类型之后,就可以直接调用方法,不用再进行类型转换
// 留意看is在这里的用法:如果bird is Bird: 如果bird是Bird类型返回true
function isBird(bird: Bird | Fish): bird is Bird {
return !!(bird as Bird).fly
}
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if (isBird(pet)) { // 这里返回true说明告知了ts,此时的pet就是Bird类型了;所以if的内部就不需要再进行类型转换才可以调用
pet.fly(); // 这里不需要再进行类型转换
} else {
pet.swim();
}
};
2. in运算符
语法:key in 联合类型K:操作符作用遍历类型;
// 示例1:
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
// 示例2:
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; };
// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;
[Prop in U]
表示依次取出联合类型U的每一个成员。
[Prop in keyof Obj]
表示取出对象Obj对象类型的每一个键名。
// 示例2:
type roles = "tester" | "developer" | "manager";
const staffCount: { [k in roles]: number } = {
tester: 100,
developer: 200,
manager: 300,
};
上述代码规定 staffCount 是一个对象,属性名为 roles 约束的三个,值为 number 类型;类型变量 k,以此绑定到对象的每一个属性,遍历三个字符串字面量组成的联合类型 roles,number 为每个属性的值的类型。
in运算符的另一个用法是:可以做新旧类型之间的映射
// 示例3:
interface publicObj {
// 定义一个开放的对象
name: string;
age: number;
}
type ReadonlyObj<T> = { // 需要传递一个类型参数
readonly [K in keyof T]: T[K]; // keyof T 返回联合类型 in 再遍历该联合类型
};
// 使用
let obj: ReadonlyObj<publicObj> = {
name: "myName",
age: 6,
};
obj.name = "yourName"; // 无法分配到 "name" ,因为它是只读属性。ts(2540)
3. extends
注意这里的extends不是类接口的继承
语法:T extends K:表示T类型是K类型的子类型(可以用作类型收窄);
让我们写一个函数,函数返回两个值中更长的那个。为此,我们需要保证传入的值有一个 number 类型的 length 属性。我们使用extends
语法来约束函数参数:
function longest<Type extends { length: number }>(a: Type, b: Type) {
if (a.length >= b.length) {
return a;
} else {
return b;
}
}
// longerArray is of type 'number[]'
const longerArray = longest([1, 2], [1, 2, 3]);
// longerString is of type 'alice' | 'bob'
const longerString = longest("alice", "bob");
// Error! Numbers don't have a 'length' property
const notOK = longest(10, 100);
4. typeof
语法:"typeof x":表示变量x的类型
在 TypeScript 中,typeof
操作符可以用来获取一个变量声明或对象的类型。
interface Person {
name: string; age: number;
}
const sem: Person = { name: 'semlinker', age: 33 };
type Sem= typeof sem; // -> Person
function toArray(x: number): Array<number> {
return [x];
}
type Func = typeof toArray; // -> (x: number) => number[]
5. keyof操作符
语法:"keyof T":表示获取T类型中的所有键名组成的联合类型
keyof
该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型
// 示例1:keyof用于interface
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
// 示例2:keyof应用class
class Person {
name: string = "Semlinker";
}
let sname: keyof Person;
sname = "name";
// 示例3:keyof引用基本类型
let K1: keyof boolean; // let K1: "valueOf"
let K2: keyof number; // let K2: "toString" | "toFixed" | "toExponential" | ...
let K3: keyof symbol; // let K1: "valueOf"
keyof应用场景:
// 示例1:
// ts中会报错:元素隐式地拥有 any 类型,因为 string 类型不能被用于索引 {} 类型
// 报错原因:key的类型并不是string,在js中默认帮助转为string,但ts默认不会帮转,所以认为key不是string类型
for (const key in obejct) {
obejct[key]
}
// 修复:
export function isValidKey(
key: string | number | symbol,
object: object
): key is keyof typeof object {
return key in object;
}
for (const key in obejct) {
if(isValidKey(key, obejct)){
obejct[key]
}
}
// 解析:typeof object 会获取到object这个对象的类型X,然后keyof X就会获取到X类型中的所有属性名的联合
// 示例2:同示例1报错
function prop(obj: object, key: string) {
return (obj as any)[key];
}
// 修复:
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
在以上代码中,我们使用了 TypeScript 的泛型和泛型约束。首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型
6. instanceof
7. T[key]方括号索引运算符
T[key]语法含义:类似于 js 中使用对象索引的方式,只不过 js 中是返回对象属性的值,而在 ts 中返回的是 T 对应属性 key 的类型。
interface Person {
name: string
age: number
weight: number | string
gender: 'man' | 'women'
}
type NameType = Person['name'] // string
type WeightType = Person['weight'] // string | number
运算符高级类型和技巧
1. Omit<Type, Keys>: 排除属性
语法:Omit<Type, Keys>:表示从 Type 类型中排除部分属性键名keys,将剔除keys属性之后剩下的属性组成的对象作为新的对象类型返回。(备注:指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错)
// Omit<Type, Keys>的实现方式:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
// 示例1:
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit<Todo, "description">;
// TodoPreview类型中就只有title、completed、createdAt
const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};
// 示例2:
type TodoInfo = Omit<Todo, "completed" | "createdAt">;
// TodoInfo中只剩下title、description
const todoInfo: TodoInfo = {:
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};
// 示例3:
interface A {
x: number;
y: number;
}
// 上面示例中,对象类型A中不存在属性z,所以就原样返回了。
type T = Omit<A, 'z'>; // { x: number; y: number }
使用场景:
比如需要继承某个类型,并且重写对应的属性类型时(不能直接使用接口继承,因为overwrite,因为会报错)。
interface Colorful {
color: string;
}
interface ColorfulSub extends Colorful {
color: number
}
// 会报错
这时候可以借用Omit实现继承类型并且重写对应的属性类型 (本质是通过Omit把要重写的属性过滤,然后再和要重写的属性进行交叉运算生成新类型)
// 示例1:重写Todo的description
interface Todo {
title: string;
description: string;
}
type MyTodo = Omit<Todo, 'description'> & {description: () => void}
// 相当于
type MyTodo = {
title: string,
description: () => void
}
2. Pick<Type, Keys>:提取属性
语法:Pick<Type, Keys>:表示从 T 类型中提取部分属性键名keys,作为新的对象类型返回。(备注:指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错)
// Pick<Type, Keys>的实现方式:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
// 示例1:
interface A {
x: number;
y: number;
}
type T1 = Pick<A, 'x'>; // { x: number }
type T3 = Pick<A, 'x'|'y'>; // { x: number; y: number }
使用场景:比如发送网络请求时,只需要传递类型中的部分参数,此时可以借助Pick实现:
interface Goods {
type: string
goodsName: string
price: number
}
// 作为网络请求参数,只需要 goodsName 和 price 就可以
type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>
// 返回类型:
// type RequestGoodsParams = {
// goodsName: string;
// price: number;
// }
const params: RequestGoodsParams = {
goodsName: '',
price: 10
}
3. Exclude<UnionType, ExcludedMembers>: 从联合类型U中剔除或者排除成员类型
语法:Exclude<U, E> :从联合类型U中剔除成员类型之后剩下的类型作为新的联合类型返回。
// Exclude<U, E>实现方式:
type Exclude<T, U> = T extends U ? never : T;
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Exclude<string | number | (() => void), Function>; // string | number
4. Extract<UnionType, Union>:从联合类型UnionType中提取指定类型Union
语法:Extract<UnionType, Union>用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回;
// Extract<UnionType, Union>的实现:
type Extract<T, U> = T extends U ? T : never;
// 示例1:不包含则返回never
type T = Extract<string|number, boolean>; // never
// 示例2:
type T1 = Extract<'a'|'b'|'c', 'a'>; // 'a'
5. Required:必选属性
语法:Required:表示将T类型中的所有属性变为必选属性
Required的实现方式:用于将 T 类型的所有属性设置为必选状态,首先通过 keyof T,取出类型 T 的所有属性,然后通过 in 操作符进行遍历,最后在属性后的 ? 前加上 -,将属性变为必选属性。
// Required<T>实现方式:
type Required<T> = {
[P in keyof T]-?: T[P];
}
interface Person {
name?: string
age?: number
}
// 使用 Required 映射后返回的新类型,name 和 age 都变成了必选属性
// 会报错:Type '{}' is missing the following properties from type 'Required<Person>': name, age
let person: Required<Person> = {}
6. Partial:可选属性
语法:Partial : 表示将T类型中的属性全部转为可选属性
Partial的实现方式:用于将 T 类型的所有属性设置为可选状态,首先通过 keyof T,取出类型 T 的所有属性,然后通过 in 操作符进行遍历,最后在属性后加上 ?,将属性变为可选属性。
// Partial<T>的实现方式
type Partial<T> = {
[P in keyof T]?: T[P]
}
// 示例1:
interface Person {
name: string
age: number
}
// 会报错:Type '{}' is missing the following properties from type 'Person': name, age
// let person: Person = {}
// 使用 Partial 映射后返回的新类型,name 和 age 都变成了可选属性
let person: Partial<Person> = {}
person = { name: 'pengzu', age: 800 }
person = { name: 'z' }
person = { age: 18 }
// 示例2:
export interface IData {
name: string;
age: number;
}
setHelpCenter(partial: Partial<IData>) {
this.$patch(partial);
}
Partial<IData>
会将IData
类型中的属性全部转为可选属性,相当于下面:
{
name?: string;
age?: number;
}
7. Parameters
语法:Parameters:从函数类型Type里面提取参数类型,组成一个元组返回(备注:type是函数类型)
// Parameters<Type>实现方式:
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P)
=> any ? P : never
// 示例1:
type FunctionType = (name: string, age: number, test: string) => boolean
type FunctionParamsType = Parameters<FunctionType> // [name: string, age: number, test: string]
const params: FunctionParamsType = ['Jack', 20, 'hello']
// 示例2:
type T1 = Parameters<string>;// 报错
type T2 = Parameters<(s:string) => void>; // [s:string]
type T3 = Parameters<<T>(arg: T) => T>; // [arg: unknown]
8. ReturnType
语法:ReturnType:提取函数类型Type的返回值类型,作为一个新类型返回
// 示例1:
type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<() => {
a: string; b: number
}>; // { a: string; b: number }
type T3 = ReturnType<(s:string) => void>; // void
9. Record<Keys, Type>
语法:Record<Keys, Type>:返回一个对象类型,参数Keys用作键名,参数Type用作键值类型。
// Record<Keys, Type>实现如下:
type Record<K extends string|number|symbol, T>
= { [P in K]: T; }
// 示例1:
// { a: number, b: number }
type T = Record<'a'|'b', number>;
// 示例2:
// { a: number|string }
type T = Record<'a', number|string>;
// 示例3:
type petsGroup = 'dog' | 'cat' | 'fish';
interface IPetInfo {
name:string,
age:number,
}
type IPets = Record<petsGroup | 'otherAnamial', IPetInfo>;
const animalsInfo:IPets = {
dog:{
name:'dogName',
age:2
},
cat:{
name:'catName',
age:3
},
fish:{
name:'fishName',
age:5
},
otherAnamial:{
name:'otherAnamialName',
age:10
}
}
10. InstanceType: 获取构造函数的返回值类型
语法:InstanceType :获取类型T的构造函数的返回类型。
// InstanceType<T>的实现方式:
type InstanceType<
T extends abstract new (...args:any) => any
> = T extends abstract new (...args: any) => infer R ? R :
any;
// 示例1:
class Person {
name: string
age: number
weight: number
gender: 'man' | 'women'
constructor(name: string, age: number, gender: 'man' | 'women') {
this.name = name
this.age = age;
this.gender = gender
}
}
type Instance = InstanceType<typeof Person> // Person
const params: Instance = {
name: 'Jack',
age: 20,
weight: 120,
gender: 'man'
}
// 示例2:实战演练
<BaseInfo
ref="baseInfoRef"
:businessRowId="!!businessRowId"
:currentBusinessInfoRow="businessInfoRecord"
/>
// typeof获取模板BaseInfo的构造函数类型X, InstanceType再获取X这个构造函数的实例类型也就是构造函数的返回值类型
const baseInfoRef = ref<InstanceType<typeof BaseInfo>>();
await baseInfoRef?.value?.validateBaseInfoForm();
字符串类型工具
1. Uppercase
语法:Uppercase:将字符串类型的每个字符转为大写。
type A = 'hello';
// "HELLO"
type B = Uppercase<A>;
2. Lowercase
语法:Lowercase: 将字符串的每个字符转为小写。
type A = 'HELLO';
// "hello"
type B = Lowercase<A>;
3. Capitalize
语法:Capitalize将字符串的第一个字符转为大写。
type A = 'hello';
// "Hello"
type B = Capitalize<A>;
4. Uncapitalize
语法:Uncapitalize:将字符串的第一个字符转为小写。
type A = 'HELLO';
// "hELLO"
type B = Uncapitalize<A>;
类型映射实战
1. 类型映射
下面这两个类型的属性结构是一样的,但是属性的类型不一样;如果属性数量多的话,逐个写起来就很麻烦。
type A = {
foo: number;
bar: number;
};
type B = {
foo: string;
bar: string;
};
使用类型映射可以从A类型映射为B类型:
type A = {
foo: number;
bar: number;
};
type B = {
[prop in keyof A]: string
};
上述[prop in keyof A]是一个属性名表达式,表示这里的属性名需要计算得到;具体的计算规则如下:
- prop:属性名变量,名字可以随便起。
- in:运算符,用来取出右侧的联合类型的每一个成员。
- keyof A:返回类型A的每一个属性名,组成一个联合类型。
也可以B类型复制A的类型或者传入泛型类型
// 示例:
type A = {
foo: number;
bar: string;
};
// 复制A的属性和属性的类型
type B = {
[prop in keyof A]: A[prop];
};
// 使用泛型传入: 可以将其他对象的所有属性值都改成 boolean 类型。
type ToBoolean<Type> = {
[Property in keyof Type]: boolean;
};
2. 索引类型的映射
// 示例1:
type MyObj = {
[P in 0|1|2]: string;
};
// 等同于
type MyObj = {
0: string;
1: string;
2: string;
};
// 示例2:
type MyObj = {
[p in 'foo']: number;
};
// 等同于
type MyObj = {
foo: number;
};
// 示例3:[p in string]就是属性名索引形式[p: string]的映射写法
type MyObj = {
[p in string]: boolean;
};
// 等同于
type MyObj = {
[p: string]: boolean;
};
// 示例4:映射为可选
type A = {
a: string;
b: number;
};
type B = {
[Prop in keyof A]?: A[Prop];
};
3. 映射修饰符
映射会原样复制原始对象的可选属性和只读属性
type A = {
a?: string;
readonly b: number;
};
type B = {
[Prop in keyof A]: A[Prop];
};
// 等同于
type B = {
a?: string;
readonly b: number;
};
上面示例中,类型B是类型A的映射,把A的可选属性和只读属性都保留下来。
如果要删改可选和只读这两个特性,并不是很方便。为了解决这个问题,TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的?修饰符和readonly修饰符。
- +修饰符:写成+?或+readonly,为映射属性添加?修饰符或readonly修饰符。
- –修饰符:写成-?或-readonly,为映射属性移除?修饰符或readonly修饰符。
备注:+?或-?要写在属性名的后面;+readonly和-readonly要写在属性名的前面;+?修饰符可以简写成?,+readonly修饰符可以简写成readonly
// 示例1:
// 添加可选属性
type Optional<Type> = {
[Prop in keyof Type]+?: Type[Prop];
};:
// 移除可选属性
type Concrete<Type> = {
[Prop in keyof Type]-?: Type[Prop];
};
// 示例2:
// 添加 readonly
type CreateImmutable<Type> = {
+readonly [Prop in keyof Type]: Type[Prop];
};
// 移除 readonly
type CreateMutable<Type> = {
-readonly [Prop in keyof Type]: Type[Prop];
};
// 示例3:
type A<T> = {
+readonly [P in keyof T]+?: T[P];
};
// 等同于
type A<T> = {
readonly [P in keyof T]?: T[P];
};
4. 键名重映射
TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。
键名重映射的语法是在键名映射的后面加上 as + 新类型
子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作
// 示例1:
type A = {
foo: number;
bar: number;
};
type B = {
[p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
fooID: number;
barID: number;
};
下面示例解析:get${Capitalize<string & P>},下面是各个部分的解释。
- get:为键名添加的前缀。
- Capitalize:一个原生的工具泛型,用来将T的首字母变成大写。
- string & P:一个交叉类型,其中的P是 keyof 运算符返回的键名联合类型string|number|symbol,但是Capitalize只能接受字符串作为类型参数,因此string & P只返回P的字符串属性名。
// 示例2:
interface Person {
name: string;
age: number;
location: string;
}
type Getters<T> = {
[P in keyof T
as `get${Capitalize<string & P>}`]: () => T[P];
};
type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
5. 属性过滤
键名重映射还可以过滤掉某些属性;比如下面这种它的键名重映射as T[K] extends string ? K : never]
,使用了条件运算符。如果属性值T[K]
的类型是字符串,那么属性名不变,否则属性名类型改为never
,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性
type User = {
name: string,
age: number
}
type Filter<T> = {
[K in keyof T
as T[K] extends string ? K : never]: string
}
type FilteredUser = Filter<User> // { name: string }