目录
3、成员可见性(public、protected、private)
通过结合各种类型操作符,用一种简洁、可维护的方式来表达复杂的操作和值;用现有的类型或值来表达一个新类型的方法。
- 泛型 —— 带参数的类型
- Keyof 类型操作符 —— keyof 操作符创建新类型
- Typeof 类型操作符 —— 使用 typeof 操作符来创建新的类型
- 索引访问类型 —— 使用 Type['a'] 语法来访问一个类型的子集
- 条件类型 —— 在类型系统中像if语句一样行事的类型
- 映射类型 —— 通过映射现有类型中的每个属性来创建类型
- 模板字面量类型 —— 通过模板字面字符串改变属性的映射类型
- // 业务场景
- // 定义一个函数,这个函数将返回传入的任何参数(捕获参数的类型,以便返回内容)
- function returnType<Type>(data: Type): Type{
- return Type
- }
-
- // 调用该函数的方式
- // 方式一:显示指定函数接收 与 返回值 的类型
- let data = returnType<String>("Hello")
-
- // 方式一:不显式指定,使用隐式的类型推断
- let data = returnType("Hello")
-
- // ********************************泛型 类 与 接口************************************
- // 泛型接口
- interface GenericId<Type> {
- (arg: Type): Type;
- }
- function identity<Type>(arg: Type): Type {
- return arg;
- }
- let myIdentity: GenericId = identity
-
-
- // 泛型类
- class GenterId<Type>{
- id: Type;
- addId: (x: Type , y: Type) => Type
- }
- let newGenterId = new GenterId<Number>;
- newGenter.id = 1123432;
- newGenter.addId = function(x,y){
- return (x+y)*10;
- }
- function identity<Type>(arg: Type): Type {
- console.log(arg.length) // 会报语法错误,因为typescript会自动推断且不能保证arg是否包含length属性
- }
-
- // 给上述代码添加 类型限制
- interface hasLength {
- length: number
- }
- function identity<Type extends hasLength>(arg: Type): Type {
- console.log(arg.length) // 做了类型限制(不管传递什么值,都要有length这个属性,否则在函数调用的时候就会报错),就不会报错了
- }
声明一个受另一个类型参数约束的类型参数。例如,在这里从一个给定名称的对象中获取 一个属性值。
确保不会意外地获取一个不存在于 对象 上的属性,所以我们要在这两种类型之间放置一个约束条件。
- // 这里的 Key extends keyof Type 表示Key的类型值,只能式Type类型中的key值
- function getProp<Type,Key extends keyof Type>(obj: Type,key: Key){
- return obj[key]
- }
-
- // 这个key值,只能是'a','b','c'中的一个,其他的值都会报错
- console.log(getProp({'a':1111,'b':2222, 'c':3333},'a'));
- keyof 运算符接收一个对象类型,并产生其键的字符串或数字字面联合("x"|"y ")。
- 如果该类型有一个字符串或数字索引签名, keyof 将返回这些索引的类型。
- type Point = { x: number; y: number };
- type P = keyof Point; // 这里的P的类型为 'x'|'y'
- const p1:P = 'x'
- const p2:P = 'y'
-
-
-
- // 当一个对象有索引时,keyof 的值就是其索引对应的值
- type Arrayish = { [n: number]: unknown };
- type A = keyof Arrayish;
- const a:A = 0
-
- // M 是 string|number 是因为JavaScript对象的键总是被强制为字符串,所以 obj[0] 总是与 obj["0"] 相同
- type Mapish = { [k: string]: boolean };
- type M = keyof Mapish;
- const m:M = 'a'
- const m2:M = 1
在JavaScript中typeof是用来判断对象类型的。TypeScript添加了一个 typeof 操作符,可以在类型上下文中使用它来引用一个变量或属性的类型。
- let s = "hello";
- let n: typeof s;
- n = 'world'
- n= 100 // 报错,n的类型是string
- ype Person = { age: number; name: string; alive: boolean };
- type Age = Person["age"]; // 这个时候Age的类型就是索引age对应的类型number
- let A: Age = 100; // 正确
- let B: Age = '100'; // 错误
索引类型本身就是一个类型,所以可以使用 unions、 keyof 或者其他类型。
- interface Person {
- name: string,
- age: number,
- alive: boolean
- }
-
- // 索引 => 联合类型
- // I 的类型为 number | boolean
- type I = Person['age' | 'alive']
-
-
- // 索引 => keyof操作符
- // I 的类型为 string | number | boolean
- type I = Person[keyof Person];
-
- interface Animal {
- live: void()
- }
-
- interface Dog extends Animal {
- wfun: void
- }
-
-
- // 通过条件表达式动态决定类型
- // type expl1 = number
- type expl1 = Dog extends Animal ? number : string
-
- // type expl1 = string
- type expl2 = Date extends Animal ? number : string
条件类型中的检查会给一些提示信息。就像用类型守卫缩小范围那样给一个更具体的类型一样,条件类型的真正分支将通过检查的类型进一步约束泛型。
- // 会报语法错误: T没有索引message
- type MessageOf
= T["message"] -
- // 给T做一个类型限制就可以了
- type MessageOf
extends {message: unknown}> = T["message"] -
- // 使用条件类型去判断,也可对T进行类型限制
- type MessageOf
= T extends { message: unknown } ? T["message"] : never; - interface Email {
- message: string;
- }
- interface Dog {
- bark(): void;
- }
- // type EmailMessageContents = string
- type EmailMessageContents = MessageOf<Email>;
-
- // type DogMessageContents = never
- type DogMessageContents = MessageOf<Dog>;
条件类型提供了一种方法来推断在真实分支中使用 infer 关键字进行对比的类型。
- // 可以在 Flatten 中推断出元素类型,而不是用索引访问类型 "手动 "提取出来。
- type Flatten<Type> = Type extends Array
Item> ? Item :Type;
使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如 何在真实分支中检索 T 的元素类型。 可以使用 infer 关键字编写一些有用的辅助类型别名。
3、分布式条件类型
当给条件类型定义的新类型传入一个联合类型时,就会变成分布式
- type toArray<Type> = Type extends any ? Type[] : never
-
- // 给toArray传入一个联合类型,得到的新类型为 type newData = String[] | Number[]
- type newData = toArray<String | Number>
-
-
-
- // 如果想要避免得到这样 String[] | Number[] 类型,可以对toArray做个调整
- type toArray<Type> = [Type] extends [any] ? Type[] : never
- // type newData = (String | Number)[]
- type newData = toArray<String | Number>
映射类型是一个通用类型,他使用PropertyKeys(就是利用keyof),迭代键来创建一个类型
- type optionKeys<Type> = {
- [Property in keyof Type]: boolean
- }
-
- type transferData = {
- getData: () => void,
- returnData: () => void
- }
-
- // 使用optionKeys 和 transferData 创建一个新的类型
- // type newType = {getData: boolean, returnData: boolean}
- type newType = optionKeys
在映射过程中,有两个额外的修饰符可以应用: readonly 和 ? ,它们分别影响可变性和可选性。 你可以通过用 - 或 + 作为前缀来删除或添加这些修饰语。如果你不加前缀,那么就假定是 + (默认值) 。
- type CreateMutable<Type> = {
- // 从一个类型的属性中删除 "readonly"属性
- -readonly [Property in keyof Type]: Type[Property];
- };
- type LockedAccount = {
- readonly id: string;
- readonly name: string;
- };
- /*
- type UnlockedAccount = {
- id: string;
- name: string;
- }
- */
-
- type UnlockedAccount = CreateMutable<LockedAccount>;
- // 从一个类型的属性中删除 "可选" 属性
- type Concrete<Type> = {
- [Property in keyof Type]-?: Type[Property];
- };
- type MaybeUser = {
- id: string;
- name?: string;
- age?: number;
- };
- /*
- type User = {
- id: string;
- name: string;
- age: number;
- }
- */
- type User = Concrete<MaybeUser>;
2、通过as做key的重映射
在TypeScript 4.1之后的版中,都可以使用as重新映射映射类型中的键
- type MapNewKeyOfMap<Type> = {
- [Property in keyof Type as newType]: Type[Property]
- }
-
- // 可以利用其他功能,从先前的属性名称中创建新的属性名称。
- type Getters<Type> = {
- [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
- };
- interface Person {
- name: string;
- age: number;
- location: string;
- }
- /*
- type LazyPerson = {
- getName: () => string;
- getAge: () => number;
- getLocation: () => string;
- }
- */
- type LazyPerson = Getters<Person>
TypeScript 对 ES2015中的 class 关键字完全支持。
与其他JavaScript语言功能一样,TypeScript增加了类型注释和其他语法,允许你表达类和其他类型之间的关系
- // 声明并初始化属性的两种方式 (如果只声明,不初始化就会报错)
-
- // 方式一
- class pointer {
- x: number = 0
- y: number = 0
- }
- // 方式二,在constructor函数中初始化
- class pointer {
- x: number
- y: number
- constructor(){
- this.x = 0
- this.y = 0
- }
- }
字段的前缀可以是 readonly 修饰符。这可以防止在构造函数之外对该字段进行赋值。
- class Greeter {
- readonly name: string = "world";
- constructor(otherName?: string) {
- if (otherName !== undefined) {
- this.name = otherName; // name 只读属性的值只能在 constructor 函数中被修改
- }
- }
- err() {
- this.name = "not ok"; // name是只读属性,重新修改他的值会报错
- }
- }
- const g = new Greeter();
- g.name = "also not ok"; // name是只读属性,重新修改他的值会报错
类构造函数与函数非常相似,可以添加带有类型注释的参数、默认值和重载:
- class Point {
- x: number;
- y: number;
- // 带默认值的正常签名
- constructor(x = 0, y = 0) {
- this.x = x;
- this.y = y;
- }
- }
- class Point {
- // 重载
- constructor(x: number, y: string);
- constructor(s: string);
- constructor(xs: any, y?: any) {
- // ...
- }
- }
Super 调用 :
就像在JavaScript中一样,如果你有一个基类,在使用任何 this. 成员之前,你需要在构造器主体中调 用 super()。
- class A {
- data: String = "Hello"
- }
-
- class B extends A {
- dataB: String
- constructor(){
- super()
- this.dataB = "world"
- }
- }
注意:在类的方法体中,仍然必须通过 this 访问字段和其他方法。
- class Point {
- x = 10;
- y = 10;
- scale(n: number): void {
- this.x *= n;
- this.y *= n;
- }
- }
- class A {
- _data = 0
- get data(){
- return this._data
- }
-
- set data(value){
- this._data = value
- }
- }
注意:一个没有额外逻辑的字段支持的 get/set 对在JavaScript中很少有用。如果你不需要在 get/set 操作中添加额外的逻辑,暴露公共字段也是可以的。
TypeScript对访问器有一些特殊的推理规则:
- 如果存在 get ,但没有 set ,则该属性自动是只读的
- 如果没有指定 setter 参数的类型,它将从 getter 的返回类型中推断出来
- 访问器和设置器必须有相同的成员可见性
类可以声明索引签名;这些签名的作用与其他对象类型的索引签名相同。
- class MyClass {
- [s: string]: boolean | ((s: string) => boolean);
- check(s: string) {
- return this[s] as boolean;
- }
- }
使用 implements 去继承接口。如果一个类不能正确地实现去实现接口,就会发出一个错误(将接口内的所有方法重写一遍)。
- interface Pingable {
- ping(): void;
- }
- class Sonar implements Pingable {
- ping() {
- console.log("ping!");
- }
- }
-
- // 这个类 Ball 会报错,因为没有实现接口中的方法ping
- class Ball implements Pingable {
- pong() {
- console.log("pong!");
- }
- }
注意:
重要的是要明白, implements 子句只是检查类是否可以被当作接口类型来对待。它根本不会改变类的类型或其方法。一个常见的错误来源是认为 implements 子句会改变类的类型
- interface Checkable {
- check(name: string): boolean;
- }
-
- class NameChecker implements Checkable {
- check(s) {
- // 接口不会影响类实现时的参数类型,所以这里的s 有 any类型的隐式转换,所以s不能调用toLowercse 这个方法
- return s.toLowercse() === "ok";
- }
- }
-
-
-
- // 假如接口中存在可选属性,那么类在实现这个接口的时候写不写都不影响,但是如果不实现的话,这个类在实例化的时候就不能调用接口那个可选参数
- interface A {
- x: number;
- y?: number;
- }
- class C implements A {
- x = 0;
- }
- const c = new C();
- c.y = 10; // 会报错,因为类C没有实现可选参数y,故可选参数y不能被其实例化对象调用
类与类之间的继承关系可以使用关键字extends去实现
- class Animal {
- move() {
- console.log("Moving along!");
- }
- }
- class Dog extends Animal {
- woof(times: number) {
- for (let i = 0; i < times; i++) {
- console.log("woof!");
- }
- }
- }
- const d = new Dog();
- // 父类的类方法
- d.move();
- // 子类的类方法
- d.woof(3);
子类也可以覆盖父类的一个字段或属性。你可以使用 super. 语法来访问父类方法。
注意:
因为 JavaScript类是一个简单的查找对象,没有 "超级字段 "的概念。
- class Base {
- greet() {
- console.log("Hello, world!");
- }
- }
- class Derived extends Base {
- // 重写父类的方法greet 正常greet这个函数是跟父类一致的应该没有参数的,所以name用的是可选参数,如果不是的话,则不属于重写,而是重新定义的一个新的函数
- greet(name?: string) {
- if (name === undefined) {
- super.greet();
- } else {
- console.log(`Hello, ${name.toUpperCase()}`);
- }
- }
- }
- const d = new Derived();
- d.greet();
- d.greet("reader");
- class Base {
- name = "base";
- constructor() {
- console.log("My name is " + this.name);
- }
- }
- class Derived extends Base {
- name = "derived";
- constructor() {
- super()
- console.log("My name is " + this.name);
- }
- }
- // 打印 "My name is base",然后才是 "My name is derived"
- const d = new Derived();
按照JavaScript的定义,类初始化的顺序是:
- 父类的字段被初始化
- 父类构造函数运行
- 子类的字段被初始化
- 子类构造函数运行
这意味着基类构造函数在自己的构造函数中看到了自己的name值,因为派生类的字段初始化还没有运 行。
在继承内置类型的时候,子类在实例化一个新的对象,并使用这个对象调用类中的方法的时候可能会出现“xxx is not a function”这个错误。这个时候需要在子类的构造函数中进行特殊操作
- class MsgError extends Error {
- constructor(m: string) {
- super(m);
- // 明确地设置原型。(不明确设置原型,在实例化对象之后调用sayHello方法会有运行时错误)
- Object.setPrototypeOf(this, MsgError.prototype)
- }
- sayHello() {
- return "hello " + this.message;
- }
- }
类成员的默认可见性是公共( public )的。一个公共( public )成员可以在任何地方被访问。 因为 public 已经是默认的可见性修饰符,所以你永远不需要在类成员上写它,但为了可读性的原 因,可能会选择将其写上。
受保护的( protected )成员只对它们所声明的类的子类可见。
- class Greeter {
- public greet() {
- console.log("Hello, " + this.getName());
- }
- protected getName() {
- return "hi";
- }
- }
- class SpecialGreeter extends Greeter {
- public howdy() {
- // 在此可以访问受保护的成员
- console.log("Howdy, " + this.getName());
- }
- }
- const g = new SpecialGreeter();
- g.greet(); // 没有问题
- g.getName(); // 报错 无权访问
受保护成员的暴露:
子类需要遵循它们的父类契约,但可以选择公开具有更多能力的父类的子类型。这包括将受保护的成 员变成公开(感觉像废话,其实就是在子类中直接将父类受保护的属性直接定义为公共的就可以实现其成员的暴露)
- lass Base {
- protected m = 10;
- }
- class Derived extends Base {
- // 没有修饰符,所以默认为'公共'('public') 也就实现了方法的暴露
- m = 15;
- }
- const d = new Derived();
- console.log(d.m); // OK
3、private
private 和 protected 一样,但不允许从子类中访问该成员。 私有( private )成员对子类是不可见的,所以子类不能增加其可见性。
类可以有静态成员。这些成员并不与类的特定实例相关联。它们可以通过类的构造函数对象本身来访问。
- class MyClass {
- static x = 0;
- static printX() {
- console.log(MyClass.x);
- }
- }
- // 类中的静态成员,只能类本身访问,不与类的实例对象相关联
- console.log(MyClass.x);
- MyClass.printX();
- // 静态成员也可以使用相同的 public 、 protected 和 private 可见性修饰符。
- class MyClass {
- private static x = 0;
- static printX() {
- console.log(MyClass.x);
- }
- }
-
-
- // 静态成员也会被继承。
- class Base {
- static getGreeting() {
- return "Hello world";
- }
- }
- class Derived extends Base {
- myGreeting = Derived.getGreeting();
- }
函数原型覆盖属性是不安全的。因为类本身就是可以用 new 调用的函数,所以某些静态名称不能使用。像 name 、 length 和 call 这样的函数属性(都属于其内置属性),定义为静态成员是无效的。
静态块允许写一串有自己作用域的语句,可以访问包含类中的私有字段。这意味着我们可以用写语句的所有能力来写初始化代码,不泄露变量,并能完全访问我们类的内部结构。
- class Foo {
- static #count = 0; // 私有字段count,#只能在es2015之后使用
- get count() {
- return Foo.#count;
- }
- // 静态区块
- static {
- try {
- const lastInstances = {
- length: 100
- };
- Foo.#count += lastInstances.length;
- }
- catch {}
- }
- }
类可以像接口一样使用通用约束和默认值。 静态成员中的类型参数
- class Box<Type> {
- // 报错 静态成员不能引用类的类型参数。
- static defaultValue: Type;
- }
- // Box
.defaultValue = 'hello' - // console.log(Box
.defaultValue
TypeScript并没有改变JavaScript的运行时行为,而JavaScript的运行时行为偶尔很奇 特。 比如,JavaScript对这一点的处理确实是不寻常的
- class MyClass {
- name = "MyClass";
- getName() {
- return this.name;
- }
- }
- const c = new MyClass();
- const obj = {
- name: "obj",
- getName: c.getName,
- };
- // 输出 "obj", 而不是 "MyClass" this指向很奇特
- console.log(obj.getName());
-
-
- // 想要解决这种this指向问题的方法如:
- // 1、在类中定义方法时,使用箭头函数
- class MyClass {
- name = "MyClass";
- getName = () => {
- return this.name;
- };
- }
- const c = new MyClass();
- const obj = {
- name: "obj",
- getName: c.getName,
- };
- // 输出 "MyClass"
- console.log(obj.getName());
-
- // 2、this参数
- // 在方法或函数定义中,一个名为 this 的初始参数在TypeScript中具有特殊的意义。这些参数在编译过程中会被删除
- class MyClass {
- name = "MyClass";
- getName(this: MyClass) {
- return this.name;
- };
- }
- const c = new MyClass();
- // 输出 "MyClass"
- console.log(c.getName());
TypeScript提供了特殊的语法,可以将构造函数参数变成具有相同名称和值的类属性。这些被称为参数属性,通过在构造函数参数前加上可见性修饰符 public 、 private 、protected 或 readonly 中的一 个来创建。
- class Params {
- // 参数属性
- constructor(public readonly x: number, protected y: number, private z: number)
- {
- // ……
- }
- }
- const a = new Params(1, 2, 3);
- // (property) Params.x: number
- console.log(a.x);
- console.log(a.z
类表达式与类声明非常相似。唯一的区别是,类表达式不需要一个名字,可以通过它们最终绑定的任何标识符来引用它们。
- const someClass = class<Type> {
- content: Type;
- constructor(value: Type) {
- this.content = value;
- }
- };
- // const m: someClass
- const m = new someClass("Hello, world")
抽象的方法或抽象的字段是没有提供实现的方法或字段。这些成员必须存在于一个抽象类中, 不能直接实例化。 抽象类的作用是作为子类的父类,实现所有的抽象成员。
- abstract class Base {
- abstract getName(): string;
- printName() {
- console.log("Hello, " + this.getName());
- }
- }
- // const b = new Base(); //这是错误的,抽象类不能直接被实例化
-
- class Derived extends Base {
- getName() {
- return "world";
- }
- }
- const d = new Derived(); // 继承抽象类Base,并将抽象类中的所有抽象方法都给重写了,不然会报错
- d.printName()