TypeScript 快速上手及学习笔记

TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。

TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。

什么是 TypeScript

TypeScript 是 JavaScript 的、带有类型超集,并且能够编译成普通的 JavaScript。

编译:

类型:

超集:

TypeScript 本身支持所有 JavaScript 的语法,并在此基础上添加了额外的功能和特性。

TypeScript 是由微软开发的一款开源的编程语言。

TypeScript 是 Javascript 的超集,遵循最新的 ES6、Es5 规范。TypeScript 扩展了 JavaScript 的语法。

TypeScript 更像后端 java、C#这样的面向对象语言,可以让 js 开发大型企业项目。

谷歌也在大力支持 Typescript 的推广,谷歌的 angular2.x+就是基于 Typescript 语法。

最新的 Vue 、React 也可以集成 TypeScript。

Nodejs 框架 Nestjs、midway 中用的就是 TypeScript 语法。

带有类型,是说js在定义变量的时候,类型是动态的,只有在运行的时候才能知道它的具体类型,比如 number 或者 string,并且类型也是可以动态变化的,而 TypeScript 则是要求变量有确定的类型,并且在编写代码的时候就已经确定,如果把字符串赋给类型为 number ,数字类型的变量,就会出错。

为什么用 TypeScript

在 stackoverflow 发起的2020年程序员调查中,TypeScript 在程序员最爱的编程语言中排在了第二位

https://stackoverflow.blog/2020/05/27/2020-stack-overflow-developer-survey-results/

好处优势:

类型检查、代码补全、易于维护、入门简单

之所以大家喜欢 TypeScript,是因为:

  • TypeScript 有类型检查机制,我们可以在写代码的时候就能够发现错误,比如给函数误传了类型不同的参数,那么通过 VS Code 对 TypeScript 的强力支持,我们能立刻看到错误。
  • 另外 VS Code 能根据 TypeScript 的类型信息提供更好的代码提示和补全功能。
    此外,对于大型项目、多人协作编写代码时,类型起到了文档的作用,可以清楚的知道我这个变量是什么类型,或者我定义的函数需要什么样的参数,我的对象里又有哪些属性。这样让代码更易于维护,这也是为什么大公司、大型项目更偏爱 TypeScript
  • 最后 TypeScript 入门的门槛低,只要你会 JavaScript,那么你就已经能编写 TypeScript 代码了。另外因为 JS 的快速发展,好多以前在 typescript 才能用的功能,你可能在JS 里已经用到了,所以说要学习的东西就更少了。

除了这些好处之外,它也有其他静态类型语言比如 Java/c++ 的通病,就是代码量会增加,并且有时候类型过于复杂反而使得代码显的更难阅读,不过跟它带来的优势相比,也显得不那么突出了。

 

安装TypeScript

有两种主要的方式来获取TypeScript工具:

  • 通过npm(Node.js包管理器)
  • 安装Visual Studio的 TypeScript 插件

Visual Studio 2017和Visual Studio 2015 Update 3默认包含了TypeScript。 如果你的Visual Studio还没有安装TypeScript,你可以下载它。

针对使用npm的用户:

npm install -g typescript

构建你的第一个TypeScript文件

在编辑器,将下面的代码输入到greeter.ts文件里:

编译代码

我们使用了.ts扩展名,但是这段代码仅仅是JavaScript而已。 你可以直接从现有的JavaScript应用里复制/粘贴这段代码。

在命令行上,运行TypeScript编译器:

tsc greeter.ts

然后可以看到,输出结果为一个 greeter.js 文件,它包含了和输入文件中相同的JavsScript代码。

// greeter.js
function greeter(person) {
    return "Hello, " + person;
}
var user = "Jane User";
document.body.innerHTML = greeter(user);

一切准备就绪,我们可以运行这个使用 TypeScript 写的 JavaScript 应用了!

接下来让我们看看 TypeScript 工具带来的高级功能。 给 person 函数的参数添加 :string 类型注解,如下:

function greeter(person: string) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

运行编译:

tsc greeter.ts

可以看到 greeter.js 的代码没变。

类型注解

TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式。 在这个例子里,我们希望 greeter函数接收一个字符串参数。 

然后尝试把 greeter的调用改成传入一个数组:

function greeter(person: string) {
    return "Hello, " + person;
}

let user = [0, 1, 2];

document.body.innerHTML = greeter(user);

可以看到报错:

重新运行编译也可以看到报错:

类似地,尝试删除 greeter 调用的所有参数。 TypeScript会告诉你使用了非期望个数的参数调用了这个函数。 在这两种情况中,TypeScript提供了静态的代码分析,它可以分析代码结构和提供的类型注解。

要注意的是尽管有错误,greeter.js 文件还是被创建了。 就算你的代码里有错误,你仍然可以使用TypeScript。但在这种情况下,TypeScript会警告你代码可能不会按预期执行。

新建一个 index.ts 文件:

let a:number = 10;
console.log(a);

编译并运行:可以看到

另一种编译方法是通过 deno:deno 本身就支持 TypeScript ,所以只需要安装 deno 的运行环境就可以了。

// 使用 Shell:

curl -fsSL https://x.deno.js.cn/install.sh | sh

// 使用 PowerShell:

iwr https://x.deno.js.cn/install.ps1 -useb | iex

安装完成后,输入:

deno run index.ts

也可以输出结果:10

打开 index.js 可以看到:除了去除了类型没有什么变化

var a = 10;
console.log(a);

有个问题:JavaScript 版本那么多,tsc 怎么知道要编译成那个版本呢?

tsc 默认会编译成 ES3 版。

下面我们写一个 async 函数:

let a:number = 10;
console.log(a);

async function func() {
  
}

再重新编译下,然后打开 index.js 文件,发现生成了很多复杂的代码来支持 async。

那如果我想要生成 ES2017 的代码呢?

需要在根目录下创建一个 tsconfigt.json 文件:

{
  "compilerOptions": {
    "target": "ES2017"
  }
}

这时编译可以输入:

tsc

因为有了 tscofigt.josn 文件后,这个文件夹会自动成为 TypeScript 项目,tsc 会自动找到 .ts 文件,并进行编译。如果指定了文件名,那么  tscofigt.josn 设置就会被忽略

然后打开 index.js 文件可以看到:

let a = 10;
console.log(a);
async function func() {
}

还可以用开发工具 Vscode 自动编译.ts 文件

在终端中输入:可以生成配置文件 tsconfig.json

tsc --init

注意:如果你已经创建过了,就不会再生成了。

TypeScript 基本语法

布尔类型(boolean)、数字类型(number)、字符串类型(string)、数组类型(array)、元组类型(tuple)、枚举类型(enum)、任意类型(any)、任意类型(any)、对象类型(object)、void类型、never类型、组合类型、类型别名、null 和 undefined

1、基本类型

给变量定义类型有两种方式:隐式类型和显式类型

隐式类型:是由 TypeScript 根据变量的值来推断类型,代码写法和js一样,但不同的是:后面不能用其他类型的值来给它重新赋值。

比如:

let a = 10;
a = "hello";

可以看到a上报错,鼠标移到a上可以看到:

显式类型:和之前运行的ts代码示例一样,用 : + 类型 来规定这个变量是什么类型的。

·常用类型:

布尔类型(boolean)、数字类型(number)、字符串类型(string)、null 和 undefined

例如:

let b: boolean = true;

你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `),并且以 ${ expr } 这种形式嵌入表达式

let username: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ username }. I'll be ${ age + 1 } years old next month.`;
console.log(sentence); // Hello, my name is Gene. I'll be 38 years old next month.

这与下面定义sentence的方式效果相同:

let username: string = `Gene`;
let age: number = 37;
let sentence: string = "Hello, my name is " + username + ".
" +
  "I'll be " + (age + 1) + " years old next month.";
console.log(sentence); 
// Hello, my name is Gene.
// I'll be 38 years old next month.

·任意类型 any

如果想让一个变量可以是任何类型,就像 js 中可以任意更改的话。那么我们可以把它的类型定义为 any

例如:这时可以看到不会报错

let a: any = 10;
a = "hello";

类型也可以用在函数的参数和返回值中

比如:

function add(a: number, b: number): number {
  return a + b;
}
add(1, 2);

// 小括号后面的:number 是返回值的类型,当然也可以省略不写

function add(a: number, b: number) {
  return a + b;
}
add(1, 2);

注意:调用函数时必须传递跟参数列表相同的参数(也就是说实参和形参的数量要一致),不像 js 可以不传或者只传前面的几个参数。

例如:我们只传a的值

function add(a: number, b: number): number{
  return a + b;
}
add(1);

会报错:

·void类型

某种程度上来说,void 类型像是与 any 类型相反,它表示没有任何类型。 如果函数不返回值的话,可以使用 :void 类型,代表函数没有返回值

例如:

function add(a: number, b: number): void {
  console.log(a + b);
}

2、组合类型

如果一个变量可以有多个类型,但是又不想使用 any 破坏类型检查,那么可以使用组合类型。组合类型使用 | 操作符来定义

例如:

let a :number | string = 10;
a = "hello";

不过这样代码看起来不太方便,并且这个组合类型只能给 a 使用,如果再有一个变量 b,也可以是number 或者 string 类型,那么还需要再重复定义这个类型。

要解决这个问题,我们可以使用 type 关键字,来给这个组合类型起个别名,让代码更易读,也方便其他变量使用。

例如:

type NumStr = number | string;
let a: NumStr = 10;
a = "hello";
let b: NumStr = '123';
b = 123;

另外组合类型也可以直接使用字面值来定义,这样就规定了一个变量的取值范围。

例如:

let c: "on" | "off" = "on";
c = "off";
c = "other";

可以看到最后一条报错:

3、对象类型

使用 interface 接口,接口是用来规范一个对象里应该都有哪些属性,包括它的名字和类型

例子:如果有一个 post 文章变量,里面有 title 和 author 属性,并且都是 string 类型的。那么我们可以使用接口来定义一个 post 类型。

interface Post {
  title: string;
  author: string;
}

let post: Post = {
  title: '标题',
  author: '作者'
}

如果在增加一个属性就会报错:

同样,去掉 title 属性,一样会报错:

接口除了可以检查对象是否符合规范外,也可以用于函数参数的类型检查

注意:如果传递进来的对象没有定义类型的话,只要它的属性里包括接口中定义的规范,那么就可以通过检查,哪怕它有额外的属性。

例如:

interface Post {
  title: string;
  author: string;
}

function getTitle(post: Post) {
  console.log(post.title)
}

let post = {
  title: '标题',
  author: '作者',
  publishDate: '2020-10-10'
}

getTitle(post); // 标题

如果想要严格检查对象参数的话,可以像之前那样把 post 对象定义为 Post 接口类型的:

let post: Post = {

}

或者直接给函数传递对象字面值:

getTitle({
  title: '标题',
  author: '作者',
}); // 标题

4、数组类型

给数组规定类型,可以保证里面的元素都是同一个类型。以防在统一处理数组元素时,会混进来其他类型的元素,导致异常;或者防止意外给数组元素赋了其他类型的值。

let arr: number[] = [1, 2, 3];

还有一种写法是:使用数组泛型 Array<元素类型>

let arr: Array<number> = [1, 2, 3];

当然也可以使用 any 定义任意类型的数组:

let arr: any[] = [1, "fh", true];

5、元祖 (tuple)

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同

有限元素数量的数组,每个元素要分别指定是什么类型

let tup: [number, string, boolean] = [1, "fh", true];

6、枚举类型(enum) 

enum 类型是对 JavaScript 标准数据类型的一个补充。 像 C# 等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

如果能在程序中用自然语言中有相应含义的单词来代表某一状态,则程序就很容易阅读和理解。也就是说,事先考虑到某一变量可能取的值,尽量用自然语言中含义清楚的单词来表示它的每一个值,这种方法称为枚举方法,用这种方法定义的类型称枚举类型。

enum Status {success=1, error=2};
let s:Status = Status.success;
console.log(s); //1
enum Color {blue, red, orange};
let c:Color = Color.red;
console.log(c); //1 如果标识符没有赋值 它的值就是下标

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到 Color 里的哪个名字,我们可以查找相应的名字:

enum Color {blue , red, orange}
let colorName1: string = Color[2];
console.log(colorName1);  // orange

enum NewColor {blue = 1, red, orange}
let colorName2: string = NewColor[2];
console.log(colorName2);  // // 显示'red'因为上面代码里它的值是2

7、对象类型(object)

object 表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK
let arr2:object = [11, 'asd', 1321];
console.log(arr2);

8、null 和 undefined:其他(never类型)数据类型的子类型

TypeScript里,undefined 和 null 两者各自有自己的类型分别叫做 undefined 和 null。 和 void 相似,它们的本身的类型用处不是很大:

let num:number;
console.log(num)  //输出:undefined   报错
let num1:undefined;
console.log(num1)  //输出:undefined  //正确
let num2:number | undefined;
num2 = 123;
console.log(num2);

默认情况下 null 和 undefined 是所有类型的子类型。 就是说你可以把 null  和undefined 赋值给 number 类型的变量。

然而,当你指定了--strictNullChecks 标记,null和undefined只能赋值给void和它们各自。 这能避免 很多常见的问题。 也许在某处你想传入一个 string或null或undefined,你可以使用联合类型string | null | undefined。 再次说明,稍后我们会介绍联合类型。

注意:我们鼓励尽可能地使用--strictNullChecks,但在本手册里我们假设这个标记是关闭的。

9、never类型

never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never 类型是其他类型 (包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明 never 的变量只能被 never 类型所赋值。 即使 any 也不可以赋值给 never

let a: never;
a = 123; //错误写法
a = (function () {
  throw new Error("错误")
})();

// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
  throw new Error(message);
}

// 推断的返回值类型为never
function fail() {
  return error("Something failed");
}

// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
  while (true) {
  }
}

类型断言

有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

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;

两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as语法断言是被允许的。

关于let

你可能已经注意到了,我们使用 let 关键字来代替大家所熟悉的JavaScript关键字 var。 let关键字是JavaScript的一个新概念,TypeScript实现了它。 我们会在以后详细介绍它,很多常见的问题都可以通过使用 let来解决,所以尽可能地使用let来代替var

变量声明

let和const是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,它能阻止对一个变量再次赋值。
因为TypeScript是JavaScript的超集,所以它本身就支持let和const。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var。

var 声明

一直以来我们都是通过 var 关键字定义JavaScript变量。

function f() {
    var a = 10;
    return function g() {
        var b = a + 1;
        return b;
    }
}

var g = f();
g(); // returns 11;

上面的例子里,g可以获取到f函数里定义的a变量。 每当 g被调用时,它都可以访问到f里的a变量。 即使当 gf已经执行完后才被调用,它仍然可以访问及修改a

function f() {
    var a = 1;

    a = 2;
    var b = g();
    a = 3;

    return b;

    function g() {
        return a;
    }
}

f(); // returns 2

作用域规则

对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:

function f(shouldInitialize: boolean) {
    if (shouldInitialize) {
        var x = 10;
    }

    return x;
}

f(true);  // returns '10'
f(false); // returns 'undefined'

变量 x 是定义在*if语句里面*,但是我们却可以在语句的外面访问它。 这是因为 var 声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为 var 作用域函数作用域*。 函数参数也使用函数作用域。

这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:

function sumMatrix(matrix: number[][]) {
    var sum = 0;
    for (var i = 0; i < matrix.length; i++) {
        var currentRow = matrix[i];
        for (var i = 0; i < currentRow.length; i++) {
            sum += currentRow[i];
        }
    }
    return sum;
}

这里很容易看出一些问题,里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。

捕获变量怪异之处

快速的猜一下下面的代码会返回什么:

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
     console.log(i); 
  }, 100 * i);
}

好吧,看一下结果:

10
10
10
10
10
10
10
10
10
10

很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:

0
1
2
3
4
5
6
7
8
9

还记得我们上面提到的捕获变量吗?

我们传给 setTimeout 的每一个函数表达式实际上都引用了相同作用域里的同一个 i

让我们花点时间思考一下这是为什么。 setTimeout 在若干毫秒后执行一个函数,并且是在 for 循环结束后。 for 循环结束后,的值为 10。 所以当函数被调用的时候,它会打印出 10

一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时 i 的值:

for (var i = 0; i < 10; i++) {
  // capture the current state of 'i'
  // by invoking a function with its current value
  (function(i) {
      setTimeout(function() { console.log(i); }, 100 * i);
  })(i);
}

这种奇怪的形式我们已经司空见惯了。 参数 会覆盖 for 循环里的 i,但是因为我们起了同样的名字,所以我们不用怎么改for循环体里的代码。

let 声明

现在你已经知道了 var 存在一些问题,这恰好说明了为什么用 let 语句来声明变量。 除了名字不同外, let 与 var 的写法一致。

主要的区别不在语法上,而是语义,我们接下来会深入研究。

块作用域

当用 let 声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var 声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。

function f(input: boolean) {
    let a = 100;

    if (input) {
        // Still okay to reference 'a'
        let b = a + 1;
        return b;
    }

    // 报错 Error: 'b' doesn't exist here
    return b;
}

这里我们定义了2个变量 a 和 b。 a 的作用域是 函数体内,而 b 的作用域是 if 语句块里。

在 catch 语句里声明的变量也具有同样的作用域规则。

try {
    throw "oh no!";
}
catch (e) {
    console.log("Oh well.");
}

// 报错 Error: 'e' doesn't exist here
console.log(e);

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 它只是用来说明我们不能在 let 语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。

注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。

function foo() {
    // okay to capture 'a'
    return a;
}

// 不能在'a'被声明前调用'foo'
// 运行时应该抛出错误
foo();

let a;

重定义及屏蔽

我们提过使用 var 声明时,它不在乎你声明多少次;你只会得到1个。

function f(x) {
  var x;
  var x;

  if (true) {
      var x;
  }
}

在上面的例子里,所有x的声明实际上都引用一个相同的 x,并且这是完全有效的代码。 这经常会成为 bug 的来源。 好的是, let 声明就不会这么宽松了。

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}

function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}

并不是说块级作用域变量不能用函数作用域变量来声明。 而是块级作用域变量需要在明显不同的块里声明。

function f(condition, x) {
  if (condition) {
      let x = 100;
      return x;
  }
  return x;
}

f(false, 0); // returns 0
f(true, 0);  // returns 100

在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let 重写之前的 sumMatrix 函数。

function sumMatrix(matrix: number[][]) {
  let sum = 0;
  for (let i = 0; i < matrix.length; i++) {
      var currentRow = matrix[i];
      for (let i = 0; i < currentRow.length; i++) {
          sum += currentRow[i];
      }
  }
  return sum;
}

这个版本的循环能得到正确的结果,因为内层循环的 i 可以屏蔽掉外层循环的 i

块级作用域变量的获取

在我们最初谈及获取用 var 声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

function theCityThatAlwaysSleeps() {
  let getCity;

  if (true) {
      let city = "Seattle";
      getCity = function() {
          return city;
      }
  }
  return getCity();
}

console.log(theCityThatAlwaysSleeps()); // Seattle

因为我们已经在 city 的环境里获取到了 city,所以就算 if 语句执行结束后我们仍然可以访问它。

回想一下前面 setTimeout 的例子,我们最后需要使用立即执行的函数表达式来获取每次 for 循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。 

当 let 声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout 例子里我们仅使用 let 声明就可以了。

for (let i = 0; i < 10; i++) {
  setTimeout(function() {
     console.log(i); 
  }, 100 * i);
}

会输出与预料一致的结果:

0
1
2
3
4
5
6
7
8
9

const 声明

const 声明是声明变量的另一种方式。

const numLivesForCat = 9;

它们与 let 声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let 相同的作用域规则,但是不能对它们重新赋值

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

console.log(kitty); // { name: 'Cat', numLives: 8 }

除非你使用特殊的方法去避免,实际上 const 变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。

let vs. const

现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。

使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。

解构

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

这创建了2个命名变量 first 和 second。 相当于使用了索引,但更为方便:

first = input[0];
second = input[1];

解构作用于已声明的变量会更好:

// 交换变量
[first, second] = [second, first];

作用于函数参数:

function f([first, second]: [number, number]) {
  console.log(first);
  console.log(second);
}
f([1, 2]);  // 1 // 2

你可以在数组里使用 ... 语法创建剩余变量:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其它元素:

let [, second, , fourth] = [1, 2, 3, 4];
console.log(second); // outputs 2
console.log(fourth); // outputs 4

对象解构

你也可以解构对象:

let o = {
  a: "foo",
  b: 12,
  c: "bar"
};
let { a, b } = o;
console.log(a); // foo
console.log(b); // 12

这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。

就像数组解构,你可以用没有声明的赋值:

({ a, b } = { a: "baz", b: 101 });

注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。

你可以在对象里使用 ... 语法创建剩余变量:

let o = {
  a: "foo",
  b: 12,
  c: "bar"
};
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
console.log(total); // 15
console.log(passthrough); // { b: 12, c: 'bar' }

属性重命名

你也可以给属性以不同的名字:

let o = {
  a: "foo",
  b: 12,
  c: "bar"
};
let { a: newName1, b: newName2 } = o;
console.log(newName1); // foo
console.log(newName2); // 12

这里的语法开始变得混乱。 你可以将 a: newName1 读做 "a 作为 newName1"。 方向是从左到右,好像你写成了以下样子:

let newName1 = o.a;
let newName2 = o.b;

令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。

let o = {
  a: "foo",
  b: 12,
  c: "bar"
};
let {a, b}: {a: string, b: number} = o;
console.log(a); // foo
console.log(b); // 12

默认值

默认值可以让你在属性为 undefined 时使用缺省值:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。

function keepWholeObject(wholeObject: { a: string, b?: number }) {
  let { a, b = 1001 } = wholeObject;
  return {a, b};
}

console.log(keepWholeObject({a: '11'})); // { a: '11', b: 1001 }

TypeScript中的函数

1、函数的定义

传递参数和返回值都要进行指定类型
返回类型必须是 string 类型,不能写成别的数据类型,否则会报错

// 有返回值
function run1(): string {
  return 'run';
}
// 无返回值
function run2(): void{
  console.log('run');
}
// 箭头函数
const materials = [
  'Hydrogen',
  'Helium',
  'Lithium',
  'Beryllium'
];

console.log(materials.map((material): any => material.length)); // [8, 6, 7, 9] 加不加any都可以

匿名函数的写法:

 var  run2=function():string{//指定返回值类型为string字符串类型
  return '1243';
}

run2(); //调用方法
console.log(run2()); // 1243

定义方法中的传参:

function getInfo(name: string, age: number): string {
  return `${name} --- ${age}`;
}
console.log(getInfo("zhangsan", 20)); // zhangsan --- 20

2、可选参数

es5 里面方法的实参和行参可以不一样,但是 ts 中必须一样,如果不一样就需要配置可选参数,用问号?表示

如果调用函数的时候,不传第二个参数,则要在第二个函数 age?上加一个问号。问号就表示这个age可以传,可以不传。

function getInfo(name: string, age?: number): string {
  if (age) {
    return `${name} --- ${age}`;
  } else {
    return `${name} ---年龄保密`;
  }
}
console.log(getInfo('zhangsan')); // zhangsan ---年龄保密
console.log(getInfo('zhangsan', 123)); // zhangsan --- 123

注意:可选参数必须配置到参数的最后面。

3、默认参数

es5 里面没法设置默认参数,es6 和 ts 中都可以设置默认参数 。位置可以放在第一个参数,也可以放在最后一个参数的位置

如果没有传年龄的参数,则默认为number=20,如果传参数,就是传的那个年龄的参数,和上面的可选参数是类似的。

function getInfo(name: string, age: number = 20): string {
  if (age) {
    return `${name} --- ${age}`;
  } else {
    return `${name} ---年龄保密`;
  }
}
console.log( getInfo('张三')); // 张三 --- 20
console.log(getInfo('张三', 30)); // 张三 --- 30

4、剩余参数

必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在 JavaScript 里,你可以使用 arguments 来访问所有传入的参数。在 TypeScript 里,你可以把所有参数收集到一个变量里。

剩余参数会被当做个数不限的可选参数。可以一个都没有,同样也可以有任意个。

例子:求和

function   sum(a:number,b:number,c:number,d:number){
  return a+b+c+d
}
sum (1,2,3,4);
console.log(sum (1,2,3,4,)); // 10

例子:求积

function sum(a:number,b:number,c:number,d:number){
  return a*b*c*d
}
sum (1,2,3,4);
console.log(sum (1,2,3,4,)); // 24

三点运算符,接收形参传过来的值(剩余参数)(把sum里面所有传过来的参数全部赋值给result数组)

function sum(...result: number[]): number {
  var sum = 0;
  for (var i = 0; i < result.length; i++) {
    sum += result[i];
  }
  return sum;
}
console.log(sum(1, 2, 3, 4, 5, 6)); // 21

另一种写法,把1赋给a,2赋给b,后面三个数赋给...result

function sum(a: number, b: number, ...result: number[]): number {
  var sum = a + b;
  for (var i = 0; i < result.length; i++) {
    sum += result[i];
  }
  return sum;
}
console.log(sum(1, 2, 3, 4, 5, 6)); // 21

5、函数重载

java中方法的重载:重载指的是两个或者两个以上同名函数,但它们的参数不一样,这时会出现函数重载的情况

TS中的重载:通过为同一个函数提供多个函数类型定义来试下多种功能的目的

es5中出现同名方法,下面的会替换上面的方法

// 重载就相当于同名的函数,当es5中出现同名方法,下面的会替换上面的方法
function css(config){
}
function css(config,value){
}

ts重载,通过为同一个函数提供多个函数类型定义来实现多种功能的目的

function getInfo(name: string): string;
function getInfo(age: number): number;
function getInfo(str: any): any{
    if(typeof  str === 'string'){
        return '我叫:'+ str;
    }else{
        return '我年龄:' + str;
    }
}
console.log(getInfo('张三')); // 我叫:张三
console.log(getInfo(20)); // 我年龄:20

TypeScript的重载,分别返回了字符串(张三)和number类型(20)

改下代码:年龄属性变为可选

function getInfo(name: string): string;
function getInfo(name: string, age: number): string;
function getInfo(name: any, age?: any): any {
  if (age) {
    return '我叫:' + name + ',我的年龄是' + age;
  } else {
    return '我叫:' + name;
  }
}
console.log(getInfo('张三')); // 我叫:张三
console.log(getInfo('张三', 20)); // 我叫:张三,我的年龄是20

再改下:年龄属性给个默认值

function getInfo(name: string): string;
function getInfo(name: string, age: number): string;
function getInfo(name: any, age: number = 18): any {
  if (age) {
    return '我叫:' + name + ',我的年龄是' + age;
  } else {
    return '我叫:' + name;
  }
}
console.log(getInfo('张三')); // 我叫:张三,我的年龄是18
console.log(getInfo('张三', 20)); // 我叫:张三,我的年龄是20

TypeScript中的类

1、ts中类的定义

定义类的关键字为 class,后面紧跟类名,类可以包含以下几个模块(类的数据成员):

  • 字段 − 字段是类里面声明的变量。字段表示对象的有关数据。

  • 构造函数 − 类实例化时调用,可以为类的对象分配内存。

  • 方法 − 方法为对象要执行的操作。

创建类的数据成员:

以下实例我们声明了类 Car,包含字段为 engine,构造函数在类实例化后初始化字段 engine。
this 关键字表示当前类实例化的对象。注意构造函数的参数名与字段名相同,this.engine 表示类的字段。
此外我们也在类中定义了一个方法 disp()。

class Car { 
    // 字段 
    engine:string; 
 
    // 构造函数 
    constructor(engine:string) { 
        this.engine = engine 
    }  
 
    // 方法 
    disp():void { 
        console.log("发动机为 :   "+this.engine) 
    } 
}

编译上面代码可以得到:

class Car {
    // 构造函数 
    constructor(engine) {
        this.engine = engine;
    }
    // 方法 
    disp() {
        console.log("发动机为 :   " + this.engine);
    }
}

创建实例化对象:

使用 new 关键字来实例化类的对象,语法格式如下:

var object_name = new class_name([ arguments ])

例子:创建来一个 Car 类,然后通过关键字 new 来创建一个对象并访问属性和方法:

class Car { 
  // 字段
  engine:string; 
  
  // 构造函数
  constructor(engine:string) { 
     this.engine = engine 
  }  
  
  // 方法
  disp():void { 
     console.log("函数中显示发动机型号 : " + this.engine) 
  } 
} 

// 创建一个对象
var obj = new Car("XXSY1")

// 访问字段
console.log("读取发动机型号 : " + obj.engine);   // 读取发动机型号 : XXSY1

// 访问方法
obj.disp(); // 函数中显示发动机型号 : XXSY1

例子1:

class Person {
  name: string;   //属性  前面省略了public关键词
  constructor(n: string) {  //构造函数   实例化类的时候触发的方法
    this.name = n;
  }
  run(): void {
    console.log(this.name);
  }
}
var p = new Person('张三');
p.run(); // 张三

例子2:

class Person {
  name: string;
  constructor(name: string) {  //构造函数   实例化类的时候触发的方法
    this.name = name;
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}
var p = new Person('张三');
console.log(p.getName()); // 张三
p.setName('李四');
console.log(p.getName()); // 李四

2、ts 中实现继承 extends、super

类继承使用关键字 extends,子类除了不能继承父类的私有成员(方法和属性)和构造函数,其他的都可以继承。

TypeScript 一次只能继承一个类,不支持继承多个类,但 TypeScript 支持多重继承(A 继承 B,B 继承 C)。

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
var p = new Person('王五');
console.log(p.run()); // 王五在运动

class Web extends Person { // 继承
  constructor(name: string) {
    super(name);  /*初始化父类的构造函数*/
  }
  work() {
    console.log(`${this.name}在工作`)
  }
}
var w = new Web('李四');
console.log(w.run()); // 李四在运动
console.log(w.work()); // 李四在工作

类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写。

其中 super 关键字是对父类的直接引用,该关键字可以引用父类的属性和方法。

需要注意的是子类只能继承一个父类,TypeScript 不支持继承多个类,但支持多重继承,如下实例:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}

class Child extends Person {} 
class Web extends Child {} // 多重继承,继承了 Child 和 Person 类

var w = new Web('张三');
console.log(w.run()); // 张三在运动

3、类里面的修饰符

TypeScript 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。TypeScript 支持 3 种不同的访问权限。属性如果不加修饰符 默认就是 公有 (public)

  • public(默认) : 公有,可以在任何地方被访问。

  • protected : 受保护,可以被其自身以及其子类和父类访问。

  • private : 私有,只能被其定义所在的类访问。

public : 公有,在当前类里面、子类 、类外面都可以访问

在子类中:

class Person {
  public name: string;  /*公有属性*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
  
class Web extends Person {
  constructor(name: string) {
    super(name);  /*初始化父类的构造函数*/
  }
  run(): string {
    return `${this.name}在运动-子类`
  }
  work() {
    console.log(`${this.name}在工作`)
  }
}
var w = new Web('李四');
console.log(w.run()); // 李四在运动-子类
w.work(); // 李四在工作

类外部访问公有属性:

class Person {
  public name: string;  /*公有属性*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
var p = new Person('哈哈哈');
console.log(p.name); // 哈哈哈

protected:保护类型,在类里面、子类里面可以访问,在类外部没法访问

在子类中:

class Person {
  protected name: string;  /*保护属性*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}

class Web extends Person {
  constructor(name: string) {
    super(name);  /*初始化父类的构造函数*/
  }
  work() {
    console.log(`${this.name}在工作`)
  }
}
var w = new Web('李四');
w.work(); // 李四在工作
console.log(w.run()); // 李四在运动

类外外部没法访问保护类型的属性:会报错

class Person {
  protected name: string;  /*保护类型*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
var p = new Person('哈哈哈');
console.log(p.name); // 会报错

private :私有,在类里面可以访问,子类、类外部都没法访问

在子类中:会报错

class Person {
  private name: string;  /*私有*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
class Web extends Person { constructor(name: string) { super(name) } work() { console.log(`${
this.name}在工作`) } }

在类外部里:会报错

class Person {
  private name: string;  /*私有*/
  constructor(name: string) {
    this.name = name;
  }
  run(): string {
    return `${this.name}在运动`
  }
}
var p = new Person('哈哈哈');
console.log(p.name);
console.log(p.run());

4、静态属性 静态方法

static 关键字用于定义类的数据成员(属性和方法)为静态的,静态成员可以直接通过类名调用。 

例子1:

class Father {
  static myName: string = 'mySkey';
  static getAge(): number{
    return 23;
  }
}
console.log(Father.myName); // mySkey
console.log(Father.getAge()); // 23

例子2:

class Per {
  public name: string;
  public age: number = 20;

  //静态属性
  static sex = "男";
  constructor(name: string) {
    this.name = name;
  }
  run() {  /*实例方法*/
    console.log(`${this.name}在运动`)
  }
  work() {
    console.log(`${this.name}在工作`)
  }
  static print() {  /*静态方法  里面没法直接调用类里面的属性*/
    console.log('print方法' + Per.sex);
  }
}

Per.print(); // print方法男
console.log(Per.sex); //

5、只读

readonly 申明的只读的属性不能再更改

class Person {
  readonly name: string = 'mySkey';
}
let mySkey = new Person();
console.log(mySkey.name);   // mySkey
mySkey.name = '1111';       // Error 报错

 

6、get 与 set 来截取对对象成员的访问

TypeScript支持getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问 

// 就像女人的年龄是个秘密,只会对不喜欢的人告诉真实的
let isLike: boolean = false;
class Woman{
  private _age: number = 16;

  get age(): number{
    return this._age;
  }

  set age(num: number){
    if(!isLike){
      this._age = num;
    }else{
      console.log('告诉你的也是假的!!')
    }
  }
}
let woman = new Woman();
woman.age = 23;
console.log(woman.age); // 23

// 看吧,不喜欢你,所以你知道了她的真实年龄,如果把isLike改为true,那么她每天就是16了

let isLike: boolean = true;
class Woman{
  private _age: number = 16;

  get age(): number{
    return this._age;
  }

  set age(num: number){
    if(!isLike){
      this._age = num;
    }else{
      console.log('告诉你的也是假的!!')
    }
  }
}
let woman = new Woman();
woman.age = 23;
console.log(woman.age); 
// 告诉你的也是假的!!
// 16

7、多态

父类定义一个方法不去实现,让继承它的子类去实现 每一个子类有不同的表现

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {   //具体吃什么  不知道   ,  具体吃什么?继承它的子类去实现 ,每一个子类的表现不一样
    console.log('吃的方法')
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name)
  }
  eat() {
    console.log(this.name + '啃骨头')
  }
}
class Cat extends Animal {
  constructor(name: string) {
    super(name)
  }
  eat() {
    console.log(this.name + '吃鱼')
  }
}
let ANAME: string = "Tom";
var D = new Dog(ANAME);
D.eat(); // Tom啃骨头
var C = new Cat(ANAME);
C.eat(); // Tom吃鱼

8、抽象类

typescript 中的抽象类:它是提供其他类继承的基类,不能直接被实例化。

abstract 关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现

abstract 抽象方法只能放在抽象类里面

例子1:

abstract class Person {
  abstract type: string = 'animal'; // 抽象类
  sing(){
    console.log('我会唱歌!')
  }
}
// let person = new Person() // Error 无法创建抽象类的实例
class Myskey extends Person {
  type: string = 'animal123';    // 无法继承,而且必须重写
  constructor(){
    super()
    this.sing()
  }
}

var my = new Myskey(); // 我会唱歌!
console.log(my.type);  // animal123

例子2:

abstract class Animal {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract eat(): any;  //抽象方法不包含具体实现并且必须在派生类中实现。
  run() {
    console.log('其他方法可以不实现')
  }
}
// var a = new Animal() /*错误的写法*/

class Dog extends Animal {
  //抽象类的子类必须实现抽象类里面的抽象方法
  constructor(name: any) {
    super(name)
  }
  eat() {
    console.log(this.name + '吃粮食')
  }
}

class Cat extends Animal {
  //抽象类的子类必须实现抽象类里面的抽象方法
  constructor(name: any) {
    super(name)
  }
  eat() {
    console.log(this.name + '吃老鼠')
  }
}

var d = new Dog('小花花');
d.eat(); // 小花花吃粮食
var c = new Cat('小花猫');
c.eat(); // 小花花吃老鼠
  

TypeScript中的接口

接口的作用:在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范,在程序设计里面,接口起到一种限制和规范的作用。

接口定义了某一批类所需要遵守的规范,接口不关心这些类的内部状态数据,也不关心这些类里方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要。

typescrip 中的接口类似于java,同时还增加了更灵活的接口类型,包括属性、函数、可索引和类等

1、ts 中自定义方法传入参数,对 json 进行约束

function printLabel(labelInfo: { label: string }): void {
  console.log('printLabel', labelInfo);
}

// printLabel('张三'); //错误写法
// printLabel({name:'张三'});  //错误的写法
printLabel({label:'张三'});  //正确的写法 printLabel { label: '张三' }

2、接口 interface:行为和动作的规范,对批量方法进行约束

//就是传入对象的约束    属性接口
interface FullName {
  firstName: string;   //注意;结束
  secondName: string;
}

function printName(name: FullName) {
  // 必须传入对象  firstName  secondName
  console.log(name.firstName + '--' + name.secondName);
}
// printName('1213');  // 错误

var obj = {   /*传入的参数必须包含 firstName  secondName*/
  age: 20,
  firstName: '张',
  secondName: '三'
};
printName(obj); // 张--三

修改为:

//就是传入对象的约束    属性接口
interface FullName {
  firstName: string;   //注意;结束
  secondName: string;
  age: number;
}

function printName(name: FullName) {
  // 必须传入对象  firstName  secondName
  console.log(name.firstName + '--' + name.secondName + '--' + name.age );
}
// printName('1213');  // 错误

var obj = {   /*传入的参数必须包含 firstName  secondName*/
  age: 20,
  firstName: '张',
  secondName: '三'
};
printName(obj); // 张--三--20

3、参数的顺序可以不一样

interface FullName {
  firstName: string;
  secondName: string;
}

function getName(name: FullName) {
  console.log(name)
}
// 参数的顺序可以不一样
getName({
  secondName: 'secondName',
  firstName: 'firstName'
})
// { secondName: 'secondName', firstName: 'firstName' }

4、也可以设置可选属性

interface FullName {
  firstName: string;
  secondName?: string;
}

function getName(name: FullName) {
  console.log(name)
}
getName({
  firstName: 'firstName'
})
// { firstName: 'firstName' }

注意:属性接口中定义的属性,在传入的参数必须全部包含,否则会报错。或者可以设置为可选属性。

例如:ajax

interface Config {
  type: string;
  url: string;
  data?: string;
  dataType: string;
}

//原生js封装的ajax 
function ajax(config: Config) {
  var xhr = new XMLHttpRequest();
  xhr.open(config.type, config.url, true);
  xhr.send(config.data);
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {
      console.log('success!!!');
      if (config.dataType == 'json') {
        console.log(JSON.parse(xhr.responseText));
      } else {
        console.log(xhr.responseText)
      }
    }
  }
}

ajax({
  type: 'get',
  data: 'name=zhangsan',
  url: 'http://a.itying.com/api/productlist', //api
  dataType: 'json'
})

TypeScript中的泛型

软件工程中,我们不仅要创建一致的定义良好的 API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

function add<T>(a: T, b: T): void {
  console.log(a)
  console.log(b)
}
add(1, 2) // 1 // 2
add('a', 'b') // a // b

接口 + 泛型 -> 函数类型

let addFn: AddFn<boolean> = function (a, b) {
  console.log(a)
  console.log(b)
  return [{ id: 1, name: '浙大正呈' }]
}
interface arrIF {
  readonly id: number,
  name: string
}
interface AddFn<T> {
  (a: T, b: number): arrIF[]
}

console.log(addFn(false, 2)) 
// false 
// 2
// [ { id: 1, name: '浙大正呈' } ]

TypeScript熟悉使用者?

https://www.jianshu.com/p/0ebd56cb22d2

https://www.bilibili.com/video/BV1Qa4y1E7U1/?spm_id_from=333.788.videocard.15

原文地址:https://www.cnblogs.com/joe235/p/13814753.html