TS笔记(7-8)

89 阅读7分钟

类型别名

简单使用

type a = string
type b = 200 | 201 | 204

const c:b = 201
接受泛型,成为工具类型
type Factory<T> = T | number | string;

type Bool = Factory<boolean>;
const foo: Bool = true;
工具类型的简单使用
type MaybeNull<T> = T | null;

function process(input: MaybeNull<{ handler: () => {} }>) {
  input?.handler();
}

该工具类型接受一个类型,返回与null的联合类型

交叉类型

交叉类型使用符号&,如符号所示,实现交叉类型需要满足其中的所有类型,如下面的例子

interface a {
  name: string;
}
interface b {
  age: number;
}
type test = a & b;
const obj: test = {
  name: "a",
  age: 123,
};

上面的例子是对对象类型的合并,如果对原始类型合并,我们会得到never类型

type foo = string;
type bar = number;
type baz = foo & bar;
const s: baz = 1; // 不能将number分配给never

对于对象类型的交叉类型,其内部的同名属性则会按交叉方式进行合并

type Struct1 = {
  primitiveProp: string;
  objectProp: {
    name: string;
  };
};

type Struct2 = {
  primitiveProp: number;
  objectProp: {
    age: number;
  };
};

type Composed = Struct1 & Struct2;

type PrimitivePropType = Composed["primitiveProp"]; // never
type ObjectPropType = Composed["objectProp"]; // { name: string; age: number; }

对于两个联合类型组成的交叉类型,需要实现两个联合类型的最小交集即可

索引类型

type foo = {
    [key: string]: number
}
const bar: foo = {
    name: 1
}

但由于 JavaScript 中,对于 obj[prop] 形式的访问会将数字索引访问转换为字符串索引访问,也就是说, obj[599] 和 obj['599'] 的效果是一致的。因此,在字符串索引签名类型中我们仍然可以声明数字类型的键。类似的,symbol 类型也是如此:
索引签名类型也可以和具体的键值对类型声明并存,但这时这些具体的键值类型也需要符合索引签名类型的声明:

interface AllStringTypes {
  // 类型“number”的属性“propA”不能赋给“string”索引类型“boolean”。
  propA: number;
  [key: string]: boolean;
}

这里的符合即指子类型,因此自然也包括联合类型,上述改成number | boolean即可.

使用场景

重构JS代码时,为内部属性较多的对象指定一个any的索引类型签名,以此获取类型支持,并慢慢补全类型

interface AnyTypeHere {
  [key: string]: any;
}

const foo: AnyTypeHere["linbudu"] = "any value";
索引类型查询

使用keyof关键字,将对象中的键全部转化为字面量类型再组成联合类型,

interface Foo { linbudu: 1, 599: 2 } 
type FooKeys = keyof Foo;
索引类型访问

类似于js中通过 obj[expression] 的方式来动态访问一个对象属性, ts中也可以通过这种方式访问到类型,如下面的例子:

interface foo {
  propa: number;
  [key: string]: number;
}

type bar = foo[string];
type baz = foo["propa"];

本质上就是通过键的字面量类型访问键对应的类型,当然,你也可以通过keyof关键字获取所有键的字面量类型从而获取到对象中所有的键对应的类型。

映射类型

映射类型的主要作用是根据键名映射到键值类型,举例如下:
这个工具类型会接受一个对象类型(假设我们只会这么用),使用 keyof 获得这个对象类型的键名组成字面量联合类型,然后通过映射类型(即这里的 in 关键字)将这个联合类型的每一个成员映射出来,并将其键值类型设置为 string。
in的作用是将类型映射出来

type foo<T> = {
  [K in keyof T]: string;
};

interface Foo {
  prop1: string;
  prop2: number;
  prop3: boolean;
  prop4: () => void;
}

type bar = foo<Foo>;

const baz: bar = {
  prop1: "123",
  prop2: "123",
  prop3: "123",
  prop4: "123",
};

要想克隆一个对象类型,我们可以这样写

type Clone<T> = {
    [K in keyof T]: T[K]
}
#### typeof
ts中的typeof用于类型查询, 例子如下
```ts
const str = "linbudu";
const obj = { name: "linbudu" };
const nullVar = null;
const undefinedVar = undefined;
const func = (input: string) => {
  return input.length > 10;
};
type Str = typeof str; // "linbudu"
type Obj = typeof obj; // { name: string; }
type Null = typeof nullVar; // null
type Undefined = typeof undefined; // undefined
type Func = typeof func; // (input: string) => boolean

你也可以在工具类型上使用typeof

const func = (name: string) => {
  return name.length > 10;
};
const fn2: typeof func = (name: string) => {
  return true;
};

绝大部分情况下,typeof 返回的类型就是当你把鼠标悬浮在变量名上时出现的推导后的类型,并且是最窄的推导程度(即到字面量类型的级别)
为了隔离类别与逻辑层,类型查询操作符后不允许使用表达式

const isInputValid = (input: string) => {
    return input.length > 10;
  }
  // 不允许表达式
 let isValid: typeof isInputValid("linbudu");

类型控制流分析

TypeScript 中提供了非常强大的类型推导能力,它会随着你的代码逻辑不断尝试收窄类型,这一能力称之为类型的控制流分析(也可以简单理解为类型推导)。

这么说有点抽象,我们可以想象有一条河流,它从上而下流过你的程序,随着代码的分支分出一条条支流,在最后重新合并为一条完整的河流。在河流流动的过程中,如果遇到了有特定条件才能进入的河道(比如 if else 语句、switch case 语句等),那河流流过这里就会收集对应的信息,等到最后合并时,它们就会嚷着交流: “我刚刚流过了一个只有字符串类型才能进入的代码分支!”   “我刚刚流过了一个只有函数类型才能进入的代码分支!” ……就这样,它会把整个程序的类型信息都收集完毕。

function foo(input: string | number) {
  if (typeof input === "string") {
  }
  if (typeof input === "number") {
  }
  // ...
}

但当判断条件被提取出时,控制流就无法根据提取出的内容进行收缩类型,你可以理解为只能进行从起点到终点进行逻辑的判断,不能跳出。
function isString(input: unknown): boolean {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 类型“string | number”上不存在属性“replace”。
    input.replace("linbudu", "linbudu599");
  }
  if (typeof input === "number") {
  }
  //
}
类型守卫

为了解决上述问题,ts引入is关键字来显示提供类型信息,如下例子:

function isString(input: unknown): input is string {
  return typeof input === "string";
}

function foo(input: string | number) {
  if (isString(input)) {
    // 类型“string | number”上不存在属性“replace”。
    input.replace("linbudu", "linbudu599");
  }
  if (typeof input === "number") {
  }
  // ...
}

上述isString函数使用了is关键字对返回值进行类型约束,由以下组成

  • 函数的某个参数;
  • is string,即 is 关键字 + 预期类型,即如果这个函数成功返回为 true,那么 is 关键字前这个入参的类型,就会被这个类型守卫调用方后续的类型控制流分析收集到。 可以看到,只要函数返回值为真,那么这个变量的类型就会被is关键字后的类型指定,类似于类型断言,但区别于类型断言,类型守卫并不会对逻辑判断与指定类型的关联进行检查。
    除此以外,还可以在类型守卫中使用联合类型、对象类型
export type Falsy = false | "" | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val;

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive =>
  ["string", "number", "boolean", "undefined"].includes(typeof val);
基于in和instanceof的类型保护

对于同名但不同类型的属性,我们需要使用字面量类型的区分,仅凭typeof对类型进行区分不起作用。

function ensureArray(input: number | number[]): number[] {
  if (Array.isArray(input)) {
    return input;
  } else {
    return [input];
  }
}

interface Foo {
  kind: "foo";
  diffType: string;
  fooOnly: boolean;
  shared: number;
}

interface Bar {
  kind: "bar";
  diffType: number;
  barOnly: boolean;
  shared: number;
}

function handle1(input: Foo | Bar) {
  if (input.kind === "foo") {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}
function handle2(input: Foo | Bar) {
  // 报错,并没有起到区分的作用,在两个代码块中都是 Foo | Bar
  if (typeof input.diffType === "string") {
    input.fooOnly;
  } else {
    input.barOnly;
  }
}

我们也可以利用instanceof进行约束

class FooBase {}

class BarBase {}

class Foo extends FooBase {
  fooOnly() {}
}
class Bar extends BarBase {
  barOnly() {}
}

function handle(input: Foo | Bar) {
  if (input instanceof FooBase) {
    input.fooOnly();
  } else {
    input.barOnly();
  }
}
断言守卫

断言守卫使用assert关键字,具体使用如例:

function assert(condition: any, msg?: string): asserts condition {
  if (!condition) {
    throw new Error(msg);
  }
}
const a: string = "123";
assert(typeof a === "string");
console.log(a.length);

也可以配合is关键字进行进一步约束

let name: number = 1;
function assertIsNumber(val: any): asserts val is number {
  if (typeof val !== "number") {
    throw new Error("Not a number!");
  }
}

assertIsNumber(name);

// number 类型!
const a = name.toFixed(1);

这样就可以省去传入的表达式,而是可以将这个判断用的表达式放进断言守卫的内部,来获得更独立地代码逻辑。
区别于类型守卫,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。