15

1.new关键字在类型中的使用

泛型

在泛型里使用类类型

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

function create<T>(c: {new(): T; }): T {//这边的new()不好理解
    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!

查了不少资料,比较好的解释是what is new() in Typescript?意思就是create函数的参数是构造函数没有参数的T类的类型,同理,createInstance函数的参数是构造函数没有参数的A类的类型。
带着疑问写了测试代码:


vscode依然报错,仔细想下,createInstance函数return new c();这句话是类的实例化,所以传进来的参数c是个类,而不是类的实例,故要使用(c: new () => A)标明c是个类,而不是(c: Animal)类的实例,从下面的调用也可以看出传递的是类而不是实例。
我们知道js里面是没有类的,ES6里面的class也只是个语法糖,编译后依然为一个function。所以去修饰一个class也就是修饰一个function,但是修饰的是构造函数,所以这边加以区别,前面有个new。


接口

类静态部分与实例部分的区别

这边同样用到了关键字new()
第一个例子报错了,
官方解释:
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick:()=>void;
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {//这个和泛型中使用类类型相同,
    return new ctor(hour, minute);//需要类型为ClockInterface的两个参数的构造器类,只是二者写法有点区别
}

class DigitalClock implements ClockInterface {//这边实现的接口不能是直接的构造器
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

再结合react官方接口的书写,

interface Component<P = {}, S = {}> extends ComponentLifecycle<P, S> { }
    class Component<P, S> {//这里全部是实例方法和属性
        constructor(props?: P, context?: any);

        // Disabling unified-signatures to have separate overloads. It's easier to understand this way.
        // tslint:disable:unified-signatures
        setState<K extends keyof S>(f: (prevState: S, props: P) => Pick<S, K>, callback?: () => any): void;
        setState<K extends keyof S>(state: Pick<S, K>, callback?: () => any): void;
        // tslint:enable:unified-signatures

        forceUpdate(callBack?: () => any): void;
        render(): JSX.Element | null | false;

        // React.Props<T> is now deprecated, which means that the `children`
        // property is not available on `P` by default, even though you can
        // always pass children as variadic arguments to `createElement`.
        // In the future, if we can define its call signature conditionally
        // on the existence of `children` in `P`, then we should remove this.
        props: Readonly<{ children?: ReactNode }> & Readonly<P>;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }

 interface ComponentClass<P = {}> {
        new (props?: P, context?: any): Component<P, ComponentState>;//此处对Component做了修饰,规定react的构造函数类型
        propTypes?: ValidationMap<P>;//下面几个全部是静态方法和属性
        contextTypes?: ValidationMap<any>;
        childContextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }

为什么写了interface Component又写了class Component,interface Component没有写里面具体的实例方法和属性,而写了同名的class Component,是不是意味着class Component就是interface的具体实现,这里是index.d.ts文件,应该在.ts文件里面不能这么写,同事被相同的问题纠结住,

 interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
    class Component<P, S> {

        static contextType?: Context<any>;

        // TODO (TypeScript 3.0): unknown
        context: any;

        constructor(props: Readonly<P>);
        /**
         * @deprecated
         * @see https://reactjs.org/docs/legacy-context.html
         */
        constructor(props: P, context?: any);

        // We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
        // See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
        // Also, the ` | S` allows intellisense to not be dumbisense
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;

        forceUpdate(callback?: () => void): void;
        render(): ReactNode;

      
        readonly props: Readonly<P> & Readonly<{ children?: ReactNode }>;
        state: Readonly<S>;
        /**
         * @deprecated
         * https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs
         */
        refs: {
            [key: string]: ReactInstance
        };
    }

再次看React index.d.ts 会发现interface Component没有声明任何内容,生命周期方法也是继承过来的,包括setState,forceUpdate,render的声明都是在class Component中的,通过组件中的this.setState也是定位到class Component中,我们知道interface Component是用来修饰组件的实例的,但是没法描述组件的静态属性,比如contextType,这时候也许只能借助class来描述static成员,通过上述分析可知interface是可以用来修饰类的,再结合interface ComponentClass

    interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
        new (props: P, context?: any): Component<P, S>;
        propTypes?: WeakValidationMap<P>;
        contextType?: Context<any>;
        contextTypes?: ValidationMap<any>;
        childContextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }

ComponentClass是用来描述class Component的,这个时候限制Component的static成员,接着上面的例子做个测试:

// 用来描述类类型
interface ClockConstructor {
  new (hour: number, minute: number);
  contextType: any; // 新增个成员变量
}

interface ClockInterface {
  tick: () => void;
}

class DigitalClock implements ClockInterface {
  //这边实现的接口不能是直接的构造器
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  //这个和泛型中使用类类型相同,
  return new ctor(hour, minute); //需要类型为ClockInterface的两个参数的构造器类,只是二者写法有点区别
}

用类类型描述class的静态属性
vscode给报错了,说是DigitalClock缺少contextType,其实就是缺少静态属性 contextType,

class DigitalClock implements ClockInterface {
  static contextType;
  //这边实现的接口不能是直接的构造器
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}
let digital = createClock(DigitalClock, 12, 17);

给DigitalClock 加上static contextType就不会报错了,ClockInterface是用来描述类的实例的,ClockConstructor是用来描述类的,用ClockConstructor描述类有哪些静态属性没问题,但是怎么去描述实例的静态属性呢?仿造React的方式在.ts文件中定义同名interface跟class

interface ClockInterface {}

class ClockInterface {
  tick(): void;
}

clipboard.png

说函数没有具体实现,将代码贴到d.ts文件,就不会报错,

interface ClockInterface {
  name: string;
}

class ClockInterface {
  tick(): void;
  static jump(): void;
}

使用该接口,

class DigitalClock implements ClockInterface {
  static jump: () => {};
  name: string = "";
  tick() {
    console.log("beep beep");
  }
}

DigitalClock.jump();

在vscode中输入DigitalClock. 或者static j都有相应的提示,所以可以在d.ts中用同名interface和class描述类的静态属性

到此new()关键字在类型中的使用基本搞清楚了。

类装饰器
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

2. 装饰器的使用

装饰器(Decorator)在React中的应用
JavaScript 中的装饰器是什么?

3. 函数类型声明的不同方式

1. 最常见的方式

函数声明(Function Declaration)类型的,就是普通的具名函数

  function add(x: number, y: number): number {
    return x + y
  }
2. 函数表达式(Function Expression)类型声明
  • 1.这种就是后面赋值号后面有个匿名或者具名函数,比较好理解的写法,都在函数体内写类型

    handle = (
      baseValue: number,
      increment: number,
    ): number => {
      return baseValue
    }
  • 2.给变量定义类型,同时也在函数内部定义类型

    handle: (baseValue: number, increment: number) => number = (
      baseValue: number,
      increment: number,
    ): number => {
      return baseValue
    }
  • 3.将变量的类型抽取到接口中

    interface IHandle {
    (baseValue: number, increment: number): number
    }
    
     handle: IHandle = (baseValue: number, increment: number): number => {
      return baseValue
    }

    既然前面的变量声明了接口,那么后面的函数里面的类型就可以去掉了

    interface IHandle {
    (baseValue: number, increment: number): number
    }
    
     handle: IHandle = (baseValue,increment)=> {
      return baseValue
    }

    但是发现个问题

    interface IHandle {
    (baseValue: number, increment: number): number
    }
    
     handle: IHandle = (baseValue)=> {
      return baseValue
    }

    这么写居然vscode没有报错,increment明明不是可选参数,不是很理解,有待讨论

查阅了typescript的官方文档的函数章节

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

这么写的接口,和函数类型的接口声明惊人的相似,

interface IHandle {
  (baseValue: number, increment: number): number
}

差别在哪呢.函数类型的接口声明是匿名的,而上面对象的类型声明的函数是具名的,做了一下测试

var src:UIElement = function() {}
//报错:
// Type '() => void' is not assignable to type 'UIElement'.
// Property 'addClickListener' is missing in type '() => void'.
// var src: UIElement

interface UIElement {
  addClickListener(name: string): void
}

var src: UIElement = {//不报错
  addClickListener() {},
}

var src: UIElement = { //报错
  addClickListener(name: string, age: number) {},
}
// Type '{ addClickListener(name: string, age: number): void; }' is not assignable to type 'UIElement'.
// Types of property 'addClickListener' are incompatible.
//   Type '(name: string, age: number) => void' is not assignable to type '(name: string) => void'.

看样就是实际的函数的参数可以比接口少定义,但是不能多定义
函数接口的是用来修饰变量的,当然包括函数的形参的修饰,以及返回值的修饰,但是不能修饰具名函数

interface IHandle {
  props?: object
  (baseValue: number, increment: number): number
}

function source:IHandle(baseValue) {//这么修饰具名函数会报错
  return baseValue
}

function source(baseValue): IHandle {//这么写是可以的,表明返回了一个IHandle 的函数
  return baseValue
}

3. 怎样修饰类(类类型)

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    greet() {
        if (this.greeting) {
            return "Hello, " + this.greeting;
        }
        else {
            return Greeter.standardGreeting;
        }
    }
}

let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());

let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
  1. 我们一般都是修饰一个类的实例的,怎么简单的修饰类,typeof是个很好的办法,
    *这个例子里,greeter1与之前看到的一样。 我们实例化 Greeter类,并使用这个对象。 与我们之前看到的一样。
    再之后,我们直接使用类。 我们创建了一个叫做 greeterMaker的变量。 这个变量保存了这个类或者说保存了类构造函数。 然后我们使用 typeof Greeter,意思是取Greeter类的类型,而不是实例的类型。 或者更确切的说,"告诉我 Greeter标识符的类型",也就是构造函数的类型。 这个类型包含了类的所有静态成员和构造函数。 之后,就和前面一样,我们在 greeterMaker上使用new,创建Greeter的实例。*
    也就是使用typeof ClassName

    interface IPerson {
      age: number;
    }
    class Person {
      age: 99;
    }
    
    let p: typeof IPerson = Person;//这么写会报错的

  2. 使用构造函数接口修饰类
interface IPerson {
  age: number;
}

interface IPersonConstructor {
  new (): IPerson;
}

class Person {
  age: 99;
}

let p: IPersonConstructor = Person;

之前对new (): IPerson;这句话后面的返回值不是很理解,直到看到了将基类构造函数的返回值作为'this',也就是说new Person()的时候执行的是构造函数,那么构造函数就返回了Person的实例,自然new (): IPerson;构造函数返回IPerson就很好理解了

4. keyof

比如有个

interface a{
     a1: 'a1';
     a2: 'a2';
     .....
     .....
     a100: 'a100';
}

然后又个类型要继承这个interface的某一个value的值
比如 type anum = 'a1' | 'a2' | 'a3' | ....| 'a100',
应该怎么写?

type anum = typeof a.a1 | typeof a.a2 | typeof a.a3 | ....| typeof a.a100;

有没有简单的写法 那个interface 有可能随时在变

a1到a100全要?

对全要

而且有可能 到a100以上 一直在添加

我想在添加的时候只添加interface type不用在修改了 有没有办法

key还是value

value

keyof

keyof.png

5. 函数名后面的感叹号(非空断言操作符)

    onOk = () => {
      this.props.onOk!(this.picker && this.picker.getValue());
      this.fireVisibleChange(false);
    }

react-component/m-picker
群里问了是非空操作符

再去搜索,在typescript 2.0的文档里找到了,叫 非空断言操作符

// 使用--strictNullChecks参数进行编译
function validateEntity(e?: Entity) {
    // 如果e是null或者无效的实体,就会抛出异常
}

function processEntity(e?: Entity) {
    validateEntity(e);
    let s = e!.name;  // 断言e是非空并访问name属性
}

vscode检测

需要在tsconfig.json 里面加上"strictNullChecks": true,这样vscode会自动检测null和undefined

6. ts差集运算

Add support for literal type subtraction

具体应用在高阶组件里面的props
TypeScript在React高阶组件中的使用技巧

7. 方法重写

重写

方法重写子类的参数需要跟父类一致,否则会报错

8. 剩余参数

const renderWidget = ({ field, widget, ...restParams }) => {
  const min = restParams.min;
};

这段代码的剩余参数restParams 怎么表示
感觉应该这么写

const renderWidget = (
  {
    field,
    widget,
    ...restParams
  }: { field: string; widget: string;restParams: [key: string]: any },
  idx: number,
  primaryField: string
) => {
  const min = restParams.min;
};

但是还是报错,其实应该省去restParams

const renderWidget = ({
  field,
  widget,
  ...restParams
}: {
  field: string;
  widget: string;
  [key: string]: any;
}) => {
  const min = restParams.min;
};

这样就好了Destructuring a function parameter object and …rest

9. 元组推断

群里看到的疑问,为什么最后推断出是string|number

type TTuple = [string, number];

type Res = TTuple[number]; // string|number

一开始不理解,自己改下写法

type TTuple = [string, number];

type Res = TTuple[string]; 

错误写法
再尝试写下去

type TTuple = [string, number];

type Res = TTuple[0];//string
type Res1 = TTuple[1];//number
type Res2 = TTuple[2];//报错

2报错
TTuple[2]报错

首先js中没有元组概念,数组中可以存放任何数据类型,ts中把存放不同数据类型的数组叫做元组,这样就很好理解TTuple[number]中的number就是索引index,索引只能是数字,而且不能越界,所以两次报错就好理解了,TTuple[number]为什么返回string|number是因为没有指定具体的索引,只能推断出两种可能,string或number

联想到获取接口的属性的类型

interface IProps {
  name: string;
  age: number;
}

type IAge = IProps["name"];// string

const per: IAge = "geek";

10. type interface 泛型差异

下面实现类似于Record的代码

interface Collection<T extends string, U> {
  [P in T]: U;
}

type Collection33<K extends string, T> = {
  [P in K]: T;
};

ESD(W@7WYG6U)90@$Y`JVY6.png

0]Z%[$KN8AQB41693SDT~5F.png

type完全没问题,interface 的两种写法都会报错,好奇葩

11. 函数的this参数

function toHex() {
  return this.toString(16);
}

function声明的函数的this是在调用的时候决定的,为了限制调用者的类型,ts中的函数可以通过this参数限制调用者的类型

function toHex(this: number) {
  return this.toString(16);
}

const x = toHex.call("xxx");

image.png

此时vscode会给出报错提示,单纯的建个ts文件,是不会报错的,需要在项目根目录配置tsconfig.json,

{
  "compilerOptions": {
    "strict": true,//设置为严格模式
    "target": "ES5",
    "noImplicitThis": true//这个设置无效
  }
}

直接在js中显式的指定this会报错

function show(this) {
  console.info("this:", this);
  console.info("arguments:", arguments);
}

const state = {
  person: {
    age: 10,
  },
};

const next = show.bind(state);

next("dd", "ccc");

输出:
image.png
在ts中限制this类型
image.png
这样不会报错,而且this并不会影响arguments参数,为什么?因为编译后的函数中是没有this作为参数的。咋一看觉得this会影响arguments,要看到this后面的类型,说明是在ts中,就有了清醒的认识。

再看个例子

let bar = {
  x: 'hello',
  f(this: { message: string }) {
    this; // { message: string }
  }
};

bar.f()

image.png
函数的this声明了类型即使是外面的定义的对象调用,类型不匹配也会报错。

12. infer关键字

infer翻译过来是推断,infer关键字可以理解为泛型变量的声明关键字,ts内置的一些使用到infer的类型

  • 获取函数参数类型

    /**
     * Obtain the parameters of a function type in a tuple
     */
    type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
    

    例子

    type Func = (animal: Animal) => void;
    type Param = Parameters<Func>;

    image.png

T extends (...args: infer P) => any 是个整体,判断T是不是个函数,然后根据条件判断得出结果

  • 获取函数返回值类型

    /**
     * Obtain the return type of a function type
     */
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    

    例子

    type NextFunc = (animal: Animal) => number;
    type CC = ReturnType<NextFunc>;

image.png

这里需要注意的是泛型中传递的是类型,不是具体的函数

function show(animal: Animal) {
  return 99;
}
type NextFunc = (animal: Animal) => number;
type CC = ReturnType<show>;

image.png
这样就会报错,用typeof show 可以推断函数的类型

function show(animal: Animal) {
  return 99;
}
type NextFunc = (animal: Animal) => number;
type CC = ReturnType<typeof show>;

这样就不会报错了

通常我们声明泛型的形式有下面几种

  • interface
interface Animal<T> {
  age: T;
}
  • 函数
function show<T extends string>(name: T): string {
  return "xx";
}

但看下ReturnType的定义

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

T是定义在尖括号内,但是R并没有定义在尖括号内,如果把infer去掉会怎样?

image.png

报错:找不到R,T是调用ReturnType传递进来的,而R并不是调用传递的参数,这个时候ts的编译系统并不认识R,所以需要一个关键字来定义这种特殊的泛型,从某种意义上来讲调用不需要传递的泛型是需要用infer进行定义的,infer可以理解为在尖括号外声明泛型的关键字。
再看个例子

type Head<T extends any[]> = T extends [infer F, ...infer P] ? F : never;
type TupleOne = [string, number, boolean];
type TupleHead = Head<TupleOne>; // type TupleHead = string

结合上述的例子,infer的作用是通过条件语句提取一个类型的部分类型。

TypeScript 类型编程: 从基础到编译器实战
TypeScript infer 关键字


assassin_cike
1.3k 声望74 粉丝

生活不是得过且过


下一篇 »
git 常用命令