Typescript学习笔记(一)

什么是TypeScript?

TypeScript 是添加了类型系统的 JavaScript,适用于任何规模的项目。
TypeScript 是一门静态类型、弱类型的语言。

安装TypeScript

npm install -g typescript

编译

tsc hello.ts

TypeScript 只会在编译时对类型进行静态检查,如果发现有错误,编译的时候就会报错。而在运行时,与普通的 JavaScript 文件一样,不会对类型进行检查。

TypeScript 编译的时候即使报错了,还是会生成编译结果。

基础

1、原始数据类型

JavaScript基本类型TypeScript基本类型备注
布尔值boolean注意Boolean对象不是布尔值
数值numberES6中的二进制和八进制会被编译为十进制数字
字符串string
空值voidJs中没有空值的概念,在 TypeScript 中,可以用 void 表示没有任何返回值的函数
Nullnull
Undefinedundefinedundefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量,而void 类型的变量不能赋值给 number 类型的变量

2、任意值

  • 任意值(Any)用来表示允许赋值为任意类型。,普通类型在赋值过程中改变类型是不被允许的;
  • 在任意值上访问任何属性都是允许的;
  • 在任意值上调用任何方法都是允许的;
  • 声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。
  • 变量如果在声明的时候,未指定其类型,那么它会被识别为任意值类型

3、类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型。
举个例子:

let a = '123'
a = 1

编译

tsc hello.ts 
hello.ts:2:1 - error TS2322: Type 'number' is not assignable to type 'string'.

2 a = 1
  ~


Found 1 error in hello.ts:2

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

4、联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。
联合类型使用 | 分隔每个类型。

let a: string | number
a = 'app'
a = 1

当一个变量被声明(但未赋值时)为联合类型,仅能访问联合类型中所有类型都共有的属性或方法。
联合类型的变量在被赋值的时候,会根据类型推论的规则推断出一个类,此时可以正常访问该类型的属性或方法。

5、对象的类型——接口

在面向对象语言中,接口(Interfaces)是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类(classes)去实现(implement)。
在 TypeScript 中,我们使用接口(Interfaces)来定义对象的类型。

// 定义接口
interface Animal {
	type: string,
	age: number
}

// 使用该接口声明变量
let cat : Animal = {
	name: 'mimi',
	age: 2
}

可选属性

在使用接口声明变量时,多声明属性或少声明属性都不被允许。
但我们可以使用可选属性来达到不完全匹配接口属性的目的,可选属性的含义是该属性可以不存在,但仍然不允许添加未定义的属性

// 定义接口
interface Animal {
	type: string,
	age?: number
}

任意属性

在接口中定义任意属性,即可让我们能够在声明变量是添加一个任意的属性。

// 定义接口
interface Animal {
	type: string,
	[propName: string]: any;
}

// 声明变量
// 使用该接口声明变量
let cat : Animal = {
	name: 'mimi',
	gender: 'female'
}

注意:一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集:

interface Animal {
	type: string,
	age: number
	[propName: string]: string;
}

上面这段代码会报错,因为任意属性只允许string,但age属性声明为了number

只读属性

使用readonly定义只读属性,该属性仅在创建的时候可被赋值
注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候

6、数组的类型

在TypeScript中可以使用多种方式定义数组:

  • 类型+方括号:let arr: number[] = [1,2,3,4,5]
  • 数组泛型:let arr: Array<number> = [1,2,3,4,5]
  • 用接口便是数组:
interface NumberArray {
	[index: number]: number
}
let arr:NumberArray = [1,2,3,4,5]
// NumberArray 表示:只要索引的类型是数字时,那么值的类型必须是数字。
  • 类数组:
    类数组不是数组类型,比如函数的arguments参数
function sum() {
    let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

上例中,arguments 实际上是一个类数组(常用的类数组有:IArgumentsNodeListHTMLCollection),不能用普通的数组的方式来描述,而应该用接口:

function sum() {
    let args: {
        [index: number]: number;
        length: number;
        callee: Function;
    } = arguments;
}

另外,可以使用any表示数组中允许出现任意类型:

let arr: any[] = ['a', 1, true]

7、函数的类型

js中有两种常见的函数定义方式:

  • 函数声明
  • 函数表达式

函数声明

输入多余的(或者少于要求的)参数,是不被允许的

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

函数表达式

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

在这里插入图片描述
注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>。

在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。

用接口定义函数的形状

interface SearchFunc {
	(source: stirng, subString: string): boolean
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
	return source.search(subString) !== -1
}

可选参数

可选参数必须接在必需参数后面

function buildName(firstName: string, lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');

参数默认值

TypeScript 会将添加了默认值的参数识别为可选参数:

function buildName(firstName: string = 'Tom', lastName: string) {
    return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat'); // Tom Cat
let cat = buildName(undefined, 'Cat'); // Tom Cat

剩余参数

重载

// 函数定义
function reverse(x: number): number;
// 函数定义
function reverse(x: string): string;
// 函数实现
function reverse(x: number | string): number | string | void {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}

注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。

8、类型断言

语法

as 类型
// 例如 mimi as Cat

用途

将一个联合类型断言为其中其中一个类型

我们知道,我们只能访问联合类型中所有类型公有的属性和方法,否则在编译时会报错,为了“欺骗”TypeScript编译器,我们可以使用类型断言

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

但这种方法只能避免编译错误,无法避免运行时错误
使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

将一个父类断言为更具体的子类
class ApiError extends Error {
    code: number = 0;
}
class HttpError extends Error {
    statusCode: number = 200;
}

function isApiError(error: Error) {
    if (typeof (error as ApiError).code === 'number') {
        return true;
    }
    return false;
}
将任何一个类型断言为any
window.foo = 1;

// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.

此时可以使用断言,将window断言为any类型

(window as any).foo = 1

将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。

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

上面的例子中,我们调用完 getCacheData 之后,立即将它断言为 Cat 类型。这样的话明确了 tom 的类型,后续对 tom 的访问时就有了代码补全,提高了代码的可维护性。

类型断言的限制

若 A 兼容 B,那么 A 能够被断言为 B,B 也能被断言为 A

9、声明文件

declare var 声明全局变量
declare function 声明全局方法
declare class 声明全局类
declare enum 声明全局枚举类型
declare namespace 声明(含有子属性的)全局对象
interfacetype 声明全局类型
export 导出变量
export namespace 导出(含有子属性的)对象
export default ES6 默认导出
export = commonjs 导出模块
declare global 扩展全局变量
declare module 扩展模块
/// <reference /> 三斜线指令

声明语句

declare var jQuery: (selector: string) => any;

jQuery('#foo');

声明文件

声明文件即:将声明语句放在一个单独的文件(jQuery.d.ts)中
声明文件必需以 .d.ts 为后缀。
如何书写声明文件?

declare var /declare let/ declare const
declare const name = 'abc'

// 声明语句中只能定义类型,切勿在声明语句中定义具体的实现,以下代码会报错
declare const getAge = function(name: string) {
	return 18
}
declare function
declare function jQuery(selector: string): any;
declare class

也只能定义类型,不能写具体实现

declare class {
	name: string,
	age: number,
	sayHi(): string 
}
declare enum定义外部枚举

声明文件:

// src/Directions.d.ts

declare enum Directions {
    Up,
    Down,
    Left,
    Right
}

使用:

// src/index.ts

let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];

其中 Directions 是由第三方库定义好的全局变量。

npm包

如何看npm包是否存在声明文件?
npm包的声明文件可能存在于2个地方:

  • 与该 npm 包绑定在一起。判断依据是 package.json 中有 types 字段,或者有一个 index.d.ts 声明文件。这种模式不需要额外安装其他包,是最为推荐的,所以以后我们自己创建 npm 包的时候,最好也将声明文件与 npm 包绑定在一起。
  • 发布到 @types 里。我们只需要尝试安装一下对应的 @types 包就知道是否存在该声明文件,安装命令是 npm install @types/foo --save-dev。这种模式一般是由于 npm 包的维护者没有提供声明文件,所以只能由其他人将声明文件发布到 @types 里了。

如果通过以上两种方式没有找到对应的声明文件,则需要自己编写:
创建一个 types 目录,专门用来管理自己写的声明文件,将 foo 的声明文件放到 types/foo/index.d.ts 中。这种方式需要配置下 tsconfig.json 中的 paths 和 baseUrl 字段。
如何编写可参考:http://ts.xcatliu.com/basics/declaration-files.html

模块插件 declare module

有时通过 import 导入一个模块插件,可以改变另一个原有模块的结构。此时如果原有模块已经有了类型声明文件,而插件模块没有类型声明文件,就会导致类型不完整,缺少插件部分的类型。ts 提供了一个语法 declare module,它可以用来扩展原有模块的类型。
扩展原有模块:

// types/moment-plugins/index.d.ts

import * as moment from 'moment' // 引入原模块

declare module 'moment' {
	export function foo(): moment.Calendarkey
}
// src/index.ts

import * as moment from 'moment';
import 'moment-plugin';

moment.foo();
声明文件中的依赖

一个声明文件有时会依赖另一个声明文件中的类型,比如在前面的 declare module 的例子中,我们就在声明文件中导入了 moment,并且使用了 moment.CalendarKey 这个类型:

// types/moment-plugin/index.d.ts

import * as moment from 'moment';

declare module 'moment' {
    export function foo(): moment.CalendarKey;
}

除了可以在声明文件中通过 import 导入另一个声明文件中的类型之外,还有一个语法也可以用来导入另一个声明文件,那就是三斜线指令

三斜线指令用于声明模块之间的依赖关系
类似于声明文件中的 import,它可以用来导入另一个声明文件。与 import 的区别是,当且仅当在以下几个场景下,我们才需要使用三斜线指令替代 import:

  • 当我们在书写一个全局变量的声明文件时
  • 当我们需要依赖一个全局变量的声明文件时

1、书写一个全局变量的声明文件
在全局变量的声明文件中,是不允许出现 import, export 关键字的。一旦出现了,那么他就会被视为一个 npm 包或 UMD 库,就不再是全局变量的声明文件了。故当我们在书写一个全局变量的声明文件时,如果需要引用另一个库的类型,那么就必须用三斜线指令了
三斜线指令必须放在文件的最顶端,三斜线指令的前面只允许出现单行或多行注释

// env.d.ts

/// <reference types="vite/client" />

2、依赖一个全局变量的声明文件
在另一个场景下,当我们需要依赖一个全局变量的声明文件时,由于全局变量不支持通过 import 导入,当然也就必须使用三斜线指令来引入了

// types/node-plugin/index.d.ts

/// <reference types="node" />

export function foo(p: NodeJS.Process): string;
// src/index.ts

import { foo } from 'node-plugin';

foo(global.process);

在上面的例子中,我们通过三斜线指引入了 node 的类型,然后在声明文件中使用了 NodeJS.Process 这个类型。最后在使用到 foo 的时候,传入了 node 中的全局变量 process。
由于引入的 node 中的类型都是全局变量的类型,它们是没有办法通过 import 来导入的,所以这种场景下也只能通过三斜线指令来引入了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值