高级类型

交叉类型

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

// 同时满足T跟U两种类型
function extend<T, U>(first: T, second: U): T & U {
  let result = <T & U>{};
  for(let id in first){
    (<any>result)[id] = (<any>first)[id];
  }
  for(let id in second){
    if(!result.hasOwnProperty(id)){
      (<any>result)[id] = (<any>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 ((<Fish>pet).swim) {
  (<Fish>pet).swim();
}
else {
  (<Bird>pet).fly();
}

用户自定义的类型保护

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

// 每当使用一些变量调用isFish时,TS会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的
function isFish(pet: Fish | Bird): pet is Fish{
  return (<Fish>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<T> = { value: T };
// 用类型别名来在属性里引用自己
type Tree<T> = {
  value: T;
  left: Tree<T>;
  right: Tree<T>;
}
// 与交叉类型一起使用,可以创建出一些十分稀奇古怪的类型
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
  name: string;
}

var people: LinkedList<Person>;
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<Yikes>; // 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<T, K extends keyof T>(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<T> {
  [key: string]: T;
}
let keys: keyof Maps<number>; // string
let value: Maps<number>['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<T> = {
  readonly [P in keyof T]: T[P];
}
type Partials<T> = {
  [P in keyof T]?: T[P];
}
// 使用
type PersonPartial = Partials<Person>;
type ReadonlyPerson = Readonlyp<Person>;

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

预定义的有条件类型

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

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型
  • Extract<T, U> -- 提取T中可以赋值给U的类型
  • NonNullable<T> -- 从T中剔除null和undefined
  • ReturnType<T> -- 获取函数返回值类型
  • InstanceType<T> -- 获取构造函数类型的实例类型
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<string | number | (() => void), Function>;  // string | number
type T03 = Extract<string | number | (() => void), Function>;  // () => void

type T04 = NonNullable<string | number | undefined>;  // 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>() => T)>;  // {}
type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T14 = ReturnType<typeof f1>;  // { a: number, b: string }
type T15 = ReturnType<any>;  // any
type T16 = ReturnType<never>;  // any
// type T17 = ReturnType<string>;  // Error
// type T18 = ReturnType<Function>;  // Error

type T20 = InstanceType<typeof C>;  // C
type T21 = InstanceType<any>;  // any
type T22 = InstanceType<never>;  // any
// type T23 = InstanceType<string>;  // Error
// type T24 = InstanceType<Function>;  // Error
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容