可索引类型

Typescript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。

这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致。

  1. class Animal {
  2. name: string;
  3. }
  4. class Dog extends Animal {
  5. breed: string;
  6. }
  7. // 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
  8. interface NotOkay {
  9. [x: number]: Animal;
  10. [x: string]: Dog;
  11. }

下面的例子里,name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

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

interface 和 type 区别

interfacetype 两个关键字的含义和功能都非常的接近。这里我们罗列下这两个主要的区别:
interface

  • 同名的 interface 自动聚合,也可以跟同名的 class 自动聚合
  • 只能表示 objectclassfunction 类型
interface Point{
  x:number;
  y:number;
}

interface SetPoint{
  (x:number,y:number):void;
}

type:

  • 不仅仅能够表示 objectclassfunction
  • 不能重名(自然不存在同名聚合了),扩展已有的 type 需要创建新 type
  • 支持复杂的类型操作
type Point = {
  x:number;
  y:number;
}

type SetPoint = (x:number,y:number) => void;

type Name = string;

type PartialPointX = {x:number;};
type PartialPointY = {y:number;};

type PartialPoint = PartialPointX | PartialPointY;

type Data = [number,string,boolean];

extend

都可以被继承,但是语法上会有些不同。另外需要注意的是,「interface 和 type 彼此并不互斥」

interface extends interface

interface PartialPointX {x:number;};
interface Point extends PartialPointX {y:number;};

type extends type

type PartialPointX = {x:number;};
type Point = PartialPointX & {y:number;};

interface extends type

type PartialPointX = {x:number;};
interface Point extends PartialPointX {y:number;};

type extends interface

interface ParticalPointX = {x:number;};
type Point = ParticalPointX & {y:number};

implements

一个类,可以以完全相同的形式去实现interface 或者 type。但是,类和接口都被视为静态蓝图(static blueprints),因此,他们不能实现/继承 联合类型的 type

interface Point {
  x: number;
  y: number;
}
class SomePoint implements Point {
  x: 1;
  y: 2;
}
type Point2 = {
  x: number;
  y: number;
};
class SomePoint2 implements Point2 {
  x: 1;
  y: 2;
}
type PartialPoint = { x: number; } | { y: number; };
// FIXME: can not implement a union type
class SomePartialPoint implements PartialPoint {
  x: 1;
  y: 2;
}

typescript语法 - 图1

only interface can

在实际开发中,有的时候也会遇到 interface 能够表达,但是 type 做不到的情况:「给函数挂载属性」

interface FuncWithAttachment {
  (param: string): boolean;
  someProperty: number;
}
const testFunc: FuncWithAttachment = function(param: string) {
  return param.indexOf("Neal") > -1;
};
const result = testFunc("Nealyang"); // 有类型提醒
testFunc.someProperty = 4;

typescript语法 - 图2

& 和 | 操作符

这里我们需要区分,|& 并非位运算符。我们可以理解为&表示必须同时满足所有的契约。|表示可以只满足
一个契约。

interface IA{
  a:string;
  b:string;
}
type TB{
  b:number;
  c:number [];
}
type TC = TA | TB;// TC 的 key,包含 ab 或者 bc 即可,当然,包含 bac 也可以
type TD = TA & TB;// TD 的 可以,必须包含 abc

交叉类型

交叉类型,我们可以理解为合并。其实就是「将多个类型合并为一个类型」

Man & WoMan
  • 同时是 Man 和 Woman
  • 同时拥有 Man 和 Woman 这两种类型的成员
    interface ObjectConstructor{
    assign<T,U>(target:T,source:U):T & U;
    }
    
    以上是 ts 的源码实现,下面我们再看一个我们日常使用中的例子
    interface A{
    name:string;
    age:number;
    sayName:(name:string)=>void
    }
    interface B{
    name:string;
    gender:string;
    sayGender:(gender:string)=>void
    }
    let a:A&B;
    // 这是合法的
    a.age
    a.sayGender
    
T & never = never

extends

extends 即为扩展、继承。在 ts 中,「extends 关键字既可以来扩展已有的类型,也可以对类型进行条件限定」。在扩展已有类型时,不可以进行类型冲突的覆盖操作。例如,基类型中键astring,在扩展出的类型中无法将其改为number

type num = {
  num:number;
}
interface IStrNum extends num {
  str:string;
}
// 与上面等价
type TStrNum = A & {
  str:string;
}

在 ts 中,我们还可以通过条件类型进行一些三目操作:T extends U ? X : Y

type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false;
type NumberEqualsToString = IsEqualType<number,string>; // false
type NumberEqualsToNumber = IsEqualType<number,number>; // true

keyof

「keyof 是索引类型操作符」。用于获取一个“常量”的类型,这里的“常量”是指任何可以在编译期确定的东西,例如constfunctionclass等。它是从 「实际运行代码」 通向 「类型系统」 的单行道。理论上,任何运行时的符号名想要为类型系统所用,都要加上 typeof
在使用class时,class名表示实例类型,typeof class表示 class本身类型。是的,这个关键字和 js 的 typeof 关键字重名了 。
假设 T 是一个类型,那么keyof T产生的类型就是 T 的属性名称字符串字面量类型构成的联合类型(联合类型比较简单,和交叉类型对立相似,这里就不做介绍了)。
「注意!上述的 T 是数据类型,并非数据本身」

interface IQZQD{
    cnName:string;
    age:number;
    author:string;
}
type ant = keyof IQZQD;

vscode 上,我们可以看到 ts 推断出来的 ant
typescript语法 - 图3
注意,如果 T 是带有字符串索引的类型,那么keyof Tstring或者number类型。
索引签名参数类型必须为 “string” 或 “number”

interface Map<T> {
  [key: string]: T;
}
//T[U]是索引访问操作符;U是一个属性名称。
let keys: keyof Map<number>; //string | number
let value: Map<number>['antzone'];//number

泛型

泛型可能是对于前端同学来说理解起来有点困难的知识点了。通常我们说,泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,而后再通过实际运行或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。说白了,「泛型就是不预先确定的数据类型,具体的类型在使用的时候再确定的一种类型约束规范」
泛型可以应用于 functioninterfacetype 或者 class 中。但是注意,「泛型不能应用于类的静态成员」
几个简单的例子,先感受下泛型

function log<T>(value: T): T {
    console.log(value);
    return value;
}
// 两种调用方式
log<string[]>(['a', ',b', 'c'])
log(['a', ',b', 'c'])
log('Nealyang')
  • 泛型类型、泛型接口

    type Log = <T>(value: T) => T
    let myLog: Log = log
    interface Log<T> {
      (value: T): T
    }
    let myLog: Log<number> = log // 泛型约束了整个接口,实现的时候必须指定类型。如果不指定类型,就在定义的之后指定一个默认的类型
    myLog(1)
    

    「我们也可以把泛型变量理解为函数的参数,只不过是另一个维度的参数,是代表类型而不是代表值的参数。」

    class Log<T> { // 泛型不能应用于类的静态成员
      run(value: T) {
          console.log(value)
          return value
      }
    }
    let log1 = new Log<number>() //实例化的时候可以显示的传入泛型的类型
    log1.run(1)
    let log2 = new Log()
    log2.run({ a: 1 }) //也可以不传入类型参数,当不指定的时候,value 的值就可以是任意的值
    

    类型约束,需预定义一个接口

    interface Length {
      length: number
    }
    function logAdvance<T extends Length>(value: T): T {
      console.log(value, value.length);
      return value;
    }
    // 输入的参数不管是什么类型,都必须具有 length 属性
    logAdvance([1])
    logAdvance('123')
    logAdvance({ length: 3 })
    

    泛型的好处:

  • 函数和类可以轻松的支持多种类型,增强程序的扩展性

  • 不必写多条函数重载,冗长的联合类型声明,增强代码的可读性
  • 灵活控制类型之间的约束

泛型,在 ts 内部也都是非常常用的,尤其是对于容器类非常常用。而对于我们,还是要多使用,多思考的,这样才会有更加深刻的体会。同时也对塑造我们类型思维非常的有帮助。

infer

infer 关键字最早出现在 PR 里面,「表示在 extends 条件语句中待推断的类型变量」
是在 ts2.8 引入的,在条件判断语句中,该关键字用于「替换手动获取类型」

type PramType<T> = T extends (param : infer p) => any ? p : T;

在上面的条件语句中,infer P 表示待推断的函数参数,如果T能赋值给(param : infer p) => any,则结果是(param: infer P) => any类型中的参数 P,否则为T.

interface INealyang{
  name:'Nealyang';
  age:'25';
}
type Func = (user:INealyang) => void;
type Param = ParamType<Func>; // Param = INealyang
type Test = ParamType<string>; // string

工具泛型

类型断言

  • 推荐类型断言的预发使用 as关键字,而不是<> ,防止歧义
  • 类型断言并非类型转换,类型断言发生在编译阶段。类型转换发生在运行时

    FAQ

    namespace 和 module 的区别?

    TypeScript的模块和ECMAScript 2015的模块概念相同, 任何包含 top-level import 或 export 的文件被视作一个模块。
    namespace作用于多个文件,module作用于单个文件

参考

TypeScript 入门教程-https://ts.xcatliu.com/