# TypeScript-高级类型

# 类型推断 类型兼容性

这块稍微去看下文档即可

# 交叉类型 &

interface A {
  a: string;
}
interface B {
  b: string;
}
type C = A & B;
const c: C = { a: '1', b: '1' };

变量c必须有用a和b属性, 否则不合法。 再举个泛型的例子

function extend<T extends object, U extends object>(
  first: T,
  second: U
): T & U {
  const result = {} as T & U;
  for (let key of Object.keys(first)) {
    result[key] = first[key];
  }
  for (let key of Object.keys(second)) {
    result[key] = second[key];
  }
  return result;
}

感觉这个交叉类型, 应该不是取几个种类的交集,而是并集。 所以叫合并类型比较合适。

# 联合类型 |

可以理解为类型必须是多种类型的其中一种, 类型兼容也是合法的, 比如下面的三个c变量都是合法的

interface A {
  a: string;
}
interface B {
  b: string;
}
type C = A | B;
const c1: C = { a: '1', b: '1' };
const c2: C = { a: '1' };
const c3: C = { b: '1' };

比如 type numStr = number | string, numStr 表示类型可以是 number也可以是string

# 类型保护与区分类型 类型收窄

举个例子, 下面是用户自定义类型保护

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

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

  function getSmallPet(): Fish | Bird {
    return Math.random() > 0.5
      ? {
          swim() {},
          layEggs() {}
        }
      : {
          fly() {},
          layEggs() {}
        };
  }
  // 如此的话就需要断言
  const pet = getSmallPet();
  if ((<Bird>pet).fly) {
    (<Bird>pet).fly();
  } else {
    (<Fish>pet).swim();
  }

  // 我们可以自定义类型保护
  function isFish(pet: Fish | Bird): pet is Fish {
    return (<Fish>pet).swim !== undefined;
  }

  if (isFish(pet)) {
    pet.swim();
  }

function isFish(pet: Fish | Bird): pet is Fish 说实话不是很好理解, 还是断言看起来合适一点

typeof 类型保护

function getStr(s: number | string): string {
  if (typeof s === 'number') {
    return s.toString();
  }
  return s;
}

instanceof类型保护

和上述的类似, 不展开了

类型保护和类型断言

function bar(name: string | null) {
  if (name === null) {
    return '';
  }
  return name;
}

使用if判断收窄类型, 采用 return name === null ? '' : name 也是一样的道理

如果编译器不能够去除 nullundefined,你可以使用类型断言手动去除。 语法是添加 !后缀

function fun(name: string | null | undefined) {
    return name!.toLocaleLowerCase()
}

# 类型别名

所谓类型别名,顾名思义,一个可以指代任意类型的名字。 语法如下

type ID = number | string;
type Node<T> = {
  data: T;
  next: Node<T> | null;
};

# 接口和类型别名的区别

基本上可以看做一样。区别也有一点

其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 这个区别其实没什么大问题。

另一个重要区别是类型别名不能被 extendsimplements(自己也不能 extendsimplements其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的 (opens new window),你应该尽量去使用接口代替类型别名。

另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

个人理解, 不需要设计到上述的区别时, 直接使用type也是可以的。

# 字面量类型

用法如下, 可以使用一些常量作为类型

type B = true | false;
type A = 1 | 2;
type C = 'A' | 'B' | 'C';
// c 只能是三个字母中的其中一个, 其他的任何值都是错的
const c:C = 'A'; 

# 索引类型

我们之前说过的是索引签名, 不要搞混。使用索引类型,编译器就能够检查使用了动态属性名的代码

首先我们要知道关键字keyof, 索引类型查询操作符, 比如keyof T, 对于任何类型 Tkeyof T的结果为 T上已知的公共属性名的联合。 举个例子

interface Person {
  name: string;
  age: number;
}
type PersonKey = keyof Person;
// 可以认为等价与 type PersonKey = 'name' | 'age' 只不过数据是动态获取
// 当Person增加了属性时, PersonKey 也会动态扩展
const a: PersonKey = 'name';
const b: PersonKey = 'age';
// 不合法
// const c: PersonKey = 'sex';

再举一个例子, 比如说对象转换特定key值对应的值为数组

function trans<T, K extends keyof T>(obj: T, keys: K[]): T[K]
  return keys.map(key => obj[key]);
}
const obj = {
  a: 'a',
  b: 2,
  c: 3,
  d: true
};
// 这里其实也会把ret自动推断为 (string | number | boolean)[]
const ret = trans(obj, ['a', 'b', 'c', 'd']);

K extends keyof T 这个还是比较实用的, T[K]我们称为索引访问操作符

再举一个简单的例子, 获取对象的属性值

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
// as: number
const a2 = getProperty({ a: 1 }, 'a');
// a3: string
const a3 = getProperty({ a: '1' }, 'a');
# 索引类型和字符串索引签名
interface Map<T> {
  [key: string]: T;
}
// string | number
type keyOfMap = keyof Map<number>;
const v: Map<number> = { foo: 1 };
//   foo: number
const foo = v['foo'];

# 映射类型

通常用于基于一个类型创建另外的类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

  interface Person {
    name: string;
    age: number;
  }
  // 变成只读的
  type Readonly<T> = {
    readonly [K in keyof T]: T[K];
  };
  /* 
  type PersonReadonly = {
    readonly name: string;
    readonly age: number;
   }
  */
  type PersonReadonly = Readonly<Person>;
  // 变成可选的
  type Partial<T> = {
    [K in keyof T]?: T[K];
  };
  /* 
  type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
  }
  */
  type PersonPartial = Partial<Person>;

通过 in keyof T 的语法, 获取泛型T里面所有的键值。 里面的in其实也可以是一个单独的语法, 我们看一个简单的例子

type keys = 'A' | 'B';
type Flags = {
  [K in keys]: boolean;
};
/* 等价于
  type Flags = {
      A: boolean;
      B: boolean;
  }
*/

还有 PickRecord, 和 PartialReadopnly 一样内置的

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
type Record<K extends string, T> = {
    [P in K]: T;
}

Pick获取类型里某一个键值对应的类型, 比如type pp = Pick<Person, 'name'>; pp的类型就是 string

Record 是根据键值集合生成对应值的类型。 比如

type keys2 = 'A' | 'B' | 'C' | '1';
type RecordKeys = Record<keys2, number>;
/*  等价于
type RecordKeys = {
  A: number;
  B: number;
  C: number;
  1: number;
}
*/
# 由映射类型进行推断 拆包处理
  type Proxy<T> = {
    get(): T;
    set(value: T): void;
  };

  type Proxify<T> = {
    [K in keyof T]: Proxy<T[K]>;
  };

  function unproxify<T>(t: Proxify<T>): T {
    const result = {} as T;
    for (let key in Object.keys(t)) {
      result[key] = t[key].get();
    }
    return result;
  }

  interface Person {
    name: string;
    age: number;
  }

  type ProxifyPerson = Proxify<Person>;
  function proxify<T>(obj: T): Proxify<T> {
    const result = {} as Proxify<T>;
    for (let key of Object.keys(obj)) {
      result[key] = {
        get: () => {
          return obj[key];
        },
        set: v => {
          obj[key] = v;
        }
      };
    }
    return result;
  }
  const p: Person = {
    name: 'xdyuan',
    age: 20
  };
  const pp = proxify<Person>(p);
  console.log(pp);
  pp.name.set('Cloud');
  console.log(pp.name.get());
# 通过as实现键名重新映射
type Getter<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
  name: string;
  age: number;
}
/* 
type PersonGetter = {
  getName: () => string;
  getAge: () => number;
}
*/
type PersonGetter = Getter<Person>;

通过as可以基于之前的属性名创建一个新的属性名

# 预定义的有条件类型

TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:

  • Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
  • Extract<T, U> -- 提取T中可以赋值给U的类型。
  • NonNullable<T> -- 从T中剔除nullundefined
  • ReturnType<T> -- 获取函数返回值类型。
  • InstanceType<T> -- 获取构造函数类型的实例类型。

利用条件类型返回一个 never 从而过滤掉某些属性:

type RemoveKindField<T> = {
  [K in keyof T as Exclude<K, 'kind'>]: T[K];
};
interface Circle {
  kind: 'circle';
  radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;

KindlessCircle 就没有kind属性了

# 类型中的字符串联合类型(String Unions in Types)
type PropEventSource<T> = {
  on(
    eventName: `${string & keyof T}Changed`,
    callback: (newValue: any) => void
  ): void;
};
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
const p: Person = { name: 'x', age: 2 };
const ret = makeWatchedObject(p);
ret.on('nameChanged', str => {});
# 内置字符操作类型

TypeScript 的一些类型可以用于字符操作,这些类型处于性能的考虑被内置在编译器中,

Uppercase, Lowercase, Capitalize, Uncapitalize

type hello = 'hello';
//   type HELLO = "HELLO"
type HELLO = Uppercase<hello>;

# Symbol

const symbol: Symbol = Symbol('foo');

# namespace 命名空间

TypeScript 有它自己的模块格式,名为 namespaces 。它在 ES 模块标准之前出现。这个语法有一系列的特性,可以用来创建复杂的定义文件。虽然命名空间没有被废弃,但是由于 ES 模块已经拥有了命名空间的大部分特性,因此更推荐使用 ES 模块,这样才能与 JavaScript 的(发展)方向保持一致。你可以在命名空间页面 (opens new window)了解更多。

namespace TS644 {
    export interface P {}
}
上次更新: 1/22/2025, 9:39:13 AM