TS学习(2)
🔥keyipatience:个人主页
🎬作者简介:C/C++后端开发学习者
🌟专栏传送门:《c++》《linux》《c++高阶数据结构》《c++数据结构与算法》《前端学习》
⭐️patience is key in life
枚举 enum
枚举(enum):定义一组命名常量,限定一个值只能是里面其中之一,作用等价于字面量类型 + 联合类型。通过 .方式访问
// 定义枚举 enum Direction { Up, Down, Left, Right } // 枚举直接当作类型注解 function changeDirection(direction: Direction) { console.log(direction) }默认数字枚举
enum Direction { Up, // 0 Down, // 1 Left, // 2 Right // 3 } changeDirection(Direction.Up) // 传枚举成员也可以手动设置起始值:
enum Direction { Up = 10, Down, // 11 Left, // 12 Right // 13 }和「字面量联合类型」对比
// 字面量联合写法 type Direction = 'Up' | 'Down' | 'Left' | 'Right'字符串枚举(常用)
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT" } changeDirection(Direction.Up)枚举的特点和原理
enum 是 TS 里少数编译后还保留 JS 代码的特性普通type、接口这些只是类型,编译成 JS 直接删掉; 枚举不只是类型,它编译后会生成真实 JS 对象,运行时也存在。
// TS源码:字符串枚举 enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' }编译出来的 JS
var Direction; (function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {}));对比:枚举 vs 字面量联合类型
// 字面量联合,编译后完全消失,运行时不存在 type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'enum:编译后有 JS 代码,运行时可用;可以动态遍历、取值;但是代码体积更大
type 联合:编译后直接抹除,无运行时代码;更轻量简洁
日常开发优先推荐字面量联合类型
any 类型
any:任意类型。标记为any的变量,可以做任意操作,TS 不再做类型检查,相当于关闭类型保护
let obj: any = { x: 0 } obj.bar = 100 // ✅ 允许随便新增属性 obj() // ✅ 允许把对象当成函数调用(运行时会报错,但TS不提示) const n: number = obj // ✅ 允许直接赋值给number类型,TS不拦截重点原则
尽量不要用 any!TS 的核心价值就是类型校验,any 会直接废掉这个能力。 只有一种场景临时用:类型极其复杂、临时跳过类型校验,优先想别的方案代替。
隐式 any(自动变成 any 的两种情况)
1.只声明变量,不写类型注解、也不给初始值
let a; // 隐式 any ❌ 不推荐2.函数参数不写类型注解
function fn(p) { // p 隐式 any ❌ }规范:这两种情况都要手动补上类型。
和 unknown 的简单区分
any:随便读、随便调用、随便赋值给别的类型,无限制unknown:也是未知类型,但不能随便操作,必须先做类型判断,更安全,是 any 的优选替代方案
typeof(类型查询)
区分两个完全不一样的typeof:
1.JS 运行时 typeof:在代码执行时,得到字符串("string"/"number")
console.log(typeof "Hello world") // 运行输出:string const num = 99 type NumType = typeof num // NumType 就是 number const str = "abc" type StrType = typeof str // string2.TS 类型上下文的 typeof:编译期,拿到变量 / 属性的类型,用来写类型注解
示例对比
let p = { x: 1, y: 2 } // 写法1:手动写出对象类型,即point的类型是XXX function formatPoint(point: { x: number; y: number }) {} // 写法2:TS typeof,直接拿到 p 的类型,简化代码 ✅ function formatPoint(point: typeof p) {}typeof p等价于{ x: number; y: number }
class的基本使用
TS 兼容 ES6 的class,还额外增加类型注解、访问修饰符(public/private/protected)等能力。
class Person {} // 写法1:自动类型推断,p 的类型是 Person(推荐) const p = new Person() // 写法2:手动写类型注解 Person const p: Person = new Person()class本身可以直接当作类型使用在 TS 里,类有双重身份:- 运行时:是构造函数(编译后 JS 真实存在,可以
new) - 类型层面:代表这个类实例的类型,所以写p:Person
- 运行时:是构造函数(编译后 JS 真实存在,可以
- 写
new Person()的时候,TS 会自动推断出变量类型是Person,所以类型注解: Person可以省略。
实例属性初始化
class Person { // 1. 只写类型注解,没有初始值 age: number // 2. 直接赋值初始值,TS自动推断类型,可以省略类型注解 gender = '男' // 等价于:gender: string = '男' }构造函数 constructor
class Person { age: number gender: string constructor(age: number, gender: string) { this.age = age this.gender = gender } }1.必须先在类里声明实例成员(age: number、gender: string)声明之后,才能在构造函数里用this.age/this.gender赋值、访问。
JS 可以直接
this.xxx不用提前声明;TS 要求要先声明。
2.constructor 的特点(考点)
- 参数必须加类型注解,不然参数隐式变成
any; - 构造函数不要写返回值类型:它自动返回当前类的实例,不能写
: Person。
实例实现方法
class Point { x = 10 y = 10 scale(n: number): void { this.x *= n this.y *= n } } const p = new Point() p.scale(2)类继承:extends和implements
extends:继承另一个类(拿到父类的属性、方法,JS、TS 都能用)implements:实现接口(只遵守接口的类型约定,不会继承任何代码,TS 独有,JS 没有)
extends继承类:
class Animal { move() { console.log('Moving along!') } } // Dog 继承 Animal class Dog extends Animal { bark() { console.log('汪!') } } const dog = new Dog() dog.move() // 来自父类Animal dog.bark() // 自己的方法implements 实现接口
interface Singable { sing(): void } class Person implements Singable { sing() { console.log('你是我的小呀小苹果儿') } } //一个类可以同时实现多个接口 interface Singable { sing():void } interface Runable { run():void } class Person implements Singable, Runable { sing(){} run(){} } 也可以一边继承父类、一边实现接口: class Student extends Person implements Singable {}interface(接口)只定义规范,定义一个标准:需要有哪些方法、属性,没有实现代码。implements写在类上,表示这个类遵守这个接口的约定,遵守该接口规则。- 强制要求:类里面必须实现接口声明的全部成员,少一个 TS 就报错。
访问修饰符 public /private/protected
一段代码直接看出区别:
class Person { // public:公开,任何地方都能访问(默认修饰符,可以不写) public name: string // private:私有,**只能在当前类内部**访问,子类、实例都访问不到 private age: number // protected:受保护,当前类 + 子类内部可以访问,外部实例不能访问 protected gender: string constructor(name: string, age: number, gender: string) { this.name = name this.age = age this.gender = gender } say() { console.log(this.name, this.age, this.gender) // 类内部:三个全都能访问 } } class Student extends Person { study() { console.log(this.name) // ✅ public 可以 console.log(this.gender) // ✅ protected 子类内部可以 // console.log(this.age) // ❌ private,子类拿不到 } } const p = new Person("张三", 18, "男") console.log(p.name) // ✅ public,外部可以 // console.log(p.age) // ❌ private,外部报错 // console.log(p.gender) // ❌ protected,外部实例访问报错构造函数参数上可以直接加修饰符语法糖
class Person { // 直接自动生成属性,省去单独声明 constructor(public name:string, private age:number) {} }展开后的完整等价代码:
class Person { public name: string private age: number constructor(name: string, age: number) { this.name = name this.age = age } }readonly 只读修饰符
作用:属性只能初始化一次,之后不能修改,只能读。
class Person { readonly id: number//也可以直接在这里赋值 name: string constructor(id: number, name: string) { this.id = id // ✅ 构造函数里可以赋值(唯一可以修改的地方) this.name = name } } const p = new Person(1001, "小明") console.log(p.id) // ✅ 可以读取 // p.id = 2002 // ❌ 报错!readonly,实例创建后禁止修改 p.name = "小红" // ✅ 普通属性正常修改还可以搭配语法糖(构造参数简写)
class Person { constructor(readonly id: number, public name: string) {} }readonly只限制重新赋值,如果属性是对象 / 数组,里面的内容依然可以改!
class Test { readonly arr: number[] = [1,2,3] } const t = new Test() t.arr.push(4) // ✅ 合法!只是不能 t.arr = [9,9,9]类型兼容性
1.结构化类型系统(Structural,TS、Python)
看形状(有哪些属性、方法),不看名字。长得一样,就兼容。 俗称鸭子类型:看起来像鸭子、叫起来像鸭子 → 就是鸭子。
class Point { x: number; y: number } class Point2D { x: number; y: number } const p: Point = new Point2D() // ✅ 不报错!虽然是两个完全独立的类,名字不一样,但成员一模一样,结构匹配,赋值合法。
2.标明类型系统(Nominal,Java / C# / C++)
看类型名字本身,必须是同一个类或者有继承关系才可以赋值。 上面这段代码放到 Java 直接编译报错,
Point和Point2D就是两个无关类型。
对象的类容兼容性
多的可以赋值给少的,反过来不行
class Point { x: number; y: number } // 少:x、y class Point3D { x: number; y: number; z: number } // 多:x、y、z const p: Point = new Point3D() // ✅ 合法 // const p3: Point3D = new Point() // ❌ 报错!Point 缺 z接口的类型兼容性
规则:和 class 对象的兼容规则一模一样:看结构,不看名字;多的可以赋值给少的,反过来不行
interface Point { x: number; y: number } interface Point2D { x: number; y: number } let p1: Point let p2: Point2D = p1 // ✅ 结构完全一样,可以赋值 interface Point3D { x: number; y: number; z: number } let p3: Point3D p2 = p3 // ✅ Point3D 有 x,y,满足 Point2D 的要求,多了z没关系 // p3 = p2 // ❌ 报错,p2 缺少 zinterface 和 class 互相之间也可以兼容!
class Point3D { x: number; y: number; z: number } let p3: Point2D = new Point3D() // ✅ 合法函数兼容性
参数个数角度:
和前面反过来,少的可以给多的,多的不能给少的
type F1 = (a: number) => void type F2 = (a: number, b: number) => void let f1: F1 let f2: F2 = f1 // ✅ 合法!参数少的 f1 赋值给参数多的 f2参数类型角度
相同位置的参数:原始类型要一模一样;
type F1 = (a: number) => string type F2 = (a: number) => string let f1: F1 let f2: F2 = f1 // ✅ 可以,对应位置参数类型相同 type F3 = (a: string) => string // f2 = f3 ❌,a一个是number、一个是string,原始类型不匹配如果参数是对象(走对象兼容)多的给少的!!
interface P {x:number} interface P3 {x:number; y:number} type FA = (p: P)=>void//参数是p,p的类型是P即对象 type FB = (p: P3)=>void let fa: FA let fb: FB fa = fb // ✅ P3包含P的属性,对象兼容返回值类型角度
原始类型就简单:返回值类型必须相同
type Fa = () => string type Fb = () => string let fa: Fa let fb: Fb = fa // ✅ type Fc = () => number // fa = Fc ❌ string 和 number 不兼容同样如果参数是对象,多的给少的
type F1 = () => {x: number}//返回一个对象,对象必须有 x(数字) type F2 = () => {x: number; y: number}//返回对象,同时有 x 和 y let f1: F1 let f2: F2 f1 = f2 // ✅ OK // f2 = f1 // ❌ 报错,f1返回值少y