• typescript手记


    TypeScript 常用方法

    never 与 void

    在 js 中 void 是一个操作符,它可以让任何表达式返回 undefined, undefined 不是一个保留字

    // 可以对undefined进行声明赋值
    (function () {
    	const undefined = 0;
    	console.log(undefined);
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用 void 0 则则表示 undefined

    never: 一个函数如果抛出了一个错误,则它永远不会有返回值,则返回值类型为 never

    // never
    let error = () => {
    	throw new Error("error");
    };
    
    • 1
    • 2
    • 3
    • 4
    对象类型接口
    interface List {
    	id: number;
    	name: string;
    }
    interface Result {
    	data: List[];
    }
    
    function fn(data: Result) {}
    const result = {
    	data: [
    		{ id: 1, name: "jj" },
    		{ id: 2, name: "cc" },
    	],
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    使用过程中返回的数据可能会增加一些属性,例如

    const result = {
    	data: [
    		{ id: 1, name: "jj", desc: "test" },
    		{ id: 2, name: "cc" },
    	],
    };
    fn(result); // ts并没有报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ts 并没有报错 ts 采取了一种鸭式变形法

    如果传入对象字面量的话,ts 就会对额外的字段进行检查

    fn({
    	data: [
    		{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
    		{ id: 2, name: "cc" },
    	],
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这种情况需要使用类型断言
    例如 1 建议使用 1,因为下面(例 2)这种在 React 中可以产生歧义

    fn({
    	data: [
    		{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
    		{ id: 2, name: "cc" },
    	],
    } as Result);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    例如 2

    fn(<Result>{
    	data: [
    		{ id: 1, name: "jj", desc: "test" }, // 会报错 desc
    		{ id: 2, name: "cc" },
    	],
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    索引签名
    // 用任意字符串索引 得到字符串
    interface Item {
    	[index: string]: string;
    }
    
    • 1
    • 2
    • 3
    • 4

    这样声明之后,就不能再声明一个 number 类型的成员了,如

    interface Item {
    	[index: string]: string;
    	key: number; //报错
    }
    
    • 1
    • 2
    • 3
    • 4

    两种签名是可以混用的,所以 还可以在 Item 中增加一个数字索引签名 如

    interface Item {
    	[index: string]: string;
    	[key: number]: string; //如果是 number就会报错,  如果上面是any  那么number就可以
    }
    
    • 1
    • 2
    • 3
    • 4
    函数类型接口
    function fn(x: number, y: number) {
    	return x + y;
    }
    
    • 1
    • 2
    • 3
    • 通过变量定义
    let myFn = (x: number, y: number) => number;
    
    • 1
    • 通过类型别名定义
    type MyFn = (x: number, y: number) => number;
    
    • 1
    • 通过接口定义
    interface MyFn {
    	(x: number, y: number): number;
    }
    
    • 1
    • 2
    • 3
    混合类型接口
    interface MixinFn {
    	(): void; //表示这个类是个函数,返回值是void
    	version: string;
    	toDo(): void;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    MixinFn 的实现

    let lib:MixinFn = (()=>{}) as MixinFn
    lib.version='1.0.0'
    lib.toDo = ()=>
    
    
    • 1
    • 2
    • 3
    • 4
    函数重载

    函数重载指的是在同一个类中定义多个同名函数,但它们的参数列表不同(参数类型、参数个数或参数顺序)。编译器根据调用时提供的参数信息,来确定应该调用哪个函数。函数重载可以提供不同的方法签名,使得同一个函数名可以用于处理多个不同的情况。

    class A {
    	sum(a: number, b: number) {
    		return a + b;
    	}
    }
    
    class B extends A {
    	sub(a: number, b: number) {
    		return a - b;
    	}
    }
    
    function maker(): B;
    function maker(p: string): A;
    function maker(p?: string) {
    	if (p) {
    		return new A();
    	}
    	return new B();
    }
    function maker(p?: string) {
    	if (p) {
    		return new A();
    	}
    	return new B();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    断言

    使用断言使编译器通过检查,不过不推荐这种写法,而应该使用重载

    type NumGenerator1 = () => number;
    function myFunc1(numGenerator1: NumGenerator1 | undefined) {
    	const num1 = numGenerator1!();
    	const num2 = numGenerator1!();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    重载
    type NumGenerator2 = () => number;
    function myFunc2(): undefined;
    function myFunc2(numGenerator2?: NumGenerator2) {
    	if (numGenerator2) {
    		const num1 = numGenerator2();
    		const num2 = numGenerator2();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    可选项
    type NumGenerator3 = () => number;
    function myFunc3(numGenerator3: NumGenerator3 | undefined) {
    	const num1 = numGenerator3?.();
    }
    
    • 1
    • 2
    • 3
    • 4

    ts 的类覆盖了 js 的类,同时加入了一些新特性
    无论在 ts 中还是 es 中,类成员的属性,都是实例属性,而不是原型属性,类成员的方法,都是实例方法。

    class Cat {
    	constructor(name: string) {
    		this.name = name;
    	}
    	name: string;
    	run() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    继承

    // 构造函数中一定要调用super
    class ChildCat extends Cat {
    	constructor(name: string, public color: string) {
    		super(name);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    修饰符
    • public 默认都是 public,含义就是对所有人都是可见的
    • private 私有成员,只能在类的本身调用,不能被实例调用,也不能被子类调用
    • protected 受保护成员,只能在类或者子类中访问,而不能在类的实例中访问
    • constructor 也可以添加 protected,表明这个类不能实例化,只能被继承
    • readonly 一定要初始化,跟实例属性是一样

    除了类成员可以添加修饰符之外,构造函数的参数也可以添加修饰符,它的作用就是将参数自动变成实例的属性,这样就省略在类中的定义了

    class ChildCat extends Cat {
    	constructor(name: string, public color: string) {
    		super(name);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    static,类的静态修饰符,静态成员也可以被继承

    抽象类

    es 中并没有抽象类,这是 ts 对类的拓展,所谓抽象类,就是只能被继承而不能实例化的类。在抽象类中可以定义一个方法,它可以有具体的实现,这样子类就不用实现了,就实现了方法的复用。在抽象类中也可以不指定具体的方法实现,这就构成了抽象方法。抽象方法的好处就是你明知道子类中有其他的实现,那就没必要在父类中实现了。

    abstract class Animal {
    	eat() {
    		console.log("eat");
    	}
    	abstract sleep(): void;
    }
    class Chicken extends Animal {
    	sleep() {
    		console.log("sleep");
    	}
    }
    const chicken = new Chicken();
    chicken.eat();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    链式调用

    链式调用的核心就在于调用完的方法将自身的实例返回

    class WorkFlow {
    	step1() {
    		return this;
    	}
    	step2() {
    		return this;
    	}
    }
    new WorkFlow().step1().step2();
    
    class Myflow extends WorkFlow {
    	next() {
    		return this;
    	}
    }
    new Myflow().next().step1().next().step2();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    类型与接口的关

    一个接口可以约束一个类成员有哪些属性以及他们的类型

    interface Human {
    	name: string;
    	eat(): void;
    }
    
    • 1
    • 2
    • 3
    • 4

    类实现接口的时候必须实现接口中声明的所有属性和方法
    接口只能约束类的公有成员

    interface Human {
    	name: string;
    	eat(): void;
    }
    
    • 1
    • 2
    • 3
    • 4
    class Asian implements Human {
    	constructor(name: string) {
    		this.name = name;
    	}
    	name: string;
    	eat() {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    接口的继承

    接口可以像类一样相互继承,并且一个接口可以继承多个接口

    // Human ==> name  eat
    interface Man extends Human {
    	run(): void;
    }
    interface Child {
    	cry(): void;
    }
    interface Boy extends Man, Child {}
    
    const boy: Boy = {
    	name: "",
    	run() {},
    	eat() {},
    	cry() {},
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    从接口的继承可以看出,接口的继承可以抽离出可重用的接口,也可以将多个接口合并成一个接口

    接口继承 类

    接口除了可以继承接口之外,还可以继承类,这就相当于接口把类的成员都抽象出来,也就是只有类的成员结构,而没有具体的实现

    class Auto {
    	state = 1;
    }
    interface AutoInterface extends Auto {}
    
    • 1
    • 2
    • 3
    • 4

    这样 AutoInterface 接口就隐含了 state 属性,要想实现这个 AutoInterface 接口只要一个类的成员和 state 的属性就可以了

    class C implements AutoInterface {
    	state = 1;
    }
    class Bus extends Auto implements AutoInterface {}
    // 在这个例子中我们就不必实现 state 属性了,因为 Bus 是 Auto 的子类自然继承了 state 属性 。这里需要额外注意的是,接口在抽离类的成员的时候,不仅抽离了公共成员,而且抽离了私有成员和受保护成员。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    泛型函数与泛型接口

    很多时候我们希望一个函数或者一个类可以支持多种数据类型,有很大的灵活性

    function print(value: string): string {
    	console.log(value);
    	return value;
    }
    // 重载
    function print(value: string): string;
    function print(value: string[]): string[];
    function print(value: any) {
    	console.log(value);
    	return value;
    }
    // 联合类型
    function print(value: string | string[]): string | string[] {
    	console.log(value);
    	return value;
    }
    // any类型
    function print(value: any) {
    	console.log(value);
    	return value;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    泛型概念

    不预先确定类型,在使用的时候再确定

    function log<T>(value: T): T {
    	console.log(value);
    	return value;
    }
    log<string[]>(["a", "b"]);
    log(["a", "b"]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们不仅可以用泛型来定义一个函数,也可以定义一个函数类型

    type Log = <T>(value: T) => T;
    const myLog: Log = log;
    
    • 1
    • 2

    泛型接口

    这个和类型别名的定义方式是等价的,但目前这个泛型仅约束了一个函数,也可以用泛型来约束其他成员

    interface Log1 {
    	<T>(value: T): T;
    }
    
    • 1
    • 2
    • 3

    这样接口的所有成员都受到了泛型的约束

    这里需要注意的是当泛型约束了整个接口之后,在实现的时候,我们必须指定一个类型

    let myLog1: Log2<number> = log;
    myLog1(1);
    
    • 1
    • 2

    如果不指定类型也可以在接口的定义中指定一个默认类型

    interface Log3<T = string> {
    	(value: T): T;
    }
    let myLog2: Log3 = log;
    myLog2("s");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    泛型小结

    把泛型变量和函数的参数等同对待,泛型只不过是另一个维度的参数,是代表类型而不是代表值的参数

    函数和类可以轻松地支持多种类型,增强程序的拓展性
    不必写多条函数重载,冗长的联合类型声明,增强代码可读性
    灵活控制类型之间的约束

    泛型类与泛型约束
    • 泛型类
      与泛型接口非常类似,泛型也可以约束类的成员,需要注意的是泛型不能应用于类的静态成员
    class Ame<T> {
    	run(value: T) {
    		console.log(value);
    		return value;
    	}
    }
    const ame = new Ame<number>();
    ame.run(1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当不指定类型参数的时候,value 值就可以是任意的值

    const ame1 = new Ame();
    ame1.run({ a: 1 });
    ame1.run("ss");
    
    • 1
    • 2
    • 3
    类型约束
    interface Length {
    	length: number;
    }
    function print<T extends Length>(value: T): T {
    	console.log(value, value.length);
    	return value;
    }
    // /参数需要具有length属性
    
    print([1]);
    print("ss");
    print({ length: 1 });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    类型检查机制

    TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。

    • 作用:辅助开发,提高开发效率
    • 类型推断
    • 类型兼容性
    • 类型保护
    类型推断
    interface AmeFoo {
    	name: string;
    }
    let ameFoo = {} as AmeFoo;
    ameFoo.name = "coboy";
    
    • 1
    • 2
    • 3
    • 4
    • 5

    推荐在声明的时候就指定类型 即:

    let ameFoo1: AmeFoo = { name: "coboy" };
    
    • 1

    类型断言可以增加我们代码的灵活性,在改造一些旧代码的时候非常有效,但使用类型断言,要注意避免滥用,要对上下文的环境要有充足的预判,没有任何根据的类型断言会带来安全的隐患,总之 TS 的类型推断可以为我们提供重要的辅助信息,应该善加利用

    类型兼容性

    当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y

    X 兼容 Y:X(目标类型)= Y(源类型)

    之所以我们要讨论类型兼容性问题,是因为 TS 允许我们把一些类型不同的变量相互赋值。类型兼容性的例子广泛存在于接口、函数和类中。

    接口兼容性
    interface AmeX {
    	name: any;
    	age: any;
    }
    interface AmeY {
    	name: any;
    	age: any;
    	height: any;
    }
    let ameX: AmeX = { name: "coboy", age: 25 };
    let ameY: AmeY = { name: "cobyte", age: 25, height: 180 };
    ameX = ameY;
    ameY = ameX; // Property 'height' is missing in type 'AmeX' but required in type 'AmeY'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里再次体现了 TS 的检测原则,也就是鸭式变形法(一只鸟走起来像鸭子,游起来像鸭子,叫起来像鸭子,就可以被认为是鸭子)

    源类型必须具备目标类型的必要属性,就可以进行赋值(成员少的会兼容成员多的)

    函数兼容性

    要判断两个函数是否兼容通常发生在相互赋值的情况下,也就是函数作为参数的情况下

    type Handler = (x: number, y: number) => void;
    function add(handler: Handler) {
    	return handler;
    }
    
    • 1
    • 2
    • 3
    • 4
    参数个数
    let handler1 = (x: number) => {};
    add(handler1);
    let handler2 = (x: number, y: number, z: number) => {};
    add(handler2); // Argument of type '(x: number, y: number, z: number) => void' is not assignable to parameter of type 'Handler'
    
    • 1
    • 2
    • 3
    • 4
    • 可选参数和剩余参数
    let fun1 = (p1: number, p2: number) => {};
    let fun2 = (p1?: number, p2?: number) => {};
    let fun3 = (...args: number[]) => {};
    
    • 1
    • 2
    • 3
    • 固定参数可以兼容可选参数和剩余参数
    fun1 = fun2;
    fun1 = fun3;
    
    • 1
    • 2
    • 可选参数是不兼容固定参数和剩余参数
    fun2 = fun3; // error
    fun2 = fun1; // error
    // 可以将strictFunctionTypes设置为false
    
    • 1
    • 2
    • 3
    • 剩余参数可以兼容固定参数和可选参数
    fun3 = fun1;
    fun3 = fun2;
    
    • 1
    • 2
    参数类型
    let handler3 = (a: string) => {};
    add(handler3); // error
    
    • 1
    • 2
    interface Point3D {
    	x: number;
    	y: number;
    	z: number;
    }
    interface Point2D {
    	x: number;
    	y: number;
    }
    let p3d = (point: Point3D) => {};
    let p2d = (point: Point2D) => {};
    p3d = p2d;
    p2d = p3d; // error
    // 可以将strictFunctionTypes设置为false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这种函数的参数之间可以赋值的情况,叫做函数参数的双向协变,这种情况允许我们把一个精确的类型赋值给一个不那么精确的类型,这样做我们就不需要把一个不精确的类型断言成一个精确的类型。

    返回值类型

    ts 要求我们目标的返回值类型必须与源函数的返回值类型相同,或者为其子类型

    let fun4 = () => ({ name: "coboy" });
    let fun5 = () => ({ name: "cobyte", age: 18 });
    fun4 = fun5;
    fun5 = fun4; // error
    
    • 1
    • 2
    • 3
    • 4
    function overload(x: number, y: number): number;
    function overload(x: string, y: string): string;
    function overload(x: any, y: any): any {}
    
    • 1
    • 2
    • 3

    函数重载分为两部分,第一部分就是函数重载的列表,第二部分就是函数的具体实现,这里列表中的函数就是目标函数,而具体的实现函数就是源函数。程序在运行的时候,编译器会查找重载列表,然后使用第一个匹配的定义来执行下面的函数,所以在重载列表中,目标函数的参数要多于源函数的参数,而且返回值类型也要符合相应的要求

    枚举兼容性

    枚举类型和数字类型是可以完全相互兼容的

    枚举之间是完全不兼容的

    enum Fruit {
    	Apple,
    	Banana,
    }
    enum Color {
    	Red,
    	Yellow,
    }
    let fruit: Fruit.Apple = 3;
    let no: number = Fruit.Apple;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    类的兼容性
    class AmeByte1 {
    	constructor(x: number, y: number) {}
    	id: number = 1;
    }
    class AmeByte2 {
    	static x = 1;
    	constructor(p: number) {}
    	id: number = 2;
    }
    let amebyte1 = new AmeByte1(1, 2);
    let amebyte2 = new AmeByte2(1);
    amebyte1 = amebyte2;
    amebyte2 = amebyte1;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    类的兼容性和接口的比较相似,他们也只是比较结构。注意:在比较两个类是否兼容的时候,静态成员和构造函数是不参与比较的,如果两个类具有相同的实例成员,那么他们的实例就可以互相兼容。
    如果两个类含有私有成员,那么这两个类就不兼容了,这个时候只有父类和子类之间是互相兼容的。

    泛型兼容性
    interface Empty1<T> {}
    let obj1: Empty1<number> = {};
    let obj2: Empty1<string> = {};
    obj1 = obj2;
    
    interface Empty2<T> {
    	value: T;
    }
    let obj3: Empty2<number> = {}; // error
    let obj4: Empty2<string> = {}; // error
    obj3 = obj4; // error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    只有类型参数 T 被接口成员使用的时候,才会有影响泛型的兼容性

    泛型函数
    let ameT1 = <T>(x: T): T => {
    	console.log("x");
    	return x;
    };
    let ameT2 = <U>(y: U): U => {
    	console.log("y");
    	return y;
    };
    ameT1 = ameT2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    如果两个泛型函数的定义相同但没有指定类型参数,那么他们之间也是可以互相兼容的。

    • 结构之间的兼容:成员少的兼容成员多的
    • 函数之间的兼容:参数多的兼容参数少的
    类型保护

    TypeScript 能够在特定的区块中保证变量属于某种确定的类型,可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。

    enum Type {
    	Strong,
    	Week,
    }
    
    class Java {
    	helloJava() {
    		console.log("hello Java");
    	}
    	java: any;
    }
    
    class JavaScript {
    	helloJavaScript() {
    		console.log("hello JavaScript");
    	}
    	javascript: any;
    }
    
    function isJava(lang: Java | JavaScript): lang is Java {
    	return (lang as Java).helloJava !== undefined;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    类型断言
    function getLanguage(type: Type, x: string | number) {
    	let lang = type === Type.Strong ? new Java() : new JavaScript();
    	if ((lang as Java).helloJava) {
    		(lang as Java).helloJava();
    	} else {
    		(lang as JavaScript).helloJavaScript();
    	}
    	return lang;
    }
    getLanguage(Type.Strong);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    instanceof
    function getLanguage(type: Type, x: string | number) {
    	let lang = type === Type.Strong ? new Java() : new JavaScript();
    	if (lang instanceof Java) {
    		lang.helloJava();
    	} else {
    		lang.helloJavaScript();
    	}
    	return lang;
    }
    getLanguage(Type.Strong);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    in
    function getLanguage(type: Type, x: string | number) {
    	let lang = type === Type.Strong ? new Java() : new JavaScript();
    	if ("java" in lang) {
    		lang.helloJava();
    	} else {
    		lang.helloJavaScript();
    	}
    	return lang;
    }
    getLanguage(Type.Strong);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    typeof
    function getLanguage(type: Type, x: string | number) {
    	let lang = type === Type.Strong ? new Java() : new JavaScript();
    	if (typeof x === "string") {
    		x.length;
    	} else {
    		x.toFixed(2);
    	}
    	return lang;
    }
    getLanguage(Type.Strong);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    类型谓词
    function isJava(lang: Java | JavaScript): lang is Java {
    	return (lang as Java).helloJava !== undefined;
    }
    function getLanguage(type: Type, x: string | number) {
    	let lang = type === Type.Strong ? new Java() : new JavaScript();
    	if (isJava(lang)) {
    		lang.helloJava();
    	} else {
    		lang.helloJavaScript();
    	}
    	return lang;
    }
    getLanguage(Type.Strong);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    高级类型
    交叉类型

    所谓交叉类型就是将多个类型合并为一个类型,新的类型具有所有类型的特性,所以交叉类型特别适合对象混入的场景。

    interface DogInterface {
    	run(): void;
    }
    interface CatInterface {
    	jump(): void;
    }
    let pet: DogInterface & CatInterface = {
    	run() {},
    	jump() {},
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    需要注意的是交叉类型看名称给人的感觉是几个类型的交集,实际上是取所有类型的并集。

    联合类型

    所谓联合类型就是指声明的类型并不确定,可以为多个类型中的一个。

    let ameType: number | string = "1"; // 可以等于数字也可以等于字符串
    
    • 1
    字面量类型

    有的时候我们不仅需要限定一个变量的类型,而且要限定变量的取值在某一个特定的范围内。

    let b: "a" | "b" | "c";
    
    • 1
    对象联合类型

    如果一个对象是联合类型,那么在类型未确定的情况下,它就只能访问所有类型的共有成员。

    class DogImpl implements DogInterface {
    	run() {}
    	eat() {}
    }
    class CatImpl implements CatInterface {
    	jump() {}
    	eat() {}
    }
    enum Master {
    	Boy,
    	Girl,
    }
    function getPet(master: Master) {
    	let pet = master === Master.Boy ? new DogImpl() : new CatImpl();
    	pet.eat(); // 如果一个对象是联合类型,那么在类型未确定的情况下,它就只能访问所有类型的共有成员
    	pet.run(); // error
    	return pet;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这个时候有趣的事情发生了,从名称上看联合类型给人感觉是取所有类型的并集,而实际情况只能访问所有成员的交集。

    可区分的联合类型
    interface Square {
    	kind: "square";
    	size: number;
    }
    interface Rectangle {
    	kind: "rectangle";
    	width: number;
    	height: number;
    }
    type Shape = Square | Rectangle;
    function area(s: Shape) {
    	switch (s.kind) {
    		case "square":
    			return s.size * s.size;
    		case "rectangle":
    			return s.height * s.width;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面的代码如果不去升级是不会有问题的,但如果我们想加一种新的模式,它就会有问题了。

    interface Square {
    	kind: "square";
    	size: number;
    }
    interface Rectangle {
    	kind: "rectangle";
    	width: number;
    	height: number;
    }
    interface Circle {
    	kind: "circle";
    	r: number;
    }
    type Shape = Square | Rectangle | Circle;
    function area(s: Shape) {
    	switch (s.kind) {
    		case "square":
    			return s.size * s.size;
    		case "rectangle":
    			return s.height * s.width;
    		case "circle":
    			return Math.PI * s.r ** 2;
    		default:
    			return ((e: never) => {
    				throw new Error(e);
    			})(s);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    ((e: never) => {
    	throw new Error(e);
    })(s);
    
    • 1
    • 2
    • 3

    这段函数的作用是:检测 s 是不是 never 类型,如果 s 是 never 类型,就说明上面的分支都被覆盖了,这个分支永远不会执行,那么如果 s 不是 never 类型,就说明以前的分支有遗漏。

    索引类型

    我们有时候会遇到这样的一种场景,就是从对象中获取一些属性的值然后建立一个集合。

    let obj = {
    	a: 1,
    	b: 2,
    	c: 3,
    };
    function getValues(obj: any, keys: string[]) {
    	return keys.map((key) => obj[key]);
    }
    
    console.log(getValues(obj, ["a", "b"]));
    
    console.log(getValues(obj, ["e", "f"])); // 随意指定不存在的属性,但不报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    随意指定没有的属性,但 ts 编译器并没有报错,所以这个时候我们需要对类型进行约束,这个时候我们就需要用到了索引类型。

    下面我们要先了解一下索引类型的几个必要概念

    keyof T

    表示泛型变量可以通过继承某个类型获得某些属性

    改造上面的代码

    let obj = {
    	a: 1,
    	b: 2,
    	c: 3,
    };
    function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    	return keys.map((key) => obj[key]);
    }
    console.log(getValues(obj, ["a", "b"]));
    
    console.log(getValues(obj, ["e", "f"])); // error 这个时候就报错了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    映射类型

    通过映射类型可以把一个旧的类型生成一个新的类型

    将一个接口的所有属性映射为只读:

    interface objMapping {
    	a: string;
    	b: number;
    	c: boolean;
    }
    
    type ReadonlyObj = Readonly<objMapping>;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    ReadonlyObj 与 objMapping 成员完全相同,区别是 ReadonlyObj 中的成员属性均为只读

    将一个接口的所有属性变成可选的 Partial 映射类型

    type PartialObj = Partial<objMapping>;
    
    • 1

    可以抽取对象子集的 Pick 映射类型:

    type PickObj = Pick<objMapping, "a" | "b">;
    
    • 1
    Readonly 的实现原理

    从源码可以看出 Readonly 是一个可索引类型的泛型接口

    /**
     * Make all properties in T readonly
     */
    type Readonly<T> = {
    	readonly [P in keyof T]: T[P];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    索引签名为 P in keyof T :
    其中 keyof T 就是一个一个索引类型的查询操作符,表示类型 T 所有属性的联合类型

    P in :
    相当于执行了一个 for in 操作,会把变量 P 依次绑定到 T 的所有属性上

    索引签名的返回值就是一个索引访问操作符 : T[P] 这里代表属性 P 所指定的类型

    最后再加上 Readonly 就把所有的属性变成了只读,这就是 Readonly 的实现原理

    Partial 的实现原理:
    /**
     * Make all properties in T optional
     */
    type Partial<T> = {
    	[P in keyof T]?: T[P];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Pick 映射类型有两个参数:

    第一个参数 T,表示要抽取的目标对象

    第二个参数 K,具有一个约束:K 一定要来自 T 所有属性字面量的联合类型,

    即映射得到的新类型的属性一定要从 K 中选取

    以上三种映射类型官方称为同态类型,意思是只作用于 obj 属性而不会引入新的属性

    非同态类型

    Record 是非同态类型

    type RecordObj = Record<"m" | "n", objMapping>;
    
    • 1

    第一个参数是预定义的新属性,比如 m,n

    第二个参数就是已知类型

    映射出的新类型所具有的属性由 Record 的第一个属性指定,而这些属性类型为第二个参数指定的已知类型,这种类型就是一个非同态的类型

    Record 映射类型源码:

    /**
     * Construct a type with a set of properties K of type T
     */
    type Record<K extends keyof any, T> = {
    	[P in K]: T;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    非同态类型本质上会创建新的属性
    Readonly, Partial 和 Pick 是同态的,但 Record 不是。 因为 Record 并不需要输入类型来拷贝属性,所以它不属于同态,非同态类型本质上会创建新的属性
    映射类型本质上是一种预先定义的泛型接口,通常还会结合索引类型,获取对象的属性和属性值,从而像一个对象映射成我们想要的结构。

    条件类型

    条件类型是一种由条件表达式决定的类型

    T extends U ? X : Y

    意思是如果类型 T 可以赋值给类型 U,那么结果类型就是 X 类型,否则就是 Y 类型,条件类型使类型具有了不唯一性,同样增加了语言的灵活性

    分步式条件类型

    当类型 T 为联合类型时:

    T 为类型 A 和类型 B 的联合类型,结果类型会变成多个条件类型的联合类型

    
    (A | B) extends U ? X : Y
    
    • 1
    • 2

    可以将 A 和 B 进行拆解:

    
    (A extends U ? X : Y) | (B extends U ? X : Y)
    
    • 1
    • 2

    这时定义的变量就会被推断为联合类型

    type T3 = TypeName<string | string[]>;
    
    • 1

    可以看到,传入 string | string[]联合类型,被推断为 string|object 的联合类型

    利用上边这个特性可以实现对类型的过滤

    type Diff<T, U> = T extends U ? never : T;
    
    • 1

    如果 T 可以被赋值给 U,结果类型为 never 类型,否则为 T 类型

    type T4 = Diff<"a" | "b" | "c", "a" | "e">; //通过拆解来分析
    
    • 1

    T4 的类型被推断为 b 和 c 的联合类型,过滤掉了第二个参数中已经含有类型 a

    // Diff<'a', 'a' | 'e'> | Diff<'b', 'a' | 'e'> | Diff<'c', 'a' | 'e'>
    // never | "b" | "c"
    // "b" | "c"
    
    • 1
    • 2
    • 3

    先判断 a 是否可以被赋值给这个字面量联合类型’a’ | ‘e’,答案是可以的,所以返回 never
    继续,因为 b 不可以被赋值给字面量联合类型’a’ | ‘e’,所以返回 b
    继续,c 不可以被赋值给’a’ | ‘e’,所以返回 c
    最后,never 和 b,c 的联合类型为’b’ | ‘c’

    Diff 类型作用:

    可以从类型 T 中过滤掉可以被赋值给类型 U 的类型

    也可以实现从类型 T 中移除不需要的类型,如 undefined 和 null

    定义一个 NotNull,从 T 中过滤掉 undefined 和 null

    type NotNull<T> = Diff<T, undefined | null>;
    
    • 1
    type T5 = NotNull<string | number | undefined | null>;
    
    • 1

    过滤掉 undefined 和 null,T5 的类型就变成了 string 和 number

    上述的 Diff 和 NotNull 类型,是已经在 TS 内置的类库中被实现的内置类型

    • Diff 的内置类型叫做 Exclude

    • NotNull 的内置类型叫做 NonNullable
      此外,官方还预置了一些条件类型,如:Extract 和 Exclude

    • Extract 和 Exclude 相反

    • Exclude 作用是从类型 T 中过滤掉可以赋值给类型 U 的类型

    • Extract 作用是可以从类型 T 中抽取出可以赋值给 U 的类型

    type T6 = Extract<"a" | "b" | "c", "a" | "e">;
    type T7 = Exclude<"a" | "b" | "c", "a" | "e">;
    
    • 1
    • 2

    T6 抽取了在类型 U 中存在的类型 a

    T7 抽取了在类型 U 中不存在的类型 b 和 c

    源码:

    /**
     * Exclude from T those types that are assignable to U
     */
    type Exclude<T, U> = T extends U ? never : T;
    
    /**
     * Extract from T those types that are assignable to U
     */
    type Extract<T, U> = T extends U ? T : never;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    ReturnType 源码:
    /**
     * Obtain the return type of a function type
     */
    type ReturnType<T extends (...args: any) => any> = T extends (
    	...args: any
    ) => infer R
    	? R
    	: any;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    T extends (…args: any) => any:
    ReturnType 要求参数 T 可以赋值给一个函数,这个函数有任意的参数,返回值类型也是任意的
    由于函数返回值类型不确定,这里使用了 infer 关键字,表示待推断,延迟推断,需要根据实际的情况确定
    infer R ? R : any:
    如果实际类型是 R,那么结果类型就是 R,否则返回值类型就是 any

  • 相关阅读:
    JSTL 标签库
    计算机组装与维护实训室解决方案
    基于Springboot + Vue 母婴商城系统
    20230611 再次升级SSD
    python的安装教程
    Go 复合类型之切片类型介绍
    初识spring
    gitlab添加ssh公钥
    九种分布式ID解决方案
    使用Vue实现弹窗效果
  • 原文地址:https://blog.csdn.net/qq_42975676/article/details/133996036