TypeScript高级应用

前言

本文记录TypeScript一些高级用法,首先要有一定的TypeScript基本知识和了解基础的ES6语法,如果还没学习过TypeScript,推荐2个网站学习后再来看这篇文章。

蚂蚁部落的TypeScript教程要精练一些,可以作为快速上手的教程。官方文档比较详细,可能一开始看会有些难懂。

ts数据类型

  1. 布尔类型(boolean)
  2. 数字类型(number)
  3. 字符串类型(string)
  4. 数组类型(array)
  5. 元祖类型(tuple)
  6. 枚举类型(enum)
  7. 任意类型(any)
  8. null 和 undefined
  9. void类型
  10. never类型

null 和 undefined类型

默认情况下null和undefined是所有类型的子类型,也就是可以赋值给其他类型。

undefined出现原因:

  1. 变量被声明了但是没赋值时
  2. 调用函数时,应该提供的参数没提供,则该参数为undefined
  3. 函数没有返回值时,默认返回undefined
  4. 对象没有赋值的属性

null出现原因:

  1. 正则捕获的时候,如果没有捕获到结果,默认也是Null
  2. 作为对象原型链的终点(Object.prototype.proto)
  3. 释放内存

以上只是简单列举了一些情况。

类型断言

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

在TypeScript里使用JSX时,只有 as语法断言是被允许的。因为使用尖括号<>会被JSX解析成组件。

可索引类型接口

interface IArray {
  [index: number]: string;
}
 
let arr: IArray= ["ts教程", "广东省"];
let ant: string = arr[0];

上面规定索引签名是number类型,返回值是字符串类型。

支持两种索引签名:字符串和数字。可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 数字索引类型“Animal”不能赋给字符串索引类型“Dog”
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

我们把接口左侧名字称为key,右侧的类型称为value。

可以这么理解,使用了字符串索引类型[x: string]的key,那么它对应的类型必须是其他key的父类类型。

正确使用:

interface NotOkay {
    [x: number]: Dog;
    [x: string]: Animal;
}

例子2:

interface Dictionary {
  [index: string]: string;
  length: number;    // 错误,length是number类型
  name: string       // 可以,`name`的类型与索引类型返回值的类型匹配
}

先看索引类型[index: string]: string;返回的是string类型,根据上面说,字符串索引类型其他key返回的类型的父类。

  • length返回的类型是number,并不是string的子类,所以错误。
  • name返回的类型是string,跟索引类型返回的类型一致,所以没问题。

还可以这么理解,[index: string] 这种表述形式,算是一个统称,其实是包括了length和name这两个key,只是单独把这两个key列举出来,说明这个接口里面有这两个成员。那length和name这两个key的类型就应该跟[index: string]返回的类型至少是一致的,为string类型。

泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

和普通函数的区别是,在函数签名声明的前面加上泛型类型参数。

接口中应用泛型

在接口中的成员使用泛型:

interface GenericIdentityFn {
  <T>(arg: T): T;
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;

上面接口的应用和普通函数接口区别很小,只是在函数前面添加了泛型类型变量。

在接口上增加泛型:

interface GenericIdentityFn<T> {
  (arg: T): T;
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

上面的代码将泛型参数当做接口的一个参数传递,这样接口的应用就更加灵活。

泛型类

class Antzone<T> {
  webName: T;
}
let antzone = new Antzone<string>();
antzone.webName = "蚂蚁部落";

在类名称后面传递泛型类型参数。
特别说明: 泛型类指的是实例部分的类型,类的静态属性不能使用这个泛型类型。

泛型约束

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
  
let x = { a: 1, b: 2, c: 3, d: 4 };
  
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

这里的x变量会被推导成object类型,

如果我将x类型设置为any,会有什么区别吗?

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
let x: any = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); 

这个时候编译器不会报错了,因为T为any类型,K extends keyof T相当于 K extends keyof any,也是any类型,所以getProperty(x, “m”)不会报语法错误,但得到的值为undefined。

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型。

function create<T>(c: {new(): T; }): T {
    return new c();
}

例子:

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

createInstance中的参数c类型是一个(new () => A),这个参数可以被new 一个实例,说明这个参数c并不是一个普通的类型,只能是构造函数。
在js里面是没有类的,ES6里面的class也只是个语法糖,编译后依然为一个function,也就是构造函数。(new () => A)返回时一个A的实例,所以传入的是一个class,也就是一个构造函数,为了区别普通的function,所以前面加了new关键字。

高级类型

类型别名

类型别名用来给一个类型起个新名字。

type ant = string;
let str:ant=“类型”;

类型别名和接口的区别:
通过上面的介绍,类型别名与接口有一些类似之处,但是区别也是很明显的:

  1. 错误信息不会使用别名。
  2. 接口是创建一个新的类型,别名不会创建一个新类型,是对原有类型的引用。
  3. 即使使用别名,编辑器只能提示还是会显示原有类型名称。
  4. 类型别名不能被 extends和 implements

字面量类型

字面量类型包括:字符串字面量类型和数字字面量类型,用的比较多的是字符串字面量类型。
字符串字面量类型用来约束取值只能是某几个字符串中的一个。

type sex="男" | “女";

交叉类型

可以将现有的多种类型叠加到一起成为一种类型。

interface T1 {
  a: boolean;
  b: string;
}
  
interface T2 {
  a: boolean;
  b: number;
}
  
type T = T1 & T2;
  
type Ta = T['a']; // boolean
type Tb = T['b']; // string & number

上面的代码中,T1和T2接口中,b属性类型冲突,那么最终b属性类型为交叉类型string & number,同时string类型或者number类型无法赋值给交叉类型string & number,最终导致报错。为了解决这个问题,可以将resut再断言为Any类型,这样result所有属性均为Any类型,那么赋值就不会报错了。

联合类型

联合类型表示一个值可以是几种数据类型中的某一种。类型之间使用竖线"|"分隔,代码如下:

let val:number | string;

如果一个值是联合类型,那么只能访问联合类型的共有成员。

class Bird{
  leg=2;
  color="white";
  fly(){
    // code
  }
}
class Fish {
  leg=8;
  color="black";
  swim(){
    // code
  }
}
function union(): Bird | Fish {
    return new Bird();
}
let animal = union();
animal.color;
animal.leg;
animal.eat()// 报错,只能访问共有成员

如果一个值的类型是 A | B,我们能够 确定的是它包含了 A 和 B中共有的成员。 这个例子里, Bird具有一个 fly成员。 我们不能确定一个 Bird | Fish类型的变量是否有 fly方法。 如果变量在运行时是 Fish类型,那么调用 animal.eat()就出错了。

keyof

keyof是索引类型查询操作符。
假设T是一个类型,那么keyof T产生的类型是T的属性名称字符串字面量类型构成的联合类型。
特别说明:T是数据类型,并非数据本身。

interface Person {
    name: string;
    age: number;
}
let personProps: keyof Person; // 'name' | 'age'

typeof

typeof 代表取某个值的 type

const a: number = 3
const b: typeof a = 4
// 相当于: const b: number = 4

类型保护

四种类型:自定义的类型保护、typeof类型保护、instanceof类型保护、switch case类型保护。

自定义的类型保护

export interface AccountPersonal {
  username: string;
  nickname: string;
  avatar: string;
}

// 'username' in obj表示 username是obj中的属性
export function isAccountPersonal(obj: any): obj is AccountPersonal {
  return 'username' in obj;
}

在这个例子里, obj is AccountPersonal就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

const personal = {
  username: 'zzb';
  nickname: 'binge';
  avatar: 'http://baidu.com';
}
if (isAccountPersonal(personal)) {
    // 通过判断则可以知道personal是属于AccountPersonal类型,这样就可以使用代码提示出类型里面的成员。
    console.log(personal.username) // 输出'zzb'
}
parameterName is Type

如果函数返回值为true,那么也就意味着类型谓词是成立的,于是它后面作用域的类型也就被固定为Type。

typeof类型保护

上面介绍过typeof的用法,这里可以利用它的特性来做类型保护。

function ant(param: string | number) {
  if (typeof param === "number") {
    console.log(param+5); // 
  }
  if (typeof param === "string") {
    console.log(param+"蚂蚁部落");
  }
}

由于应用了typeof,后面作用域的param就确定为number类型。

instanceof类型保护

interface Animal {
  numLegs: number;
}

class Chicken implements Animal {
  numLegs: number = 2;
  eat = () => {

  }
}

class Lion implements Animal {
  numLegs: number = 4;
  run = () => {
    
  }
}

function getAnimal(numLegs: number = 2) {
  return numLegs <= 2 ?
    new Chicken() :
    new Lion();
}

// 类型为chicken | Lion
let padder: Animal = getAnimal(2);

if (padder instanceof Chicken) {
  padder.eat(); // 类型细化为'Chicken'
}
if (padder instanceof Lion) {
  padder.run(); // 类型细化为'Lion'
}

定义:instanceof运算符用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。
简单理解,instanceof可以判断左侧对象是否是右侧类型的子类。

switch case类型保护

interface Square {
  kind: "square";
  size: number;
}
interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}
type Shape = Square | Rectangle | Circle;
  
function area(shape: Shape) {
  switch (shape.kind) {
    case "square": return shape.size * shape.size;
    case "rectangle": return shape.height * shape.width;
    case "circle": return Math.PI * shape.radius ** 2;
  }
}

Partial

// 可选类型

// 实现源码
type Partial<T> = {
  [P in keyof T]?: T[P];
};

interface User {
  id: number;
  age: number;
  name: string;
};

type PartialUser = Partial<User>
// 相当于: type PartialUser = { id?: number; age?: number; name?: string; }

Pick

// 挑选属性

// 实现源码
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};
interface User {
  id: number;
  age: number;
  name: string;
};
type PickUser = Pick<User, "id" | “age">
// 相当于: type PickUser = { id: number; age: number; }

Readonly

将一个类型转换成只读类型

// 实现源码
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

interface User {
  id: number;
  age: number;
  name: string;
};

type ReadonlyUser = Readonly<User>
ReadonlyUser = {
  readonly id: number;
  readonly age: number;
  readonly name: string;
}

装饰器(decorator)

装饰器只能作用在类,属性,
访问符,方法或方法参数
上,这篇文章只会简单讲解类装饰器和React的高阶组件关系,装饰器详细的内容会在另一篇文章记录。

装饰器是一种特殊类型的声明,它能够被附加到类声明,属性, 访问符,方法或方法参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
简单来说,装饰器就是用一个代码包装另一个代码的简单方式,就是简单的将一个函数包装成为另一个函数。

为什么要用装饰器

我们会对传入参数的类型判断、对返回值的排序、过滤,对函数添加节流、防抖或其他的功能性代码,基于多个类的继承,各种各样的与函数逻辑本身无关的、重复性的代码。
所以,对于装饰器,可以简单地理解为是非侵入式的行为修改。

如何定义装饰器

装饰器本身其实就是一个函数,理论上忽略参数的话,任何函数都可以当做装饰器使用。

function helloWord(target: any) { 
	console.log('hello Word!'); 
} 
@helloWord 
class HelloWordClass { }

后面会讲解。

装饰器执行时机

修饰器对类的行为的改变,是代码编译时发生的(不是TypeScript编译,而是js在执行机中编译阶段),而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

类装饰器

当装饰函数直接修饰类的时候,装饰函数接受唯一的参数,这个参数就是该被修饰类本身。

function helloWord(target: any) { 
	console.log('hello Word!'); 
} 
@helloWord 
class HelloWordClass { }

普通装饰器(无法传参)

function testable(target) { 
	target.isTestable = true; 
}

@testable 
class MyTestableClass {} 

console.log(MyTestableClass.isTestable) // true

装饰器工厂(可传参)

function testable(isTestable) { 
	return function(target) { 
		target.isTestable = isTestable; 
	} 
}

@testable(true) 
class MyTestableClass {} 

MyTestableClass.isTestable // true

重载构造函数

function foo(target) {   
	return class extends target {      
		name = ‘zzb’; 
		sayHello(){
			console.log("Hello"+this.name)     
		}   
	}
}

@foo
class P{   
	constructor(){  }
}
const p = new P();
p.sayHello(); // 会输出Hello zzb

React高阶组件

再看一下React高阶组件的定义
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

通过对上面装饰器的了解,高阶组件可以看做是装饰器模式(Decorator Pattern)在React的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构。

React 中实现高阶组件的方法:属性代理(Props Proxy)和 反向继承(Inheritance Inversion)。
其中属性代理的方式就是利用装饰器模式实现。

高阶组件应用装饰器实例

选用了react的初始化项目做例子,稍微修改了一下。
定义装饰器:

export function withName(name: string): any {
    return (InnerComponent: any) => {
        return function NameWrapper(props: any) {
            return (
                <div>
                    <InnerComponent
                        name={name}
                        {...props}
                    />
                </div>
            );
        };
    };
}

给App组件进行包装

@withName('zzb') // 关键代码
class App extends React.PureComponent<any, any>{

  render() {
    const {name} = this.props;
    return (
        <div className="App">
          <header className="App-header">
            <p style={{color: 'yellow'}}>通过装饰器传递进来的名字: {name}</p>
          </header>
        </div>
    );
  }
}

结果:
在这里插入图片描述

如果App是一个函数式组件

装饰器不能直接放在函数上方,前面说过会造成函数提升,装饰器只能作用在类,属性,访问符,方法或方法参数。想要对函数式组件进行包装,就需要以下方式

export default withName('zzb')(App); // 关键代码

完整代码:

const App: React.FC = ({name, ...props}: any) => {
  return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p style={{color: 'yellow'}}>通过装饰器传递进来的名字: {name}</p>
        </header>
      </div>
  );
}


export default withName('zzb')(App); // 关键代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值