高级类型

交叉类型

是将多个类型合并为一个类型,包含了所需要的所有类型的特性
使用场景:大多是在混入(mixins)或其他不适合典型面向对象模型的地方看到交叉类型的使用

// 同时满足T跟U两种类型
function extend(first: T, second: U): T & U {
  let result = {};
  for(let id in first){
    (result)[id] = (first)[id];
  }
  for(let id in second){
    if(!result.hasOwnProperty(id)){
      (result)[id] = (second)[id];
    }
  }
  return result;
}
class Person {
  constructor(public name: string){}
}
interface Loggable {
  log(): void;
}
class ConsoleLogger implements Loggable {
  log() {
    console.log('this is log')
  }
}
var jim = extend(new Person('Jim'), new ConsoleLogger());
var n = jim.name;
jim.log();

联合类型

联合类型跟交叉类型很有关联,但使用上完全不同:

  • 联合类型表示一个值可以是集中类型之一,用竖线(|)分割每个类型
  • 如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员
// 这种方式padding传入的是any类型,也就是说可以传入number跟string以外的类型
function padLeft(value: string, padding: any){
  if(typeof padding === 'number'){
    return Array(padding + 1).join(' ') + value;
  }
  if(typeof padding === 'string'){
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`)
}

// 通过联合类型实现:控制只能传入两种指定类型
function padLeft1(value: string, padding: string | number){
  // ...
}
interface Bird{
  fly();
  layEggs();
}
interface Fish{
  swim();
  layEggs();
}
function getSmallPet(): Fish | Bird {}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors
// 类型保护与类型区分:上面例子通过这种方式判断会报错
let pet = getSmallPet();

// 每一个成员访问都会报错
if (pet.swim) {
  pet.swim();
}
else if (pet.fly) {
  pet.fly();
}

// 想判断类型需要使用类型断言
let pet = getSmallPet();

if ((pet).swim) {
  (pet).swim();
}
else {
  (pet).fly();
}

用户自定义的类型保护

pet is Fish 类型谓词:parameterName is Type 形式

// 每当使用一些变量调用isFish时,TS会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的
function isFish(pet: Fish | Bird): pet is Fish{
  return (pet).swim !== undefined;
}
let pet = getSmallPet();
if (isFish(pet)) {
  pet.swim();
}else {
  pet.fly();
}

typeof 类型保护

function isNumber(x: any): x is number {
  return typeof x === 'number';
}
function isString(x: any): x is string{
  return typeof x === 'string';
}
function padL(value: string, padding: string | number) {
  if(isNumber(padding)){
    return Array(padding + 1).join(' ') + value;
  }
  if(isString(padding)){
    return padding + value;
  }
  // 两种方式:typeof v === 'typename' 其中typename必须是:number,string,boolean,symbol
  if(typeof padding === 'number'){
    return Array(padding + 1).join(' ') + value;
  }
  if(typeof padding === 'string'){
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

instanceof 类型保护

是通过构造函数来细化类型的一种方式

// instanceof 右侧要求是一个构造函数,TS将其细化为:此构造函数的prototype属性的类型,如果他的类型不为any的话,构造签名所返回的类型的联合
interface Padder {
  getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number){}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(' ');
  }
}
class StringPadder implements Padder {
  constructor(private value: string){}
  getPaddingString(){
    return this.value;
  }
}
function getRandomPadder(){
  return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(' ');
}
let padder: Padder = getRandomPadder();
if(padder instanceof SpaceRepeatingPadder){
  padder;
}
if(padder instanceof StringPadder){
  padder;
}

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型

  • 类型别名也可以是泛型
  • 也可以使用类型别名来在属性里引用自己
  • 与交叉类型一起使用

注:起别名不会新建一个类型 - 它创建了一个新 名字来引用那个类型
注:类型别名不能出现在声明右侧的任何地方

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
  if (typeof n === 'string') {
    return n;
  }
  else {
    return n();
  }
}
// 泛型
type Container = { value: T };
// 用类型别名来在属性里引用自己
type Tree = {
  value: T;
  left: Tree;
  right: Tree;
}
// 与交叉类型一起使用,可以创建出一些十分稀奇古怪的类型
type LinkedList = T & { next: LinkedList };
interface Person {
  name: string;
}

var people: LinkedList;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
// 类型别名不能出现在声明右侧的任何地方
type Yikes = Array; // error

接口 vs. 类型别名

类型别名可以像接口一样,但仍然有一些区别:

  • 接口创建了名字,可以在其他任何地方使用,类型并不创建新名字:如,错误信息就不会使用别名
  • 类型不能被extends和implements(自己也不能extends和implements其他类型)

注:因为软件中对象应该对于扩展是开放的,但对于修改是封闭的,应该尽量使用接口代替类型别名

type Alias = { num: number };
interface Interface { num: number };
// 在编译器中将鼠标悬停在 interfaced上,显示它返回的是 Interface
declare function aliased(arg: Alias): Alias;
// 悬停在 aliased上时,显示的却是对象字面量类型
declare function interfaced(arg: Interface): Interface;

字符串字面量类型

允许指定字符串必须的固定值

注:在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合,通过结合使用这些特性,可以实现类似枚举类型的字符串

数字字面量类型:很少使用可以缩小范围调试bug

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
  animate(dx: number, dy: number, easing: Easing){
    if(easing === "ease-in"){
      console.log('this is ease-in');
    }else if (easing === "ease-out"){
      console.log('this is ease-out');
    }else if (easing === "ease-in-out"){
      console.log('this is ease-in-out');
    }else {
      console.log('no animate');
    }
  }
}
let button = new UIElement();
button.animate(0,0,'ease-in');
button.animate(0, 0, "uneasy"); // error no animate
// 数字字面量类型
function rollDie(x: number) : 1 | 2 | 3 | 4 {
  // 当 x与 2进行比较的时候,它的值必须为 1,这就意味着下面的比较检查是非法的
  if(x !== 1 || x !== 2) {}
  return 1;
}

可辨识联合

可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,也称作:标签联合或代数数据类型

TS基于已有的JS模式有三个要素:

  • 具有普通的单例类型属性---可辨识的特征
  • 一个类型别名包含了那些类型的联合---联合
  • 此属性上的类型保护
// 声明了联合的接口,每个接口都有kind属性但有不同的字符串字面量类型
// kind属性:称作可辨识的特征或标签,其他属性则特定于各自接口
// 注:目前各个接口时没有联系的
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(s: Shape) {
  switch(s.kind){
    case 'square': return s.size * s.size;
    case 'rectangle': return s.height * s.width;
    case 'circle': return Math.PI * s.radius ** 2;
  }
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们
比如,如果添加了Triangle到 Shape,同时还需要更新area
有两种方式可以实现:

  • 首先是启用 --strictNullChecks并且指定一个返回值类型
  • 使用never类型,编译器用它来进行完整性检查
// 因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。 
// 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined
function area1(s: Shape): number {
  switch(s.kind){
    case 'square': return s.size * s.size;
    case 'rectangle': return s.height * s.width;
    case 'circle': return Math.PI * s.radius ** 2;
  }
}
// 第二种方法使用 never类型
function assertNever(x: never): never{
  throw new Error("Unexpected object: " + x);
}
// assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型
// 如果忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误
function area2(s: Shape): number {
  switch(s.kind){
    case 'square': return s.size * s.size;
    case 'rectangle': return s.height * s.width;
    case 'circle': return Math.PI * s.radius ** 2;
    default: return assertNever(s);
  }
}

多态的this类型

表示某个包含类或接口的子类型,这被称作 F-bounded多态性
应用:他能很容易的表现连贯接口间的继承

class BasicCalculator {
  public constructor(protected value: number = 0){}
  public currentValue(): number {
    return this.value;
  }
  public add(operand: number): this {
    this.value == operand;
    return this;
  }
  public multiply(operand: number): this {
    this.value *= operand;
    return this;
  }
}
let val = new BasicCalculator(2).multiply(5).add(1).currentValue();
// 由于这个类使用了 this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变
class ScientificCalculator extends BasicCalculator {
  public constructor(value = 0){
    super(value);
  }
  public sin(){
    this.value = Math.sin(this.value);
    return this;
  }
}
let scientVal = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();

索引类型

使用索引类型,编译器就能够检查使用了动态属性名的代码
keyof T:索引类型查询操作符,对于任何类型T,keyof T的结果为T上已知的公共属性名的联合
T[K]:索引访问操作符
索引类型和字符串索引签名:keyof和 T[K]与字符串索引签名进行交互

注: 如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。 并且 T[string]为索引签名的类型

// JS模式从对象中选取属性的子集
function pluck(o, names){
  return names.map(n => o[n]);
}
// 在TS中通过索引类型检查和索引访问操作符
function pluck1(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}
interface Person {
  name: string;
  age: number;
}
let person: Person = {
  name: 'Jarid',
  age: 20
}
let strings: string[] = pluck1(person, ['name']);
// 索引类型和字符串索引签名
interface Maps {
  [key: string]: T;
}
let keys: keyof Maps; // string
let value: Maps['foo']; // number
console.log('keys: ' + keys, + ' value: ' + value)

映射类型

TS提供了从旧类型中创建新类型的一种方式 — 映射类型
定义:在映射类型里,新类型以相同的形式去转换旧类型里每个属性

语法与索引签名的语法类型,内部使用了for...in具有三个部分:

  • 类型变量K,他会依次绑定到每个属性
  • 字符串字面量联合的keys,包含了要迭代的属性名的集合
  • 属性的结果类型
// 一个常见的任务是将一个已知的类型每个属性都变为可选的:
interface PersonPartial1 {
  name?: string;
  age?: number;
}
// 或者只想要一个只读版本:
interface PersonReadonly1 {
  readonly name: string;
  readonly age: number;
}

// 映射类型
type Readonlyp = {
  readonly [P in keyof T]: T[P];
}
type Partials = {
  [P in keyof T]?: T[P];
}
// 使用
type PersonPartial = Partials;
type ReadonlyPerson = Readonlyp;

type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

预定义的有条件类型

TS2.8中增加了一些预定义的有条件类型:

  • Exclude -- 从T中剔除可以赋值给U的类型
  • Extract -- 提取T中可以赋值给U的类型
  • NonNullable -- 从T中剔除null和undefined
  • ReturnType -- 获取函数返回值类型
  • InstanceType -- 获取构造函数类型的实例类型
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "b" | "d"
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">;  // "a" | "c"

type T02 = Exclude void), Function>;  // string | number
type T03 = Extract void), Function>;  // () => void

type T04 = NonNullable;  // string | number
type T05 = NonNullable<(() => string) | string[] | null | undefined>;  // (() => string) | string[]

function f1(s: string) {
    return { a: 1, b: s };
}

class C {
    x = 0;
    y = 0;
}

type T10 = ReturnType<() => string>;  // string
type T11 = ReturnType<(s: string) => void>;  // void
type T12 = ReturnType<(() => T)>;  // {}
type T13 = ReturnType<(() => T)>;  // number[]
type T14 = ReturnType;  // { a: number, b: string }
type T15 = ReturnType;  // any
type T16 = ReturnType;  // any
// type T17 = ReturnType;  // Error
// type T18 = ReturnType;  // Error

type T20 = InstanceType;  // C
type T21 = InstanceType;  // any
type T22 = InstanceType;  // any
// type T23 = InstanceType;  // Error
// type T24 = InstanceType;  // Error

你可能感兴趣的:(高级类型)