TS学习

153 阅读11分钟

TS配置文件(编译时读取的配置文件)

如果一个目录下存在tsconfig.json,那么当前目录就是TS的根目录。

  • 一般情况下调用tsc时,编译器会从当前目录逐级向上查找tsconfig.json文件。
  • 也可使用 -p 指定一个tsconfig.json。

tsconfig.json示例

{
    "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}

声明文件(描述文件)

作用

通俗来讲,在TS中以 .d.ts 为后缀的文件,我们称之为TS的声明文件。它的主要作用是描述JS模块内所有导出接口的类型信息。

什么时候需要声明文件

一般情况下,是不需要我们去编写声明文件的,如果我们文件本身使用TS 编写的,那么在编译的时候让TS 自动生成声明文件,并在发布的时候将 .d.ts 文件一起发布即可。

手动定义声明文件(三种情况)
  • 通过CDN引入的工具包,挂在了一些全局方法,如果在TS中直接使用的话,会报TS语法错误,此时就需要我们对这些全局方法进行TS声明。

  • 使用第三方 npm 包,但是没有提供声明文件。

  • 第三方 npm 包如果有提供声明文件的话,一般会呈一下两种方式存在:

    1. @types/xxx --- 一般是一些使用量比较高的库会提供。可以通过 npm i @type/xxx 安装。
    2. 在源代码中提供 .d.ts 声明文件。

    如果两种都不存在的话,那就需要我们自己定义了。

如何让TS编译时自动生成声明文件(.d.ts)

在tsconfig.json里如下配置即可:

{
  "compilerOptions": {
    "declaration": true
  }
}

TS规范

普通类型

String, Number, Boolean, Object

禁止使用如下类型 String, Number, Boolean, Object。这些类型非原始的装盒对象。

/*错误用法*/
function reverse(s: String): String;

应该使用 number, string, boolean

/*正确用法*/
function reverse(s: string): string;

使用非原始的objcet 类型来代替 Object

泛型

不要定义一个从来没使用过其类型参数的泛型类型。

回调函数类型

回调函数返回值类型

不要为返回值被忽略的函数设置一个any类型的返回值类型

/*错误*/
function fn(x: () => any) {
    x();
}

应该给返回值被忽略的回调函数设置void类型的返回值类型:

/* OK */
function fn(x: () => void) {
    x();
}

为什么:使用void相对安全,因为它防止了你不小心使用x的返回值:

function fn(x: () => void) {
    var k = x(); // oops! meant to do something else
    k.doSomething(); // error, but would be OK if the return type had been 'any'
}

回调函数里的可选参数

不要在回调函数里使用可选参数,除非有必要

/* 错误 */
interface Fetcher {
    getObject(done: (data: any, elapsedTime?: number) => void): void;
}

应该写出回调函数的非可选参数:

/* 正确 */
interface Fetcher {
    getObject(done: (data: any, elapsedTime: number) => void): void;
}

重载与回调函数

不要因为回调函数的参数个数不同而写不同的重载

/* 错误 */
declare function beforeAll(action: () => void, timeout?: number): void;
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

应该只是用最大参数个数写一个重载:

/* 正确 */
declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;

为什么:回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。 参数少的回调函数首先允许错误类型的函数被传入,因为它们匹配第一个重载。

函数重载

顺序

不要把一般的重载放在精确的重载前面

/* 错误 */
declare function fn(x: any): any;
declare function fn(x: HTMLElement): number;
declare function fn(x: HTMLDivElement): string;
​
var myElem: HTMLDivElement;
var x = fn(myElem); // x: any, wat?

应该排序重载令精确的排在一般的之前:

/* 正确 */
declare function fn(x: HTMLDivElement): string;
declare function fn(x: HTMLElement): number;
declare function fn(x: any): any;
​
var myElem: HTMLDivElement;
var x = fn(myElem); // x: string, :)

为什么:TypeScript会选择第一个匹配到的重载当解析函数调用的时候。 当前面的重载比后面的“普通”,那么后面的被隐藏了不会被调用。

使用可选参数

不要在末尾参数不同时,写不同的重载:

/* 错误 */
interface Example {
    diff(one: string): number;
    diff(one: string, two: string): number;
    diff(one: string, two: string, three: boolean): number;
}

应该尽可能的使用可选参数:

/* 正确 */
interface Example {
    diff(one: string, two?: string, three?: boolean): number;
}

注意: 这在所有重载都有相同类型的返回值时会不好用。

主要有以下两个原因:

  1. TypeScript解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用, 且允许外来参数。 下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时
function fn(x: (a: string, b: number, c: number) => void) { }
var x: Example;
// When written with overloads, OK -- used first overload
// When written with optionals, correctly an error
fn(x.diff);
  1. 当使用了TypeScript“严格检查null”特性时。 因为没有指定的参数在JavaScript里表示为 undefined,通常显示地为可选参数传入一个undefined。 这段代码在严格null模式下可以工作:
var x: Example;
// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'
// When written with optionals, correctly OK
x.diff("something", true ? undefined : "hour");

使用联合类型

不要为仅在某个位置上的参数类型不同的情况下定义重载:

/* 错误 */
interface Moment {
    utcOffset(): number;
    utcOffset(b: number): Moment;
    utcOffset(b: string): Moment;
}

应该尽可能地使用联合类型:

/* 正确 */
interface Moment {
    utcOffset(): number;
    utcOffset(b: number|string): Moment;
}

注意: b 不是可选的,因为签名的返回类型不同。

为什么:这对于值的传递很重要。

function fn(x: string): void;
function fn(x: number): void;
function fn(x: number|string) {
    // When written with separate overloads, incorrectly an error
    // When written with union types, correctly OK
    return moment().utcOffset(x);
}

理解TS工作的核心概念

为什么:如果理解了TS工作的核心概念,那么就能为任何结构书写声明文件

类型

  • 类型别名声明(type sn = number | string)
  • 接口声明(interface I { x: number[]; })
  • 类声明(class C { })
  • 枚举声明(enum E { A, B, C })
  • 指向某个类型的import声明

解释:值是运行时名字,可以在表达式里引用。 比如 let x = 5;创建一个名为x的值。

  • let, const, var 声明
  • 包含值 nameSpace, module声明
  • enum 声明
  • class 声明
  • 指向值的 import 声明
  • function 声明

类型命名空间

如: let a: A.B.C 则我们就认为C类型来自A.B命名空间。

简单的组合:一个名字,多种意义

一个给定的名字A,可有有三种不同的意义:类型、值或命名空间。如:

let a:A.A=A

  • 首先 A 被当做命名空间
  • 其次 A 被当作类型
  • 最后 A 被当作值

内置组合

class 同时出现在类型和值列表里,class C {} 声明创建了两个东西:

  • 类型 C 指向类的实例结构
  • C 指向类构造函数

枚举类型拥有相似的行为

用户组合

假设我们写了声明文件 foo.d.ts

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

使用:

import * as foo from './foo';
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

这可以很好地工作,但是我们知道SomeType和SomeVar很相关 因此我们想让他们有相同的名字。 我们可以使用组合通过相同的名字 Bar表示这两种不同的对象(值和对象):

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

这提供了解构使用的机会:

import { Bar } from './foo';
let x: Bar = Bar.a;
console.log(x.count);

这里我们使用Bar做为类型和值。 注意我们没有声明 Bar值为Bar类型 -- 它们是独立的。

高级组合

解释: 声明是通过其他声明组合而来的。

如: class C {} 与 interface C {} 可以同时存在,可以同时存在,并且都可以作为C类型的属性。

冲突: 组合只要不产生冲突就是合法的。如果一个普通的规则是值,那么它总是会和其他同名的值产生冲突(在同一个命名空间里),类型冲突则发生在使用类型别名声明的情况下 (type s = string) , 命名空间永远不会发生冲突。

举例如下:

  • 利用 interfice 向另一个 interfice声明添加额外成员
interface Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

类也同样适用:

class Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

注意: 不能使用接口往类别名里添加成员(type s = string)

  • 使用 namespace 添加

namespace 声明可以用来添加新类型、值和命名空间(不产生冲突即合法)

  1. 添加添加静态成员到一个类中
class C {
}
// ... elsewhere ...
namespace C {
  export let x: number;
}
let y = C.x; // OK

*解释: *在上述例子中,我们添加一个值到C的静态部分(它的构造函数)。这里我们添加了一个值,且它的值的容器是另一个值。(类型包含于命名空间,命名空间包含于另外的命名空间)。

  1. 给类添加一个命名空间类型
class C {
}
// ... elsewhere ...
namespace C {
  export interface D { }
}
let y: C.D; // OK

*解释: *当我们写了 namespace 声明才有了命名空间 C 。作为命名空间C 不会与类创建的值C或类型C相互冲突。

  1. 通过namespace声明不同的合并
namespace X {
  export interface Y { }
  export class Z { }
}
​
// ... elsewhere ...
namespace X {
  export var Y: number;
  export namespace Z {
    export class C { }
  }
}
type X = string;

解释:

  • 第一个代码块

    • 一个X值(因为namespace声明包含一个值,X)
    • 一个命名空间X(因为namespace声明包含了一个值,X)
    • 在明明空间X里的类型Y
    • 在命名空间X里的类型Z(类的实例结构)
    • 值X的一个属性值Z(类的构造函数)
  • 第二个代码块

    • 值Y(number类型),他是值X的一个属性
    • 一个命名空间Z
    • 值Z,他是值X的一个属性
    • 在X.Z命名空间下的类型C
    • 值X.Z的一个属性值C
    • 类型X

使用export= 或 import

一个重要的原则是export和import声明会导出或导入目标的所有含义。


基础类型

  • 布尔值(boolean)

  • 数字(number)

  • 字符串(string)

  • 数组

    • let list: number[] = [1, 2, 3, 4]
    • let list: Array< number > = [1, 2, 3, 4]
  • 元组(Tuple)-- 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。如:可以定义一对值为string和number类型的元组。

  // Declare a tuple type
  let x: [string, number];
  // Initialize it
  x = ['hello', 10]; // OK
  // Initialize it incorrectly
  x = [10, 'hello']; // Error
  • 枚举(enum)--- 对javascript标准数据类型的一个补充。

  enum Color = { Red, Green, Blue }
  let c: Color = Color.Green

默认情况下,从0开始为元素编号,也可以手动指定成员的数值。如:

enum Color {Red = 1, Green = 2, Blue = 4}
let colorName: string = Color[2]
console.log(colorName);  // 显示'Green'因为上面代码里它的值是2
  • Any

    • 在开发过程中,有时我们需要为不清楚类型的变量指定一个类型,这些值可能来自于动态内容,如:来自用户输入或者第三方代码库,在这种情况下我们需要屏蔽ts的类型检查,则需要用any类型。

any与Object的区别在于,Object类型的变量只允许给他赋与任意值,但不能在它上面调用任意方法(即使它真的有这个方法),any则可以(少用any)

  • Void

    • 它表示没有任何类型,当一个函数没有返回值时,一般使用void
    • 声明 void 类型的变量只能赋予其 undefind 或者 null
  • Null 和 Undefind

    • TS 里,他们他们两个分别有自己的数据类型 null 和 undefind
    • 默认情况下,null 和 undefind 时所有类型的子类型
    • 当指定了 --strictNullChecks标记 , 他们只能赋值给 void 和他们自己
  • Never

    • 表示永远不存在值的类型
    • Naver是任何类型的子类型,也可以赋值给任何类型
    • 没有类型是Naver的子类型,也不可以赋值给Naver类型(除Naver本身)
    • any 也不可以赋值给Naver
    // 返回never的函数必须存在无法达到的终点
    function error(message: string): never {
        throw new Error(message);
    }
​
    // 推断的返回值类型为never
    function fail() {
        return error("Something failed");
    }
​
    // 返回never的函数必须存在无法达到的终点
    function infiniteLoop(): never {
        while (true) {
        }
    }
  • Object类型

    • 表示非原始数据类型之外的数据类型

      • number、string、boolean、symbol、null、undefind
    declare function create(o: object | null): void;
​
    create({ prop: 0 }); // OK
    create(null); // OK
​
    create(42); // Error
    create("string"); // Error
    create(false); // Error
    create(undefined); // Error
  • 类型断言

    • 有时候你会比TS 更了解某个值的详细信息,一般这会发生在你清楚的知道一个实体具有比它现有类型更确切的类型
    • 通过类型断言的方式可以告诉编译器(程序员已经对类做了检查,编译器可不做检查)
    • 类似其他语言的类型转换,但是不进行特殊的数据检查和解构
    • 他没有运行时的影响,只是在编译阶段起作用

    类型断言有两种形式

    • 尖括号语法
          let someValue: any = "this is a string";
    ​
          let strLength: number = (<string>someValue).length;
    
    • as 语法
          let someValue: any = "this is a string";
    ​
          let strLength: number = (someValue as string).length;
    

    两种断言是等价的,但是挡在typescripe里使用tsx时,只能使用as语法