# 高级类型

# 交叉类型

交叉类型是将多个类型合并为一个新的类型(并集)。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。交叉类型通过符号&来实现,例如, Person & Serializable & Loggable包括 PersonSerializableLoggable这三种类型。 就是说这个类型的对象同时拥有了这三种类型的成员。示例:

let mergeFunc = <T,U>(arg1:T,arg2:U) => {
  let result = {};
  result = Object.assign(arg1,arg2);  // 合并
  return result;
}

上面的代码中,我们合并参数arg1arg2,那么result应该同时包含TU两种类型。因此,我们如果想要返回确定的类型,我们可以使用交叉类型T&U。如下所示:

let mergeFunc = <T,U>(arg1:T,arg2:U):T & U => {
  let result = {} as T & U;
  result = Object.assign(arg1,arg2);
  return result;
}

# 联合类型

很多情况下我们希望数据类型可以是多种类型,但是又不希望通过使用any来丢失对类型的验证,这时候我们可以使用联合类型。联合类型通过符号|来实现。number|string表示既可以是number类型,又可以是string类型。但是联合类型不同于交叉类型,不可以既包含number类型的特性又包含string类型的特性。

let func = (arg:number | string):number => {
  let result = 0;
  if(typeof arg === 'string'){
    result = arg.length;
  }else if(typeof arg === 'number'){
    result = arg.toString().length;
  }
  return result;
}

# 类型保护

联合类型适合于那些值可以为不同类型的情况。但当我们想确切地了解是否为某种类型时怎么办?ts是无法确切地区分开来联合类型数据具体类型。示例:

const arr = ['123',456]
const func2 = ():number | string => {
  const nums = Math.random();
  if(nums < 0.5){
    return arr[0];
  }else{
    return arr[1]
  }
}

let result = func2();  // number | string
if(result.length){  // error:Property 'length' does not exist on type 'number'
  console.log(result.length)
}else{
  console.log(result.toFixed());
}

上面的代码中,我们定义了一个func2函数,它会返回number|string联合类型。但是具体是什么类型无法确定。接下来我们直接把它的某种类型来作为判断result.length,这样会导致报错,因为ts在编译阶段无法确切地知道result的具体类型,所以ts会同时使用numberstring类型来判断,这样的话就会导致number类型编译时出现错误。解决方法之一:使用类型断言,确定地告诉ts类型。

if((result as string).length){
  console.log((result as string).length)
}else{
  console.log((result as number).toFixed());
}

使用类型断言,在每个使用result的部分,确切地指定类型可以避免报错。但是这需要在所有地方都使用类型断言。如果使用地方较多,那么会导致不得不多次使用类型断言。不得不多次使用类型断言。因此,我们希望只要检查过一次,在之后都可以直接使用就好。ts提供了类型保护。

类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:

function isString(value:number | string):value is string{
  return typeof value === 'string';
}

在这个例子里,value is string就是类型谓词。 类型谓词都是 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名,type必须是指定的联合类型的一种。函数体返回的是一个Boolean值。
这样的话,每当我们需要使用类型保护时,只需要在一开始调用一下这个函数。TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

if(isString(result)){        // 调用一次即可。
  console.log(result.length);
}else{
  console.log(result.toFixed());
}

如上所示,我们通过对变量result调用一次isString函数,那么result的变量类型就会缩减为具体的一种类型,在后面所有的使用中都是这种类型。

typeof 类型保护 在上面的代码中,我们使用类型保护去判断变量是不是string类型,就需要去定义一个函数。如果我们还需要判断number类型,那么我们还得定义一个简单的类型保护。事实上,对于这种简单地只需要通过typeof xx === string这种判断,ts可以直接使用typeof来作为类型保护。示例:

if(typeof result === 'string'){
  console.log(result.length);
}else if(typeof result === 'number'){
  console.log(result.toFixed());
}

注意:typeof类型保护

  1. 只有两种形式能被识别: typeof v === typenametypeof v !== typename
  2. typename必须是numberstring,booleansymbol

instanceof 类型保护 instanceof操作符是用来判断是否是某个构造函数或者类创建的实例。同样ts提供了instanceof类型保护,是通过构造函数来细化类型的一种方式。

class Class1{};
class Class2{};
const func2 = ():Class1 | Class2 => {
  const nums = Math.random();
  if(nums < 0.5){
    return new Class1();
  }else{
    return new Class2()
  }
}
let result = func2();
if(result instanceof Class1){   // instanceof 提供类型保护。自动
  console.log('result是class1的实例')
}else{
  console.log('result是class2的实例')
}

上面的代码中,instanceof提供了类型保护,自动将实例类型缩小为具体的实例类型。

# 类型别名

类型别名会给一个类型起个新名字,但是它不会新建一个类型,它只是创建了一个新名字来引用那个类型。 类型别名通过type 关键字来定义。示例:

type str = string;
type num = number;
const func = (name:str,age:num):str|num => {
    return 111;
}

通过使用type,我们可以给类型起一个名字,在使用该类型时,直接使用名字代替,类型别名的使用有点类似于接口。但是给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
泛型类型别名
同接口一样,类型别名可以使用泛型,我们可以添加类型参数并且在别名声明的右侧传入。

type Name<T> = {name:T}

let n:Name<string> = {name:'hello'};

# 字面量类型

字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。也就是这种类型必须是该字符串。示例:

type Name = "hahaha";
let n1:Name = "hahaha"; // 正常赋值
let n2:Name = 'hello';  // error Type '"hello"' is not assignable to type '"hahaha"'.

使用type指定字面量类型为hahaha,那么当属于这种类型时,只能为hahaha,不能是其他值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。示例:

type Direction = "left" | "right";
const getDirection = (direction:Direction):string => {
    let result = ""
    if(direction === 'left'){
        result = 'Go to Left';
    }else if(direction === 'right'){
        result = 'Go to right'
    }else{
        result = 'no'
    }
    return result;
}

如果是联合类型的字符串字面量类型,那么你只能从指定的这几个值中选取。
数字字面量类型
数字字面量类型同字符串字面量类型一致,允许你指定数字必须的固定值。

type Num = 18;
let num1:Num = 18;
let num2:Num = 19; // Type '19' is not assignable to type '18'.

# 枚举成员类型

当枚举的成员满足了一定条件以后,枚举成员可以作为数据类型使用。

  1. 所有的枚举成员都是未赋值的
  2. 所有的枚举成员都是数值
  3. 所有的枚举成员都是字符串
enum A {a};
enum B {b = 1,c = 2};
enum C {d = 'hello',e = 'world}

当枚举的成员,满足上面三种任一类型时,就可以作为一种类型使用,示例:

enum Animal {
    Dog = 1,
    Cat = 2
};
interface AInterface {
    type1:Animal.Dog,   // 接口属性定义成枚举成员类型
    type2:Animal.Cat,   // 接口属性定义成枚举成员类型
};
let animal:AInterface = {
    type1:Animal.Dog,  // 这里可以使用Animal.Dog
    type2:2            // 也可以直接使用对应的数值
}