TypeScript 查漏补缺

1,748 阅读13分钟

前言

在学习过typescript的入门知识后,作为一个编程小白,在公司原有的项目中增加自己的代码的过程中,还是会不理解大佬们写的代码,所以近一步深入理解TypeScript,解开了我很多疑惑,下面是我的一些个人整理。

声明空间

声明空间包含变量声明空间类型声明空间

类型声明空间

类型声明空间主要是用来注解变量的类型

class Foo {}
interface Bar {}
type Bas = {}

let foo: Foo
let bar: Bar
let bas: Bas

虽然定义了interface Bartype Bas,但是不能作为变量来使用,例如下面的写法

const bar = Bar // “Bar”仅表示类型,但在此处却作为值使用
const bas = Bas // “Bas”仅表示类型,但在此处却作为值使用

出错的原因时候由于BarBas没有定义变量声明空间

变量声明空间

变量声明空间包含了可用做变量的内容,Class Foo提供了一个类型Foo到类型声明空间,还提供了一个Foo到变量声明空间,所以在const foo = Foo时不会出错

interface/type 定义的内容也不能当做变量来使用

相似地,我们也不能使用变量来用作类型注解

const a = 123
let test: a // “a”表示值,但在此处用作类型。是否指“类型 a”?

声明空间会涉及到模块导入问题
导入模块:

import foo = require('foo')

它实际上做了以下两件事情

  • 导入foo模块的所有类型信息
  • 确定foo模块运行时的依赖关系

如果你没有把导入的名称当做变量声明空间来使用,在编译成javascript的时候,导入的模块会被完全移除

import foo from './foo'
// 编译为javascript
// res:

import foo from './foo'
let a: foo
// 编译为javascript
// res: let a

import foo from './foo'
let b = foo
// 编译为javascript
// res: 
// const foo = require('foo')
// let b = foo

使用例子:懒加载

类型推断需要提前完成,但是在某些场景下,我们只想在需要是加载模块,这个时候我只需要在类型注解中使用导入的模块名称,但不在变量中使用。由上面我们知道,导入的模块没有在变量命名空间中使用,在编译成js时,模块将会被移除。接着,我们可以在动态导入时手动加载模块

import foo from './foo';

export function loadFoo() {
  // 这是懒加载 foo,原始的加载仅仅用来做类型注解
  const _foo: typeof foo = require('foo');
  // 现在,你可以使用 `_foo` 替代 `foo` 来作为一个变量使用
}

函数重载

TypeScript允许声明函数重载,请思考下面的代码:

function padding(a: number, b?: number, c?: number, d?: any) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}

代码是根据参数个数的不同来区分不同的逻辑。我们可以使用函数重载来强制和记录这些约束。我们只需要多次声明函数头,最后一个函数头是在函数体内实际处于活动状态但不可用于外部

// 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}

padding(1); // Okay: all
padding(1, 1); // Okay: topAndBottom, leftAndRight
padding(1, 1, 1, 1); // Okay: top, right, bottom, left

padding(1, 1, 1); // Error: Not a part of the available overloads

函数声明 在没有提供函数实现的情况下,有两种声明函数类型的方式

type LongHand = {
  (a: number): number;
};

type ShortHand = (a: number) => number;

但是,当想使用函数重载时,只能使用第一种方式

type LongHandAllowsOverloadDeclarations = {
  (a: number): number;
  (a: string): string;
};

类型保护

typeof

TypeScript能够通过instanceoftypeof来分辨变量类型

function doSome(x: number | string) {
  if (typeof x === 'string') {
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }

  x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}

instanceof

class Foo {
  foo = 123;
  common = '123';
}

class Bar {
  bar = 123;
  common = '123';
}

function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  }
  if (arg instanceof Bar) {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

doStuff(new Foo());
doStuff(new Bar());

in

in也能用来判断类型,具体看下面的例子,是一个校验规则的interface

interface CompanyBasicInfo {
    companyName: string;
    country: string;
    state: string;
    city: string;
    website: string;
    phone: string;
}

interface CustomRule {
    customRule: (...agrs: any[]) => boolean
}

interface MaxRule {
    max: number,
}

interface MinRule {
    min: number
}

interface RequireRule {
    require: boolean
}

export type ValidateRule = (CustomRule | MaxRule | MinRule | RequireRule) & {
    message: string
}

从上面可以看出,ValidateRule有一个固定的message成员,但是不知道它是否包customRulemax等成员,当我们需要判断是哪种规则时,可以使用下面的判断方式

// 直接使用
rule.require // 类型“ValidateRule”上不存在属性“require”。类型“CustomRule & { message: string; }”上不存在属性“require”。

// 需要先进行类型判断后使用
if ('require' in rule) {
   rule.require
}

使用in来进行类型判断时需要特别注意我们自定义的属性名不要跟内置属性冲突

字面量类型保护

上面的示例也可以用字面量的类型来进行判断,但是使用这种方式就需要多一个共同属性kind

interface CustomRule {
    kind: 'custom',
    customRule: (...agrs: any[]) => boolean
}

interface MaxRule {
    kind: 'max',
    max: number,
}

interface MinRule {
    kind: 'min',
    min: number
}

interface RequireRule {
    kind: 'require',
    require: boolean
}

export type ValidateRule = (CustomRule | MaxRule | MinRule | RequireRule) & {
    message: string
}
function judge(rule: ValidateRule) {
    if(rule.kind === 'custom') {
        rule.customRule
    }
}

使用定义的类型保护

JavaScript 并没有内置非常丰富的、运行时的自我检查机制。当你在使用普通的 JavaScript 对象时(使用结构类型,更有益处),你甚至无法访问 instanceof 和 typeof。在这种情景下,你可以创建用户自定义的类型保护函数,这仅仅是一个返回值为类似于someArgumentName is SomeType 的函数,如下:

function isRequire(rule: ValidateRule): rule is RequireRule {
    return (rule as RequireRule).require !== undefined
}

function judge(rule: ValidateRule) {
    if(isRequire(rule) {
        console.log(rule.require)
    }
}

元组使用

这里记录在开发中遇到的一个问题,还是上面的例子

interface CompanyBasicInfo {
    companyName: string;
    country: string;
    state: string;
    city: string;
    website: string;
    phone: string;
}

interface CustomRule {
    customRule: (...agrs: any[]) => boolean
}

interface MaxRule {
    max: number,
}

interface MinRule {
    min: number
}

interface RequireRule {
    require: boolean
}

export type ValidateRule = (CustomRule | MaxRule | MinRule | RequireRule) & {
    message: string
}

export type ValidateRules = { [key in keyof CompanyBasicInfo]: Array<ValidateRule> }
function handle(val) {
    const requiredField = [
        'companyName',
        'country',
        'city',
        'website',
        'phone'
    ]
    const { basicInfo } = this.state // basicInfo: CompanyBasicInfo
    requiredFields.forEach((field)=>{
        judge(field, val)
        // ERROR: 类型“string”的参数不能赋给类型“keyof CompanyBasicInfo”的参数。
        // ERROR: 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "CompanyBasicInfo"。在类型 "CompanyBasicInfo" 上找不到具有类型为 "string" 的参数的索引签名。
    })
}
function judge(field: keyof CompanyBasicInfo, value: string) {...}

出现上面的错误的原因是,requiredField的类型是string[],所以元素具有隐式的any类型,在通过requiredField[key]的方式来取值,由于basicInfo是CompanyBasicInfo类型,只使用接口中的键来取值,所以我们需要像下面这种做法:

const requiredField = [
    'companyName',
    'country',
    'city',
    'website',
    'phone'
] as const

这样子requiredFeild的类型就是readonly ["companyName", "country", "state", "city", "website"]

索引签名

JavaScript 在一个对象类型的索引签名上会隐式调用 toString 方法,而在 TypeScript 中,为防止初学者砸伤自己的脚,它将会抛出一个错误。

const obj = {
  toString() {
    return 'Hello';
  }
};

const foo: any = {};

// ERROR: 类型“{ toString(): string; }”不能作为索引类型使用。
foo[obj] = 'World';

// FIX: TypeScript 强制你必须明确这么做:
foo[obj.toString()] = 'World';

强制用户必须明确的写出 toString() 的原因是:在对象上默认执行的 toString 方法是有害的。例如 v8 引擎上总是会返回 [object Object]

const obj = { message: 'Hello' };
let foo: any = {};

// ERROR: 索引签名必须为 string, number....
foo[obj] = 'World';

// 这里实际上就是你存储的地方
console.log(foo['[object Object]']); // World

TIP: TypeScript的索引签名必须是stringnumber或者symbol

声明一个索引签名

例如:假设你想确认存储在对象中任何内容都符合 { message: string } 的结构,你可以通过 [index: string]: { message: string } 来实现

const foo: {
  [index: string]: { message: string };
} = {};

// 储存的东西必须符合结构
// ok
foo['a'] = { message: 'some message' };

// Error, 必须包含 `message`
foo['a'] = { messages: 'some message' };

// 读取时,也会有类型检查
// ok
foo['a'].message;

// Error: messages 不存在
foo['a'].messages;

注意点

  1. 所有成员都必须符合字符串的索引签名
// ok
interface Foo {
  [key: string]: number;
  x: number;
  y: number;
}

// Error
interface Bar {
  [key: string]: number;
  x: number;
  y: string; // Error: y 属性必须为 number 类型
}

在这里,[key: string]: number会规定所有的成员的key都为string,类型是number,如果有一个成员不满足就会提示错误。这可以给你提供安全性,任何以字符串的访问都能得到相同结果。
如果我们需要实现上述的interface需求,我们应该使用交叉类型来解决,例如下面:

type FieldState = {
  value: string;
};

type FormState = { isValid: boolean } & { [fieldName: string]: FieldState };

捕获类型

TypeScript 类型系统非常强大,它支持其他任何单一语言无法实现的类型流动和类型片段。这是因为 TypeScript 的设计目的之一是让你无缝与像 JavaScript 这类高动态的语言一起工作。

复制类型和值

如果你想移动一个类,你可能会想要做以下事情:

class Foo {}

const Bar = Foo;

let bar: Bar; // Error: “Bar”表示值,但在此处用作类型。是否指“类型 Bar”?

这会得到一个错误,因为 const 仅仅是复制了 Foo 到一个变量声明空间,因此你无法把 Bar 当作一个类型声明使用。正确的方式是使用 import 关键字,请注意,如果你在使用 namespace 或者 modules,使用 import 是你唯一能用的方式:

namespace importing {
  export class Foo {}
}

import Bar = importing.Foo;
let bar: Bar; // ok

这个 import 技巧,仅适合于类型和变量。

捕获变量的类型

你可以通过 typeof 操作符在类型注解中使用变量。

let foo = 123;
let bar: typeof foo; // 'bar' 类型与 'foo' 类型相同(在这里是: 'number')

bar = 456; // ok
bar = '789'; // Error: 'string' 不能分配给 'number' 类型

捕获类成员的类型

与捕获变量的类型相似,你仅仅是需要声明一个变量用来捕获到的类型:

class Foo {
  foo: number; // 我们想要捕获的类型
}

declare let _foo: Foo;

// 与之前做法相同
let bar: typeof _foo.foo;

捕获字符串类型

许多 JavaScript 库和框架都使用原始的 JavaScript 字符串,你可以使用 const 定义一个变量捕获它的类型:

// 捕获字符串的类型与值
const foo = 'Hello World';

// 使用一个捕获的类型
let bar: typeof foo;

// bar 仅能被赋值 'Hello World'
bar = 'Hello World'; // ok
bar = 'anything else'; // Error

捕获键的名称

keyof 操作符能让你捕获一个类型的键。例如,你可以使用它来捕获变量的键名称,在通过使用 typeof 来获取类型之后:

  1. 捕获变量对象的键
const color = {
    red: 'red',
    blue: 'blue'
}
type Colors = keyof typeof color // type Colors = "red" | "blue"
  1. 捕获接口的键值
interface item {
    color: Colors,
    len: number
}
type Item = keyof item

内置的工具类型

Document - Utility Types

Partial

Partial<T>的作用就是将某个类型里的属性全部变为可选项?

type Partial<T> = {
    [P in keyof T]?: T[P];
};

首先通过keyof获取T上的所有属性,然后进行遍历,加上?使得所有的属性变成可选。这个我们在setState见过:

export const setState = (account: Partial<State>): Action => ({
  type: SET_STATE,
  payload: account});

Required

Required<T>的作用就是将某个类型里的属性全部变为必选项

type Required<T> = {
    [P in keyof T]-?: T[P];
};

以上代码中,-?就是移除了可选?

interface Props {
a?: number;
b?: string;
}

const obj: Props = { a: 5 }; // OK
const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing

Readonly

Readonly<T> 的作用是将某个类型所有属性变为只读属性,也就意味着这些属性不能被重新赋值。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Record

Record<Keys, Type>构造一个对象类型,其属性键为Keys,其属性值为Type。可用于将一种类型的属性映射到另一种类型

type Record<K extends keyof any, T> = {
    [P in K]: T
}
interface CatInfo {
    age: number;
    breed: string;
}

type CatName = "miffy" | "boris" | "mordred";
const cats: Record<CatName, CatInfo> = {
    miffy: { age: 10, breed: "Persian" },
    boris: { age: 5, breed: "Maine Coon" },
    mordred: { age: 16, breed: "British Shorthair" },
};

Pick

Pick<T, K extends keyof T>的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型

type Pick<T, K extend keyof T> = {
    [P in K]: T[P]
}
interface Todo {
    title: string;
    description: string;
    completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
    title: "Clean room",
    completed: false,
};

Omit

Omit<T, K extends keyof T>的作用是将某个类型中的子属性挑出来去除,变成包含这个类型部分属性的子类型

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Exclude

Exclude<UnionType, ExcludedMembers>的作用是将某个类型中属于另一个的类型移除掉。

type Exclude<T, U> = T extends U ? never : T;

如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。最终实现的效果就是将 T 中某些属于 U 的类型移除掉。

type T0 = Exclude<"a" | "b" | "c", "a">;
// type T0 = "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T1 = "c"

type T2 = Exclude<string | number | (() => void), Function>;
// type T2 = string | number

Extract

Extract<T, U> 的作用是从 T 中提取出 U

type Extract<T, U> = T extends U ? T : never;

如果 T 能赋值给 U 类型的话,那么就会返回 T 类型,否则返回 never 类型。

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void

ReturnType

ReturnType<T> 的作用是用于获取函数 T 的返回类型。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
type T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Error
let func = () => {}
type T8 = ReturnType<typeof func> // 不使用typeof会报:“func”表示值,但在此处用作类型。

Parameters

Parameters<T> 的作用是用于获得函数的参数类型组成的元组类型

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any 
  ? P : never;
type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]
let func = () => {}
type E = Parameters<typeof func> // 不使用typeof会报:“func”表示值,但在此处用作类型。

NonNullable

NonNullable<T> 的作用是用来过滤类型中的 null 及 undefined 类型。

type NonNullable<T> = T extends null | undefined ? never : T;
type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

InstanceType

InstanceType 的作用是获取构造函数类型的实例类型。

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
class C {
x = 0;
y = 0;
}

type T0 = InstanceType<typeof C>; // C
type T1 = InstanceType<any>; // any
type T2 = InstanceType<never>; // any
type T3 = InstanceType<string>; // Error
type T4 = InstanceType<Function>; // Error

let c = nenw C()
type T5 = typeof c // C

ThisType

ThisType<T> 的作用是用于指定上下文对象的类型。

注意:使用 ThisType<T> 时,必须确保 --noImplicitThis 标志设置为 true。

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

const obj: ThisType<Person> = {
  dosth() {
    this.name // string
  }
}

ConstructorParameters

ConstructorParameters<T> 的作用是提取构造函数类型的所有参数类型。它会生成具有所有参数类型的元组类型(如果 T 不是函数,则返回的是 never 类型)。

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
type A = ConstructorParameters<ErrorConstructor>; // [(string | undefined)?]
type B = ConstructorParameters<FunctionConstructor>; // string[]
type C = ConstructorParameters<RegExpConstructor>; // [string, (string | undefined)?]

参考文章