TypeScript 完全手册

Stack Overflow 在 90,000 名开发者中开展了 一项调查,结果显示 TypeScript 是人们最想学习的工具之一。

在过去几年中,TypeScript 的热门程度、社区规模和使用率都在不断提高。如今,甚至 Facebook 正将 Jest 项目转移至 TypeScript

什么是 TypeScript

TypeScript 是 JavaScript 的超集,具有静态类型特性,旨在简化大型 JavaScript 应用程序的开发,也被称为 JavaScript that scales可拓展的 JavaScript)。

为什么要用 TypeScript

JavaScript 在过去几年中快速发展,成为客户端和服务器端最通用的跨平台语言。

但 JavaScript 本意并不用于大型应用开发。它是一种没有类型系统的动态语言,也就是说,变量的值可以是任何类型(例如字符串或布尔值)。

而类型系统能够提高代码质量和可读性,使代码库更易于维护或重构。更重要的是它可以在编译时就捕获错误,而不是在运行时才捕获。

而 JavaScript 并没有类型系统,所以一个大型开发团队难以使用 JavaScript 构建复杂的应用程序。

而 TypeScript 能在编译时检查不同部分代码的正确性。在编译时检查出错误,便于开发者发现错误的位置和具体问题。如果运行时才检查出错误,则需要跟踪复杂的堆栈,花费大量时间进行调试。

TypeScript 的优点

在开发周期中能更早地捕获潜在的错误。

管理大型代码库。

更易于重构。

更易于团队合作:代码的耦合性越强,不同开发人员访问代码库时越不容易造成无意破坏。

文档特性:类型本身就是一种文档信息,方便日后开发者本人或者其他开发者查询。

TypeScript 的缺点

需要额外的学习:需要在短期放缓进度与长期提高效率间进行权衡。

类型错误可能多种多样。

配置极大地影响运行。

类型

Boolean (布尔值)

const isLoading: boolean = false;

Number (数字)

const decimal: number = 8;

const binary: number = 0b110;

String (字符串)

const fruit: string = 'orange';

Array (数组)

数组可以写成下面两种形式:

// 最常见的方式

let firstFivePrimes: number[] = [2, 3, 5, 7, 11];

// 不太常见的方式:使用泛型 (稍后介绍)

let firstFivePrimes2: Array<number> = [2, 3, 5, 7, 11];

Tuple (元组)

Tuple 类型表示一种组织好的数组,元素的类型预先知道,并且数量固定。这意味着你有可能得到错误提示:

let contact: [string, number] = ['John', 954683];

contact = ['Ana', 842903, 'extra argument']; /* Error!

Type '[string, number, string]' is not assignable to type '[string, number]'. */

Any (任意值)

any 与类型系统中的任何类型都兼容。意味着可以将任何内容赋值给它,也可以将它赋值给任何类型。它能让你避开类型检查。

let variable: any = 'a string';

variable = 5;

variable = false;

variable.someRandomMethod(); /* 行吧,

也许运行的时候 someRandomMethod 是存在的 */

Void (空值)

void 表示没有任何类型。它通常用作没有返回值的函数的返回类型。

function sayMyName(name: string): void {

    console.log(name);

}

sayMyName('Heisenberg');

Never

never 类型表示的是那些永不存在的值的类型。 例如,never 类型是那些总是会抛出异常、或者根本就不会有返回值的函数的返回值类型。

// 抛出异常

function error(message: string): never {

    throw new Error(message);

}

// 永远不能返回

function continuousProcess(): never {

    while (true) {

        // ...

    }

}

Null 和 Undefined

undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。和 void 相似,它们的本身的类型用处不是很大,但是在联合类型中非常有用 (稍后介绍)

type someProp = string | null | undefined;

Unknown

TypeScript 3.0 引入了 unknown (未知) 类型,它是与 any 类型对应的安全类型。任何东西都可以赋值给 unknown,但 unknown 不能赋值给除了它本身和 any 以外的任何东西。在没有先断言或指定到更具体类型的情况下,不允许对 unknown 进行任何操作。

type I1 = unknown & null; // null

type I2 = unknown & string; // string

type U1 = unknown | null; // unknown

type U2 = unknown | string; // unknown

类型别名

类型别名可以为现有类型提供替代名称,以便某些地方使用。构造它的语法如下:

type Login = string;

联合类型

TypeScript 允许让一个属性具有多种数据类型,名为 union (联合) 类型。

type Password = string | number;

交叉类型

交叉类型是将多种类型叠加到一起成为一种类型。

interface Person {

    name: string;

    age: number;

}

interface Worker {

    companyId: string;

}

type Employee = Person & Worker;

Interface (接口)

接口好似你和编译器定义契约,由你指定一个类型,预期它的属性应该是些什么类型。

边注:接口不受 JavaScript 运行时的特性影响,它只在类型检查中会用到。

可以声明**可选属性**(带有 ? 标记),意味着接口的对象可能会、也可能不会定义这些属性。

可以声明**只读属性**,意味着一旦为属性赋值,就无法更改。

interface ICircle {

    readonly id: string;

    center: {

        x: number;

        y: number;

    };

    radius: number;

    color?: string; // 可选属性

}

const circle1: ICircle = {

    id: '001',

    center: { x: 0 },

    radius: 8,

}; /* Error! Property 'y' is missing in type '{ x: number; }'

but required in type '{ x: number; y: number; }'. */

const circle2: ICircle = {

    id: '002',

    center: { x: 0, y: 0 },

    radius: 8,

}; // 正确

扩展接口

接口可以扩展成另一个接口,或者更多接口。这使得接口的编写更具有灵活性和复用性。

interface ICircleWithArea extends ICircle {

    getArea: () => number;

}

实现接口

实现接口的类需要严格遵循接口的结构。

interface IClock {

    currentTime: Date;

    setTime(d: Date): void;

}

枚举

enum (枚举) 用来组织一组的相关值,这些值可以是数值,也可以是字符串值。

enum CardSuit {

    Clubs,

    Diamonds,

    Hearts,

    Spades,

}

let card = CardSuit.Clubs;

默认情况下,枚举的本质是数字。enum 的取值从 0 开始,以 1 递增。

上一个例子所生成的 JavaScript 代码如下:

var CardSuit;

(function (CardSuit) {

    CardSuit[(CardSuit['Clubs'] = 0)] = 'Clubs';

    CardSuit[(CardSuit['Diamonds'] = 1)] = 'Diamonds';

    CardSuit[(CardSuit['Hearts'] = 2)] = 'Hearts';

    CardSuit[(CardSuit['Spades'] = 3)] = 'Spades';

})(CardSuit || (CardSuit = {}));

或者,枚举可以用字符串值来初始化,这种方法更易读。

enum SocialMedia {

    Facebook = 'FACEBOOK',

    Twitter = 'TWITTER',

    Instagram = 'INSTAGRAM',

    LinkedIn = 'LINKEDIN',

}

反向映射

enum 支持反向映射,也就是说,可以通过值来获得成员、成员名。

回顾之前 CardSuit 的例子:

const clubsAsNumber: number = CardSuit.Clubs; // 3

const clubsAsString: string = CardSuit[0]; // 'Clubs'

函数

你可以为每个参数指定一个类型,再为函数指定一个返回类型。

function add(x: number, y: number): number {

    return x + y;

}

函数重载

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);

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,

    };

}

你可以指定属性的类型和方法参数的类型。

class Greeter {

    greeting: string;

    constructor(message: string) {

        this.greeting = message;

    }

    greet(name: string) {

        return `Hi ${name}, ${this.greeting}`;

    }

}

访问修饰符

Typescript 支持 public (公有), private (私有), protected (保护) 修饰符,它们决定了类成员的可访问性。

public (公有) 成员和纯 JavaScript 的成员一样,是默认的修饰符。

private (私有) 成员对外界来说不可访问。

protected(保护) 成员和私有成员的区别在于,它能够被继承类访问。

| 具有访问权限 | public | protected | private |

| :----------- | :----: | :-------: | :-----: |

| 类本身      |  yes  |    yes    |  yes  |

| 派生类      |  yes  |    yes    |  no    |

| 类实例      |  yes  |    no    |  no    |

只读修饰符

readonly (只读) 变量必须在它声明或构造时初始化。

class Spider {

    readonly name: string;

    readonly numberOfLegs: number = 8;

    constructor(theName: string) {

        this.name = theName;

    }

}

参数属性

参数属性 可以放在一个地方创建并初始化成员。它通过给构造函数参数添加一个访问限定符来声明。

class Spider {

    readonly numberOfLegs: number = 8;

    constructor(readonly name: string) {}

}

抽象

abstract (抽象) 这个关键字可以用在抽象类上,也可以用在抽象类方法上。

**抽象类** 不会直接被实例化。抽象类主要用于继承,继承抽象类必须实现它所有的抽象方法。

**抽象成员** 不包含具体实现,因此不能被直接访问。这些成员必须在派生类中实现。 (类似接口)

类型断言

TypeScript 允许你以任何方式覆盖其推断的类型。当你比编译器本身能更好地理解变量类型时,可以使用它。

const friend = {};

friend.name = 'John'; // Error! Property 'name' does not exist on type '{}'

interface Person {

    name: string;

    age: number;

}

const person = {} as Person;

person.name = 'John'; // 正确

最初,类型断言的语法是 <type>

let person = <Person>{};

但这在 JSX 中使用时产生了歧义。因此建议使用 as 代替。

类型断言通常在从 JavaScript 迁移代码时使用,你对变量的类型了解可能比当前指派的更准确。

但断言也会 被认为有害。

我们来看看上一个示例中的 Person 接口,你注意到了什么问题吗?如果你注意到丢失了 **age** 属性,恭喜,你对了!编译器可能会帮助你自动完成 Person 的属性,但如果您遗漏了任何属性,它也不会报错。

类型推论

没有明确指定出类型时,TypeScript 会推断变量类型。

/**

* 变量声明

*/

let a = 'some string';

let b = 1;

a = b; // Error! Type 'number' is not assignable to type 'string'.

// 如果是复杂的对象,TypeScript 会用最常见的类型

// 来推断对象类型。

const arr = [0, 1, false, true]; // (number | boolean)[]

类型兼容性

类型兼容性是基于结构类型的,结构类型只使用其成员来描述类型。

结构化类型系统的基本规则是:如果 x 要兼容 y,那么 y 至少具有与 x 相同的属性。

interface Person {

    name: string;

}

let x: Person; // 正确,尽管不是Person接口的实现

let y = { name: 'John', age: 20 }; // type { name: string; age: number }

x = y;

由于 y 有一个成员 name: string 匹配 Person 接口所需的属性,这意味着 x 是 y 的子类型。因此这个赋值是合法的。

函数

**参数数量**

在函数调用中,至少需要传入足够的参数,多余的参数不会导致任何错误。

function consoleName(person: Person) {

    console.log(person.name);

}

consoleName({ name: 'John' }); // 正确

consoleName({ name: 'John', age: 20 }); // 多余的参数也合法

**返回值类型**

返回值类型必须至少包含足够的数据。

let x = () => ({ name: 'John' });

let y = () => ({ name: 'John', age: 20 });

x = y; // 正确

y = x; /* Error! Property 'age' is missing in type '{ name: string; }'

but required in type '{ name: string; age: number; }' */

类型保护

类型保护可以在条件块中缩小对象类型的范围。

typeof

在条件里使用 typeof,编译器会知道变量的类型会不一致。在下面的示例中,TypeScript 会知道:在条件块之外,x 可能是布尔值,而布尔值上无法调用函数 toFixed。

function example(x: number | boolean) {

    if (typeof x === 'number') {

        return x.toFixed(2);

    }

    return x.toFixed(2); // Error! Property 'toFixed' does not exist on type 'boolean'.

}

instanceof

class MyResponse {

    header = 'header example';

    result = 'result example';

    // ...

}

class MyError {

    header = 'header example';

    message = 'message example';

    // ...

}

function example(x: MyResponse | MyError) {

    if (x instanceof MyResponse) {

        console.log(x.message); // Error! Property 'message' does not exist on type 'MyResponse'.

        console.log(x.result); // 正确

    } else {

        // TypeScript 知道这里一定是 MyError

        console.log(x.message); // 正确

        console.log(x.result); // Error! Property 'result' does not exist on type 'MyError'.

    }

}

in

in 运算符会检查一个属性在某对象上是否存在。

interface Person {

    name: string;

    age: number;

}

const person: Person = {

    name: 'John',

    age: 28,

};

const checkForName = 'name' in person; // true

Literal Types (字面量类型)

字面量正是 JavaScript 原始数据类型具体的值,它们可以与 union (联合) 类型搭配使用,构造一些实用的概念。

type Orientation = 'landscape' | 'portrait';

function changeOrientation(x: Orientation) {

  // ...

}

changeOrientation('portrait'); // 正确

changeOrientation('vertical'); /* Error! Argument of type '"vertical"' is not

assignable to parameter of type 'Orientation'. /

条件类型

条件类型表示类型关系的测试,并根据测试的结果选择两种可能类型中的一种。

type X = A extends B ? C : D;

如果 A 类型可以赋值给 B 类型,那么 X  C 类型;否则 X  D 类型。

泛型

泛型是必须包含或引用其他类型才能完成的类型。它加强了变量之间有意义的约束。

下面例子中的函数会返回所传入的任何类型的数组。

function reverse<T>(items: T[]): T[] {

    return items.reverse();

}

reverse([1, 2, 3]); // number[]

reverse([0, true]); // (number | boolean)[]

keyof

keyof 运算符会查询给定类型的键集。

interface Person {

    name: string;

    age: number;

}

type PersonKeys = keyof Person; // 'name' | 'age'

映射类型

映射类型,通过在属性类型上建立映射,从现有的类型创建新类型。具有已知类型的每个属性都会根据你指定的规则进行转换。

Partial

type Partial<T> = {

    [P in keyof T]?: T[P];

};

泛型 Partial 类型被定义时只有一个类型参数T。

keyof T表示所有T类型属性的名字(字符串字面类型)的联合。

[P in keyof T]?: T[P]表示所有T类型的属性P的类型都应该是可选的,并且都会被转换为T[P]。

T[P]表示T类型的属性P的类型。

Readonly (只读)

正如在接口部分中所介绍的,TypeScript 中可以创建只读属性。 Readonly 类型接受一个类型 T,并将其所有属性设置为只读。

type Readonly<T> = {

    readonly [P in keyof T]: T[P];

};

Exclude

Exclude 可以从其他类型中排除某些类型。排除的是可以赋值给 T 的属性。

/**

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

*/

type User = {

    _id: number;

    name: string;

    email: string;

    created: number;

};

type UserNoMeta = Exclude<keyof User, '_id' | 'created'>;

Pick

Pick 可以从其他类型中选取某些类型。 挑选的是可以赋值给 T 的属性。

/**

* type Pick<T, K extends keyof T> = {

*  [P in K]: T[P];

*  };

*/

type UserNoMeta = Pick<User, 'name' | 'email'>;

infer

你可以使用 infer 关键字来推断条件类型的 extends 子句中的类型变量。这样的推断类型变量只能用于条件类型的 true 分支。

ReturnType

获取函数的返回类型。

/**

* 原版的 TypeScript's ReturnType

* type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

*/

type MyReturnType<T> = T extends (...args: any) => infer R ? R : any;

type TypeFromInfer = MyReturnType<() => number>; // number

type TypeFromFallback = MyReturnType<string>; // any

我们来拆解 MyReturnType:

T 的返回类型是 ...

首先,T 是不是一个函数?

如果是,那么类型解析为推断出的返回类型 R;

如果不是,类型解析为 any。

我的网站:https://www.icoderoad.com

参考资料与实用链接

https://basarat.gitbooks.io/typescript/

https://www.typescriptlang.org/docs/home.html

https://www.tutorialsteacher.com/typescript

https://github.com/dzharii/awesome-typescript

https://github.com/typescript-cheatsheets/react-typescript-cheatsheet

为了达到学习和实践 TypeScript 的目的,我用 TS 和 React-Native(用了 hooks)构建了一个简单的 CurrencyConverter (汇率转换) 程序。你可以在 这里 查看这个项目。

感谢、祝贺您阅读到这里!如果您对此有任何想法,请随时发表评论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容