接口

对象接口:约束对象字段
interface FullName{
    firstName:string;
    secondName?:string;  // 加上问号,表示可选参数
}

注意一个叫额外属性的检查的东西:

function printName(name: FullName) {
  console.log(`${name.firstName} --- ${name.secondName}`);
}

像下面这样调用函数传参会报错,说多了一个team。

printName({
    firstName:'duncan',
    secondName:'tim',
    team:'spurs'
}) // error

但是如果将这个对象赋值给一个变量,再用这个变量作为参数传递就能跳过这种检查

var someone = {
  firstName: "duncan",
  secondName: "tim",
  team: "spurs"
};

printName(someone); // ok

也可以用as来跳过检查

printName({
    firstName:'duncan',
    secondName:'tim',
    team:'spurs'
} as FullName)
函数接口:约束函数参数和返回值
interface encrypt {
    (key:string, value:string):string
}

let en:encrypt = function (k:string, v:string):string{
    return k+v
}
类接口:约束类属性和方法
interface User1 {
  name: string; // 必须要有name属性,且为string类型
  sayHello(someword: string): void; // 必须要有sayHello方法
}

class RegularUser implements User1 {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHello(str: string) {
    console.log(`${this.name} says: ${str}`);
  }
}
接口拓展
interface PlayerUser extends User1 {
  team: string;
  play(): void;
}

泛型

泛型函数

定义:

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

跟普通的函数定义多了个<T>,可以看作是:之后(形参,函数体)都需要用T,所以得先声明T

使用:

let output = identity<string>("myString");

调用的时候,传入特定类型string来锁定T类型

泛型类型

那么泛型函数的类型也就多一个<T>

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

或者这样写:

let myIdentity: {<T>(arg: T): T} = identity;

个人看法,这种写法有点像上面的函数接口写法。果然马上下面就开始讲泛型接口了。

泛型接口
interface GenericIdentityFn {
    <T>(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity;

泛型接口还有一种写法,把T当作接口的参数,这样整个接口内都能使用到T了,使用的时候传入具体类型:

interface GenericIdentityFn<T> {
    (arg: T): T;
}

let myIdentity: GenericIdentityFn<number> = identity;
泛型类

和泛型接口类似,定义的时候后面多一个<T>

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
泛型约束

因为泛型类型可以是任意类型,因此里面有哪些字段完全不确定,而当你确定T里面一定需要哪些字段的时候就需要对泛型类型进行约束了。如下例,谁也不能保证T里有length字段,因此报错。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

定义一个接口来约束T,这样就保证了T中一定会有什么。

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

类型兼容性

比较两个函数
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

这样理解,y = x的时候,最终调用签名是(b: number, s: string) => 0;,也就是说调用的时候得传入两个参数,但是实际用到的只有第一个参数而已,忽略传入的参数当然是被允许的

同理,x = y的时候,x的签名只能传入一个参数,但是赋值的y类型函数却需要两个参数,这当然是不行的。

因此,如果y的签名写成(b: number, ?s: string) => 0;就不会报错。

枚举

枚举类型与数字互相兼容,但是枚举类型之间不兼容,虽然枚举类型实际上的值都是数字。

高级类型

交叉类型 Intersection Types

a & b 表示组成一种新的类型,相当于并操作,新类型包含a,b中的字段。

联合类型 Union Types

a | b 表示该类型是a 或者b。

interface Bird {
  fly: () => void;
  layEggs: () => void;
}

interface Fish {
  swim: () => void;
  layEggs: () => void;
}

function getSmallPet(): Fish | Bird {
  class f implements Fish {
    swim() {}
    layEggs() {}
  }
  return new f();
}

被这样定义的变量,只能使用a和b共有的字段,比如下面这样不行,因为只能使用共有的字段layEggs。

let pet = getSmallPet();
if (pet.swim) { // error
  pet.swim();
} else if (pet.fly) { // error
  pet.fly();
}

那么要如何确定到底是那种类型呢?类型断言

if ((<Fish>pet).swim) {
  (<Fish>pet).swim();
}
else {
  (<Bird>pet).fly();
}

如果嫌这种方法麻烦可以直接用typeof或者instanceof来判断

用户自定义的类型保护

上面这种断言如果到处要用的话就得重复写,造成大量重复代码。可以定义成函数,反复调用。这个叫用户自定义的类型保护。里面有个 parameterName is Type这种形式的语法, parameterName必须是来自于当前函数签名里的一个参数名。

function isFish(pet: Fish | Bird): pet is Fish {
  return (<Fish>pet).swim !== undefined;
}
if (isFish(pet)) {
  pet.swim()
} else {
  pet.fly()
}

TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型。这一点也可以在代码提示上看出来。

null
function fun(arg: string | null): number {
  return arg.length // error
}

arg可能为null,那么可以用===来做判断,也可以用短路方式来避免错误,还可以加叹号!来去除null。

function fun(arg: string | null): number {
  return arg!.length  // ok
}
映射类型

这玩意有点意思,映射通常来说操作的都是数据,但是这里操作的是类型,将一个类型映射(转换)成一种新的类型。既然是操作类型,那当然就要用泛型,因为需要传入的参数是类型。

下面的代码将一个类型的所有属性都设置成只读,映射出一种新类型。

interface Per {
  name: string;
  age: number
}
type ReadonlyType<T> = {
  readonly [P in keyof T]: T[P]
}

let rp: ReadonlyType<Per> = {
  name: 'kobe',
  age: 99
}

rp.name = 'uuu' // error

主要是熟悉[P in keyof T]: T[P]这种写法,遍历类型中所有的key,然后索引得到对应key的值(也就是string,number这些的),批量操作。并且,P in keyof T取到的属性是包含属性修饰符的,即如果Per中name是被readonly或者?修饰的,那么,keyof取到的属性也同样带这些修饰符。

还可以这样用

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
let ffs: Flags = {
  option1: false,
  option2: true
}

因此ts提供了一些工具,像Partial,Readonly之类的封装了这些操作。

infer
type FunWithMappedArgt<P extends { [key: string]: any }> = (args: P) => any;
type DestructuredArguments<F extends FunWithMappedArgt<any>> = F extends FunWithMappedArgt<infer R> ? R : never;

FunWithMappedArgt类型为一个通用的函数类型,DestructuredArguments就接受一个通用类型的函数,然后返回该函数的参数类型。

infer实际上是一个声明关键字,我们可以用它来声明一个变量, 只能在extends 条件语句中使用infer关键字