TypeScript专题—基础解析

251 阅读22分钟

1、数据类型

【1】基础数据类型

基础数据类型包括:boolean/number/string/array/tuple/enum/void/null & undefined/any & unknown/never。

any和unknown的区别:任何类型都能分配给unknown,但unknown不能分配给其他基本类型,而any啥都能分配和被分配。

// 布尔值
let isBoolean: boolean = true;
// 数值
let decLiteral: number = 1234567;
let hexLiteral: number = 0xf0ac;
let notANumber: number = NaN;
let infinity: number = Infinity;
// 字符串
let name: string = 'sam';
// Undefined和Null
let u: undefined = undefined;
let n: null = null;
// unknown
let foo: unknown
foo = true // ok
foo = 123 //ok
foo.toFixed(2) // error
let foo1: string = foo // error
// any
let bar: any
bar = true // ok
bar = 123 //ok
foo.toFixed(2) // ok
let bar1:string = bar // ok
// never 表示用户无法达到的类型
function neverReach(): never {
  throw new Error('an error')
}
const x = 2
neverReach()
x.toFixed(2)  // x is unreachable

unknown的正确用法:我们可以通过不同的方式将unknown类型缩小为更具体的类型范围,这个过程叫类型收窄。

function getLen(value: unknown): number {
  if (typeof value === 'string') {
    // 因为类型保护的原因,此处value被判断为string类型
    return value.length
  }
  return 0
}

never 还可以用于联合类型的幺元:

type T0 = string | number | never // T0 is string | number

【2】对象类型

在TypeScript中,我们使用interface来定义对象类型。

【2.1.1】确定属性

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

let sam: IPerson = {
    name: 'sam',
    age: 20
};

上面的例子中,我们定义了一个接口IPerson ,然后定义了一个变量sam ,变量的类型是IPerson,这样就约束了 sam的形状必须和接口IPerson一致。

定义的变量sam比接口多或者少属性都是会报错的。

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

// 错误(比接口少了个age属性)
let sam1: IPerson = {
    name: 'sam'
};

// 错误(比接口多了个gender属性)
let sam2: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

// 正确
let sam3: IPerson = {
    name: 'sam',
    age: 20
};

注意:为了良好的编写习惯,建议接口的名称加上I前缀。

【2.1.2】可选属性

在接口上某个属性加个? ,表明不需要强制匹配该属性。

interface IPerson {
    name: string;
    age?: number;
}

// 正确
let sam1: IPerson = {
    name: 'sam'
};

// 错误(此时我们还是不能在接口上添加未定义的属性)
let sam2: IPerson = {
    name: 'sam',
    gender: 'man'
};

【2.1.3】任意属性

在需要添加任意属性的接口使用[propName: string], 一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。

interface IPerson {
    name: string;
    // 此时的age类型为number,不是string类型的子集
    age?: number;
    // 确定属性和可选属性的类型都必须是string类型的子集
    [propName: string]: string;
}

// 错误(此时的age类型为number,不是string类型的子集)
let sam1: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

一个接口中只能定义一个任意属性,如果接口中有多个类型的属性,则可以在任意属性中使用联合类型。

interface IPerson {
    name: string;
    age?: number;
    // 此时确定属性和可选属性的类型都必须是string或者number类型的子集
    [propName: string]: string | number;
}

// 正确
let sam1: IPerson = {
    name: 'sam',
    age: 20,
    gender: 'man'
};

// 正确
let sam2: IPerson = {
    name: 'sam',
    gender: 'man'
};

// 正确
let sam3: IPerson = {
    name: 'sam'
};

// 错误,任意属性未添加boolean类型
let sam4: IPerson = {
    name: 'sam',
    rich: false
};

【2.1.4】只读属性

对象中的一些字段只能在创建的时候被赋值,后续无法更改。

interface IPerson {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: string | number;
}

let sam: IPerson = {
    id: 123,
    name: 'Tom',
    gender: 'male'
};

// 错误,此时不能再次修改id的值
sam.id = 9527;

2、类型推论

如果没有明确的指定类型,那么TypeScript会依照类型推论的规则推断出一个类型。

// 基本类型推断
let hello = 'hello typescript' // ts推导出hello是string类型
// 对象类型推断
const myObj = { // ts推断出myObj: { x: number; y: string; z: boolean; }
  x: 1,
  y: '2',
  z: true
}
// 函数类型推断
function len(str: string) {// ts推导出函数返回值是number类型
  return str.length
}
// 上下文类型推断
const xhr = new XMLHttpRequest()
xhr.onload = function(event) {}// ts推导出event是ProgressEvent类型

如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成any类型。

let hello
hello = 'string2'
hello = 2
// 等价于
let hello: any
hello = 'string2'
hello = 2

3、联合类型

有时候我们希望声明一个变量时候包含多个类型,那么我们可以使用 | 分隔每个类型。

let hello: string | number = 'hello typescript'
hello = 2
console.log(hello)

这样就表明hello参数接受string和number类型。

4、交叉类型

交叉类型是将多个类型合并为一个类型。

interface iName {
  firstName: string;
  lastName: string;
}

interface iBaseInfo {
  sex: 'male' | 'female';
  age: number;
}

type iUserInfo = iName & iBaseInfo;
const user: iUserInfo = {
  firstName: 'Jack',
  lastName: 'Ma',
  sex: 'male',
  age: 40,
};

当类型存在冲突的时候,成员之间会继续合并:

interface iProps1 {
  size: string;
}

interface iProps2 {
  size: number;
}

type iProps = iProps1 & iProps2;

// 等价于
type iProps = {
  size: string & number; // never
};

let props: iProps = {
  size: 'ddd', // 编辑器报红
};

5、数组的类型

在TypeScript中,数组类型有多个定义的方式。

【1】直接定义

let arr1: number[] = [1, 2, 3, 4, 5];
let arr2: string[] = ["one", "two", "third", "four", "five"];
let arr3: any[] = [1, "two", false, "four", 5];

// 错误(two为string类型)
let arr1: number[] = [1, 'two', 3, 4, 5];
let arr2: number[] = [1, 2, 3, 4, 5];
// 错误(定义后的arr,push一个string类型是错误的)
arr2.push("six")

【2】数组泛型

用数组泛型Array<elemType>来表示数组。

let arr1: Array<number> = [1, 2, 3, 4, 5];
let arr2: Array<string> = ["one", "two", "third", "four", "five"];
let arr3: Array<string | number | boolean> = [1, "two", false, "four", 5];
let arr4: Array<any> = [1, "two", false, "four", 5];

6、函数的类型

【1】函数声明

在TypeScript声明函数时,我们需要把函数的输入和输出都要考虑在内。

function sum1 (x: number, y: number): number {
    return x + y;
}

// 错误
sum(1);
sum(1, 2, 3);

// 正确
sum(1, 2);

可见,定义好的函数,如果输入了多的或者少的参数都是不被TypeScript允许的。

【2】函数表达式

let sum2: (x: number, y: number) => number = function(x: number, y: number): number {
    return x + y
}

注意:在TypeScript的类型定义中的=>用来表示函数的定义,箭头左边是输入类型,箭头右边是输出类型,不要和 ES6的箭头函数混淆。

【3】可选参数

和接口的可选属性相似,我们也可以用?来给函数表示可选的参数。

function sum3 (x:number, y?: number) {
    return x + y
}

// 用法
sum3(1,2)
sum3(1)

注意:可选参数后面不允许放必须参数,因为这样调用的时候无法识别,除非添加参数默认值,可无视这个限制。

// 错误写法(可选参数y后面跟着必须参数z)
function sum3 (x:number, y?:number, z: number) {
    return x + y + z
}

// 给可选参数y添加参数默认值
function sum4 (x:number, y:number = 1, z: number) {
    return x + y + z
}

【4】函数重载

函数重载的意义在于能够让你知道传入不同的参数得到不同的结果,如果传入的参数不同,但是得到的结果类型却相同,那么这里就不要使用函数重载。

函数重载的基本语法:

declare function test(a: number): number
declare function test(a: string): string

const resS = test('Hello World'); // resS被推断出类型为string
const resN = test(1234); // resN被推断出类型为number

只是参数个数的区别可以使用可选参数来代替:

function func (a: number): number
function func (a: number, b: number): number

// 等价于
function func (a: number, b?: number): number

只是参数类型的区别可以使用联合类型来代替:

function func (a: number): number
function func (a: string): number
 
// 等价于
function func (a: number | string): number

7、类型兼容性

typescript的子类型是基于结构子类型的,只要结构可以兼容,就是子类型。

【1】对象子类型

子类型中必须包含源类型所有的属性和方法:

function getPointX(point: { x: number }) {
  return point.x
}

const point = {
    x: 1,
  y: '2'
}
getPointX(point) // OK

注意: 如果直接传入一个对象字面量是会报错的,因为当传入的参数是一个对象字面量时,会进行额外属性检查。

function getPointX(point: { x: number }) {
  return point.x
}

getPointX({ x: 1, y: '2' }) // error

【2】函数子类型

如果我们现在有三个类型Animal、Dog、WangCai,存在关系:WangCai ≼ Dog ≼ Animal,那么Animal => WangCai是Dog => Dog的子类型。对于函数类型来说,函数参数的类型兼容是反向的,我们称之为逆变 ,返回值的类型兼容是正向的,称之为协变。

class Animal {
  sleep: Function
}

class Dog extends Animal {
  bark: Function
}

class WangCai extends Dog {
  dance: Function
}

其实函数的参数可以转化为Tuple的类型兼容性:

type Tuple1 = [string, number]
type Tuple2 = [string, number, boolean]

let tuple1: Tuple1 = ['1', 1]
let tuple2: Tuple2 = ['1', 1, true]

let t1: Tuple1 = tuple2 // ok
let t2: Tuple2 = tuple1 // error

8、类型保护

【1】is关键词

function isString(value: unknown): value is string {
  return Object.prototype.toString.call(value) === '[object String]';// 返回值是一个boolean
}

function fn (x: string | number) {
  if (isString(x)) {
    return x.length
  } else {
    // .....
  }
}

【2】typeof关键词

typeof关键词能够帮助ts判断出变量的基本类型。

function fn (x: string | number) {
  if (typeof x === 'string') { // x is string
    return x.length
  } else { // x is number
    // .....
  }
}

typeof关键词除了做类型保护,还可以从实现推出类型:

function fn(x: string) {
  return x.length
}

const obj = {
  x: 1,
  y: '2'
}

type T0 = typeof fn // (x: string) => number
type T1 = typeof obj // {x: number; y: string }

【3】instanceof关键词

instanceof关键词能够帮助ts判断出构造函数的类型。

function fn1 (x: XMLHttpRequest | string) {
  if (x instanceof XMLHttpRequest) { // x is XMLHttpRequest
    return x.getAllResponseHeaders()
  } else { // x is string
    return x.length
  }
}

【4】针对null和undefined的类型保护

在条件判断中,ts会自动对null和undefined进行类型保护。

function fn2 (x?: string) {
  if (x) {
    return x.length
  }
}

【5】针对null和undefined的类型断言

如果我们已经知道的参数不为空,可以使用!来手动标记。

function fn2 (x?: string) {
  return x!.length
}

9、类型断言

用来手动指定一个值的类型叫做类型断言。

【1】<类型>值

无法在tsx文件使用,而且容易和泛型混淆。

let str: string = "this is a string";
let strLength1: number = (<string>str).length;

【2】值 as 类型

let str: string = "this is a string";
let strLength2: number = (str as string).length;

当TypeScript不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

function getLength(str: string | number): number {
    // 错误,因为 number 类型没有 length 方法
    return str.length;
}

为此,我们需要用到类型断言:

function getLength(str: string | number): number {
    if (typeof str === 'number') {
        // 此时将 str 的类型判断为 number 类型,可以使用 number 类型的属性和方法
        return (str as number).toString().length
    } else if (typeof str === 'string') {
        // 此时将 str 的类型判断为 string 类型,可以使用 string 类型的属性和方法
        return (str as string).length
    } else {
        throw 'error'
    }
}

类型断言的常见用途有以下几种:

  • 将一个联合类型断言为其中一个类型
interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}
  • 将一个父类断言为更加具体的子类
interface ApiError extends Error {
    code: number;
}
interface HttpError extends Error {
    statusCode: number;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}
  • 将任何一个类型断言为any
(window as any).foo = 1;
  • 将any断言为一个具体的类型
function getCacheData(key: string): any {
    return (window as any).cache[key];
}

interface Cat {
    name: string;
    run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

双重断言:既然任何类型都可以被断言为any,any又可以被断言为任何类型,那么就可以使用双重断言as any as X来将任何一个类型断言为任何另一个类型。但使用双重断言十有八九是非常错误的,因为它可能会导致运行时错误。

interface Cat {
    run(): void;
}
interface Fish {
    swim(): void;
}

function testCat(cat: Cat) {
    return (cat as any as Fish);
}

10、常量断言

TS会区别对待可修改和不可修改的值的类型推断:immutableString会被推断成单值类型'Acid Mother Temple',而mutableString则会被推断成通用的string类型。

const immutableString = 'Acid Mother Temple';
let mutableString = 'Robert Fripp';

而在一般的对象中,由于对象的属性都具有可修改性,TS都会对它们从宽类型推断:prop的类型被推断为string。

const obj = {
  prop: 'David Bowie'
};

这样的类型推断策略在大部分的情形下比较通用,但在个别情形下会显得有些棘手。例如我们想实现一个React中的自定义Hook。这个Hook能通过 Ref 维护一个状态。它的返回值与useState类似是一个元组:第一项是该状态的值,第二项是该状态的setter。

import { useRef } from 'react';

const useRenderlessState = <S>(initialState: S) => {
  const stateRef = useRef(initialState);
  const state = stateRef.current;
  const setState = (nextState: S) => stateRef.current = nextState;
  return [state, setState];
}

此时我们会发现上面Hook的返回值的类型被推导成了:(S | ((nextState: S) => S))[]

// 组件中使用
const [value, setValue] = useRenderlessState(1);

上面的value和setValue都被推导成了number | (nextState: number) => number的联合类型。

之前我们只能通过对输出值的声明或者断言来明确Hooks的返回值类型是元组而不是数组:

// 声明的做法
const useRenderlessState = <S>(initialState: S): [S, (nextValue: S) => S] => {/*...*/}

// 断言的做法
const useRenderlessState = <S>(initialState: S) => {
  return [state, setState] as [typeof value, typeof setValue];
}

上面的两种写法都各有冗余成分,从语义层面来分析,TS之所以没能将返回值推断为元组类型是因为它认为该返回值仍有可能被push 值,被修改。所以我们真正需要做的是告诉TS,这个返回值是一个final,其本身和属性都是不可篡改的,而这正是常量断言所做的事。

常量断言可以把一个值标记为一个不可篡改的常量,从而让TS以最严格的策略来进行类型推断:

const useRenderlessState = <S>(initialState: S) => {
  return [state, setState] as const;
}

这下useRenderlessState的返回类型就被推断成了readonly [S, (nextState: S) => S]。

as const中的 const与我们声明常量时使用的const的区别:

  • const常量声明是ES6的语法,对TS而言,它只能反映该常量本身是不可被重新赋值的,它的子属性仍然可以被修改,故TS只会对它们做松散的类型推断。
  • as const是TS的语法,它告诉TS它所断言的值以及该值的所有层级的子属性都是不可篡改的,故对每一级子属性都会做最严格的类型推断。

常量断言可以让我们不需要enum关键字就能定义枚举对象:

const EnvEnum = {
  Development: 'dev',
  Production: 'prod',
  Testing: 'test',
} as const

11、枚举

枚举是TypeScript对JavaScript标准数据类型的补充,不同于对象定义的key-value中,只能通过key去访问value的值,在enum中,既可以通过key访问value的值,也可以通过value访问key的值。

【1】简单定义

若没给枚举的成员赋值,那么会默认从0开始递增。

enum Person {
    Male, // 0
    Female // 1
}

当然也可以给枚举手动赋值:未赋值的枚举会接着上一个枚举项递增

enum Person {
    Male = 7,
    Female      // 此时Female会为8
}

上面提到的都是常数项,其实枚举还有一个类型叫计算所得项,如果计算所得项后面是没有赋值的项,则会报错。

enum Person {
    Male,                    // 常数项
    Female = "female".length // 计算所得项
}

// 错误
enum Person {
    Male = "man".length,
    Female
}

// 正确
enum Person {
    Male = "man".length,
    Female = 8
}

【2】常数枚举

常数枚举是用const enum定义的,常数枚举在编译的阶段会被删除,既在编译后的文件不存在编译后的常数枚举,且不能包含计算成员。

// 正确
const enum Person {
    Male,
    Female
}

// 错误
const enum Person2 {
    Male,
    Female = "female".length
}

let person = [Person.Male, Person.Female]

【3】外部枚举

外部枚举是用declare enum定义的,外部枚举用来描述已经存在的枚举类型的形状。

declare enum Person {
    Male,
    Female
}

let person = [Person.Male, Person.Female]

12、元组Tuple

元组合并了不同类型的对象,需要以元组所定义的顺序预定数据类型。

let tuple1: [string, number];

// 正确
tuple1 = ["one", 2];

// 错误
tuple1 = ["one", "two"];
tuple1 = [1, 2];
tuple1 = ["one", 2, 3]; // 未在元组中定义
tuple1 = [true, 2]      // 元组在index为0中只接受string类型

13、泛型

有时候,我们希望返回值的类型与传入参数的类型是相同的,因此有了泛型。

function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString"); // 此时的T为string类型
// 类型推论
let output2 = identity("myString"); // 此时的T为string类型
let out2 = identity(123) // 此时的T为number类型
let out3 = identity(true) // 此时的T为boolean类型

function createList<T>(): T[] {
  return [] as T[]
}

const numberList = createList<number>() // number[]
const stringList = createList<string>() // string[]

有了泛型的支持,identity方法可以传入一个类型变量T,T会帮助我们捕获用户传入的类型,之后我们就可以使用这个类型当做返回值类型,createList方法可以传入一个类型,返回有类型的数组。

【1】泛型约束

泛型T默认把arg参数当作是任意或者所有类型,而number类型没有length属性,所以会报错。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

我们可以把泛型变量T当做类型的一部分使用,而不是整个类型:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // arg
    return arg;
}

如果只希望createList函数只能生成指定类型的数组,可以使用extends关键词来约束泛型的范围和形状:

// 表示该类型有length属性
type Lengthwise = {
  length: number
}

function createList<T extends number | Lengthwise>(): T[] {
  return [] as T[]
}

const numberList = createList<number>() // ok
const stringList = createList<string>() // ok
const arrayList = createList<any[]>() // ok
const boolList = createList<boolean>() // error

【2】泛型接口

我们把上面例子的对象字面量换位接口。

interface Iidentity {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: Iidentity = identity;

【3】泛型类

与泛型接口类似,泛型也可以用于类的类型定义中。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

【4】条件控制

extends除了做约束类型,还可以做条件控制,相当于与一个三元运算符。

表达式:T extends U ? X : Y

含义:如果T可以被分配给U,则返回X,否则返回Y。一般条件下,如果T是 U 的子类型,则认为T可以分配给U。

type IsNumber<T> = T extends number ? true : false
type x = IsNumber<string>  // false

【5】映射类型

// 将T中的所有属性变成可选
type Partial<T> = { [P in keyof T]?: T[P] };

// 将T中的所有属性变成只读
type Readonly<T> = { readonly [P in keyof T]: T[P]; };

// 选择T中可以赋值给U的类型
type Pick<T, U extends keyof T> = {
  [P in U]: T[P];
};

// 将K中所有的属性的值转化为T类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

// 从T中剔除可以赋值给U的类型
type Exculde<T, U> = T extends U ? never : T;

// 提取T中可以赋值给U的类型
type Extract<T, U> = T extends U ? T : never;

// 从对象T中排除key是K的属性
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>

// 从T中剔除null和undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// 获取函数返回值类型
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

// 获取构造函数类型的实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

// 返回类型为T的函数的参数类型所组成的数组
type Parameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

【6】extends条件分发

对于T extends U ? X : Y 来说,还存在一个特性,当T是一个联合类型时,会进行条件分发。

type Union = string | number
type isNumber<T> = T extends number ? 'isNumber' : 'notNumber'
type UnionType = isNumber<Union> // 'notNumber' | 'isNumber'

// 等价于
(string extends number ? 'isNumber' : 'notNumber') | (number extends number ? 'isNumber' : 'notNumber')

Exclude和Extract就是基于此特性,再配合never幺元的特性实现的:

// 从T中剔除可以赋值给U的类型
type Exculde<T, U> = T extends U ? never : T;

// 提取T中可以赋值给U的类型
type Extract<T, U> = T extends U ? T : never;

type T1 = Exclude<string | number | boolean, string | boolean>  // number
type T2 = Extract<string | number | boolean, string | boolean>  // string | boolean

【7】infer关键词

infer可以对运算过程中的类型进行存储,ReturnType、InstanceType和Parameters就是基于此特性实现的:

// 获取函数返回值类型
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;

// 获取构造函数类型的实例类型
type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

// 返回类型为T的函数的参数类型所组成的数组
type Parameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

type Fn = (str: string) => number
type FnReturn = ReturnType<Fn> // number

14、模块

【1】全局模块和文件模块

默认情况下,我们所写的代码是位于全局模块下的:

const foo = 2;

此时,如果我们创建了另一个文件,并写下如下代码,ts认为是正常的:

const bar = foo; // ok

如果要打破这种限制,只要文件中有import或者export表达式即可:

export const bar = foo // error

【2】模块解析策略

Tpescript有两种模块的解析策略:Node和Classic。当tsconfig.json中module 设置成AMD、System、ES2015时,默认为classic ,否则为Node ,也可以使用moduleResolution手动指定模块解析策略。

两种模块解析策略的区别在于,对于下面模块引入来说:

import moduleB from 'moduleB';

Classic模式的路径寻址:

/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts

Node模式的路径寻址:

/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts

/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (如果指定了"types"属性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts

/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (如果指定了"types"属性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts

15、声明文件

声明文件以.d.ts 结尾,用来描述代码结构,一般用来为js库提供类型定义。当用npm安装了某些包并使用的时候,会出现这个包的语法提示,这个语法提示就是声明文件的功劳了。

type CancelFn = () => void;
type RequestCallback = (error: Error | null, data: any) => void;

interface Options {
    param?: string;
    prefix?: string;
    name?: string;
    timeout?: number;
}

declare function jsonp(url: string, options?: Options, cb?: RequestCallback): CancelFn;
declare function jsonp(url: string, callback?: RequestCallback): CancelFn;

export = jsonp;

编辑器是怎么找到这个声明文件:

  • 如果这个包的根目录下有一个index.d.ts,那么这就是这个库的声明文件了。
  • 如果这个包的package.json中有types或者typings字段,那个该字段指向的就是这个包的声明文件。

typescript官方提供了一个声明文件仓库,尝试使用@types前缀来安装某个库的声明文件:

npm i @types/lodash;

当引入lodash的时候,编辑器也会尝试查找node_modules/@types/lodash来为你提供lodash的语法提示。

还有一种就是自己写声明文件,编辑器会收集项目本地的声明文件,如果某个包没有声明文件,你又想要语法提示,就可以自己在本地写个声明文件:

// types/lodash.d.ts
declare module "lodash" {
  export function chunk(array: any[], size?: number): any[];
  export function get(source: any, path: string, defaultValue?: any): any;
}

如果源代码是用ts写的,在编译成js的时候,只要加上-d 参数,就能生成对应的声明文件。

【1】扩展原生对象

写过ts的小伙伴有这样的疑惑,我该如何在window对象上自定义属性呢?

window.myprop = 1 // error

默认window上是不存在myprop这个属性的,所以不可以直接赋值,可以输用方括号赋值语句,但是get操作时也必须用[] ,并且没有类型提示。

window['myprop'] = 1 // OK
window.myprop  // 类型“Window & typeof globalThis”上不存在属性“myprop”
window['myprop'] // ok,但是没有提示,没有类型

此时可以使用声明文件扩展其他对象,在项目中随便建一个xxx.d.ts:

// index.d.ts
interface Window {
  myprop: number
}

// index.ts
window.myprop = 2  // ok

也可以在模块内部扩展全局对象:

import A from 'moduleA';

window.myprop = 2;
declare global {
  interface Window {
    myprop: number
  }
}

【2】扩展其他模块

ts提供了declare module 'xxx' 的语法来扩展其他模块,这非常有利于一些插件化的库和包。

// vue-router/types/vue.d.ts
import Vue from 'vue';
import VueRouter, { Route, RawLocation, NavigationGuard } from './index';

declare module 'vue/types/vue' {
  interface Vue {
    $router: VueRouter
    $route: Route
  }
}

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    router?: VueRouter
    beforeRouteEnter?: NavigationGuard<V>
    beforeRouteLeave?: NavigationGuard<V>
    beforeRouteUpdate?: NavigationGuard<V>
  }
}

【3】处理非js文件的引入

  • 处理vue文件

对于所有以.vue 结尾的文件,可以默认导出Vue类型:

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}
  • 处理css in js

对于所有的.css,可以默认导出一个any类型的值,这样可以解决报错问题,但是丢失了类型检查:

declare module '*.css' {
    const content: any;
  export default content;
}

import * as React from 'react';
import * as styles from './index.css';

const Error = () => (
    <div className={styles.centered}>
        <div className={styles.emoji}>😭</div>
        <p className={styles.title}>Ooooops!</p>
        <p>This page doesn't exist anymore.</p>
    </div>
)

export default Error;

16、编译

ts内置了一个compiler(tsc),可以让我们把ts文件编译成js文件,配合众多的编译选项,有时候不需要babel我们就可以完成大多数工作。

【1】常用的编译选项

tsc在编译ts代码的时候,会根据tsconfig.json配置文件的选项采取不同的编译策略。

  • target - 生成的代码的JS语言的版本,比如ES3、ES5、ES2015等。
  • module - 生成的代码所需要支持的模块系统,比如es2015、commonjs、umd等。
  • lib - 告诉TS目标环境中有哪些特性,比如 WebWorker、ES2015、DOM等。

【2】更改编译后的目录

tsconfig中的outDir字段可以配置编译后的文件目录,有利于dist的统一管理。

{
  "compilerOptions": {
    "module": "umd",
    "outDir": "./dist"
  }
}

【3】编译后输出到一个js文件中

对于amd和system模块,可以配置tsconfig.json中的outFile字段,输出为一个js文件。如果需要输出成其他模块,例如umd ,又希望打包成一个单独的文件,需要怎么做?可以使用rollup或者webpack:

// rollup.config.js
const typescript = require('rollup-plugin-typescript2')

module.exports = {
  input: './index.ts',
  output: {
    name: 'MyBundle',
    file: './dist/bundle.js',
    format: 'umd'
  },
  plugins: [
    typescript()
  ]
}