Typescript学习笔记(一)
-
定义
-
什么是Typescript?
TypeScript带来了可选的静态类型检查以及最新的ECMAScript特性。TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器、任何计算机和任何操作系统上运行,并且是开源的。
- 静态类型
- javascript超集
- 需要编译
-
为什么要使用typescript
回答这个问题我们需要了解javascript的语言特性,javascript是一门动态弱类型语言。
-
静态类型/动态类型
静态类型是指变量类型在声明变量的时候就确定了
动态类型是指变量类型在执行过程中使用到他的时候才能确定他的类型
-
强类型/弱类型
弱类型支持类型转换
强类型不支持类型转换
虽然javascript在使用上很灵活,不过由于其动态弱类型的语言本质就容易在开发过程中会留下一些不易发现的bug,比如调用对象不存在的方法和属性等,这些bug在开发过程中有可能会存在很长一段时间。
而typescript就是为了解决javascript这些“痛点”的一门语言。作为javascript的超集,在javascript的基础上添加了静态类型检查及一些新特性。
- 在开发阶段就能发现因类型问题带来的隐藏bug
- 编辑器提供更好的代码提示
- 更好的可维护性和可读性
-
-
-
静态类型
在使用typescript之前,我们先创建一下文件目录及运行环境
-
基础类型
-
string
-
number
-
boolean
-
null
-
undefined
-
symbol
-
bigint
-
any/unknown
-
void/never
// string let str: string = 'string'; // number let num: number = 100; // boolean let flag: boolean = true; // undefined let un: undefined = undefined; // null let nu: null = null; /** * 默认情况下 null 和 undefined 是所有类型的子类型, * 即null和undefined赋值给number,string,boolean类型的变量(never除外,never表示没有值)。 * 需要将"strictNullChecks": false(在严格模式下不支持) */ str = undefined; // symbol /** * Symbol属于es6的新特性,需要在tsconfig的lib中配置["ES6"] */ let s: symbol = Symbol(); // bigint /** * Symbol属于es6的新特性,需要在tsconfig的lib中配置["ES2020"],target:"ES2020" */ let bigNum: bigint = BigInt(100n); // any/unknown /** * any和unknown都可以表示任意类型,unknown相对于any更加安全: * 在对unknown类型的值执行大多数操作之前,我们必须进行某种形式的检查, * 而在对 any 类型的值执行操作之前,我们不必进行任何检查 * 在使用console的时候需要对lib配置["dom"] */ let obj1: any; let obj2: unknown; console.log(obj1.name); // ok console.log(obj2.name); // false // never/void /** * `never` 类型表示的是那些永不存在的值的类型,never类型是任何类型的子类型, * 也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外) * void 通常用来表示函数没有返回值 */ const arr: never[] = []; // arr永远是空数组 const foo: () => void = () => {}; // void表示foo函数没有返回值
-
-
引用类型
-
object
-
枚举
-
数组
-
元组
// object /** * 普通对象、枚举、数组、元组通通都是 object 类型 */ enum Direction { UP, } let value: object; value = Direction; value = [1]; value = [1, 'hello']; value = {}; // enum /** * 枚举:它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。 * 1.枚举成员的默认值从0开始一次递增,也可以初始化赋值,其中一个赋值为非数字时,后面的枚举成员必须初始化。 * 2.枚举可以反向映射 * 3.分开声明枚举,他们会自动合并 */ enum Color { RED, YELLOW, BLUE, } enum Color { GREEN = 3, } console.log(Color.RED === 0); // true console.log(Color.YELLOW === 1); // true console.log(Color.BLUE === 2); // true console.log(Color.GREEN === 3); // true console.log(Color[0]); // RED console.log(Color[1]); // YELLOW console.log(Color[2]); // BLUE console.log(Color[3]); // GREEN // 数组 /** * 组有两种类型定义方式 */ let arr1: Array<number> = [1, 2, 3]; // 泛型 let arr2: number[] = [123]; // 元素类型后面接上 [] // 元组 /** * 元组和数组非常相似,唯一区别就是元素数量在声明的时候就知道了 * 元组数量确定后可以使用push方法不会报错,但是不能通过下标获取这个值 */ let tuple: [number, string] = [1, '2']; tuple.push(3); // console.log(tuple[2]); //报错
-
接口
接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。
typescript的声明如下
interface IUser{}
-
接口可以用来描述参数的结构
interface IUser { name: string age: number } const getUserName = (user: IUser) => user.name
这个接口
User
描述了参数user
的结构,当然接口不会去检查属性的顺序,只要相应的属性存在并且类型兼容即可 -
接口的可选属性
interface IUser { name: string; age: number; sex?: 'male' | 'female'; } const user:IUser = { name:"zhangsan", age:18, }
通过
?
可以用来约定可选参数,在使用IUser
接口时sex
属性就变为非必选属性。 -
接口的只读属性
我们确定
user
的名字之后就不允许修改了,interface
可以利用readonly
我们可以把一个属性变成只读性质interface IUser { readonly name: string; age: number; } const user: IUser = { name: 'zhangsan', age: 18, }; user.name = 'lisi'; // 报错
-
接口的扩展属性
我们在定义接口时,
user
除了name和age属性外,有可能还有其他的属性,可以通过添加字符串索引签名。interface IUser { readonly name: string; age: number; [prop: string]: string | number; // 值的类型必须兼容其他属性的类型 } const user: IUser = { name: 'zhangsan', age: 18, sex: 'male', // 添加额外的属性 };
-
接口可以被继承,可以继承多个接口
interface IBoy extends User { run: () => void }
-
-
类
传统的面向对象语言基本都是基于类的,JavaScript 基于原型的方式让开发者多了很多理解成本,在 ES6 之后,JavaScript 拥有了 class 关键字,虽然本质依然是构造函数,但是开发者已经可以比较舒服地使用 class了。但是 JavaScript 的 class 依然有一些特性还没有加入,比如修饰符和抽象类等。
-
抽象类
abstract class Animal { abstract eat(): void; run(): void { console.log('running...'); } } class Cat extends Animal { eat() { console.log('eatting...'); } } const cat = new Cat(); cat.run(); cat.eat();
- 抽象类不能实例化
- 抽象类可以定义抽象方法,抽象方法没有具体实现,需要在子类中实现父类抽象方法
-
访问修饰符
public:在 TypeScript 的类中,成员都默认为 public, 被此限定符修饰的成员是可以被外部访问。
private:当成员被设置为 private 之后, 被此限定符修饰的成员是只可以被类的内部访问。
protected:当成员被设置为 protected 之后, 被此限定符修饰的成员是只可以被类的内部以及类的子类访问。
class Animal { public run() { console.log('animal running...'); } private eat() { console.log('animal eating...'); } protected sing() { console.log('animal singing...'); } } class Cat extends Animal { public sing() { return super.sing(); } } const animal = new Animal(); const cat = new Cat(); animal.run(); animal.eat(); // error animal.sing(); // error cat.sing();
-
-
函数
-
定义函数类型
// 函数声明 function add1(x: number, y: number): number { return x + y; } // 函数表达式 const add2: (x: number, y: number) => number = ( x: number, y: number ): number => { return x + y; };
-
函数的参数
// 可选参数 const add1 = (a: number, b?: number) => a + (b ? b : 0); // 默认参数 const add2 = (x: number, y = 5): number => x + y; // 剩余参数 const add3 = (x: number, ...args: number[]): number => args.reduce((x, y) => x + y, x);
-
函数的重载
function add(x: number, y: number): number; function add(x: string, y: string): string; function add(x: number | string, y: number | string): number | string { if (typeof x === 'number' && typeof y === 'number') { return x + y; } return `${x}${y}`; }
-
-
-
特殊类型
-
泛型
-
泛型概念
泛型是 TypeScript 中非常重要的一个概念,原因就在于泛型给予开发者创造灵活、可重用代码的能力。
假设我们用一个函数,它可接受一个 number 参数并返回一个 number 参数。
function returnPara (para: number): number { return para }
如果我们要接受一个 string 并返回同样一个 string 呢?
function returnPara (para: string): string { return para }
这明显是重复性的代码,我们应该如何才能避免上述情况呢?我们在静态编写的时候并不确定传入的参数到底是什么类型,只有当在运行时传入参数后我们才能确定。那么我们需要变量,这个变量代表了传入的类型,然后再返回这个变量,它是一种特殊的变量,只用于表示类型而不是值。这个类型变量在 TypeScript 中就叫做「泛型」
function returnPara<T>(para: T): T { return para }
-
泛型接口
泛型也可用于接口声明,以上面的函数为例,如果我们将其转化为接口的形式。
interface ReturnParaFn<T> { (para: T): T } // 那么当我们想传入一个number作为参数的时候,就可以这样声明函数: const returnPara: ReturnParaFn<number> = para => para
-
泛型约束
现在有一个问题,我们的泛型现在似乎可以是任何类型,但是我们明明知道我们的传入的泛型属于哪一类,比如属于 number 或者 string 其中之一,那么应该如何约束泛型呢?
function returnPara<T extends number | string>(para: T) { return para; }
我们可以用
<T extends xx>
的方式约束泛型,我们约束泛型为 number 或者 string 之一,当传入 其他类型的时候,就会报错。
-
-
字面量类型/类型字面量
-
字面量类型
字面量(Literal Type)主要分为 真值字面量类型(boolean literal types),数字字面量类型(numeric literal types),枚举字面量类型(enum literal types),大整数字面量类型(bigInt literal types)和字符串字面量类型(string literal types)
const a : 9527 = 9527 // ok const b : 0b10 = 2 // ok const c : 0o114 = 0b1001100 // ok const d : 0x514 = 0x514 // ok const e : 0x1919n = 6425n // ok const f : 'haha' = 'haha' // ok const g : false = false // ok const h: 'test' = 'text' // 不能将类型“"pronhub"”分配给类型“"github"”
字面量类型的要和实际的值的字面量一一对应,如果不一致就会报错,比如最后一个例子中字面量类型是
test
,但是值却是text
,这就会产生报错.当字面量类型与联合类型结合的时候,用处就显现出来了,它可以模拟一个类似于枚举的效果:
type Direction = 'UP' | 'DOWN' | 'LEFT' | 'RIGHT'; function move(distance: number, direction: Direction) { // ... }
-
类型字面量
类型字面量对应的就是 js 中的对象字面量。所以他们的语法很相似。(可以理解为对象字面量,只不过不同于对象字面量,类型字面量的属性可以添加修饰符,也可以是一个函数)
type Handler = Function; type Foo = { baz: [ 1, number, { y: Array<string>; } ]; readonly handler: Handler; toString(): string; readonly [Symbol.iterator]: '123'; 0x1: 'foo'; "bar": number; };
我们可以把一对键值对叫做 Signature,一个类型字面量由花括号和 signature 列表组成,上面的例子中实际上由两种 signature: 一种是 toString 这种,叫做 method signature,与 js 中的 object literal 中的 method。它不能设 readonly 作为 modifier,它只能是可读可写的。上面例子中,其余的都是 property signature,property signature 中的 key 部分,也可以是字符串和数字的字面量,标识符(baz, handler),也可以是 computed property name,比如 [Symbol.iterator]。它可以设置 readonly ,默认是可读可写的。
-
类型别名
类型别名会给一个类型起个新名字,类型别名有时和接口很像,但是可以作用于原始值、联合类型、元组以及其它任何你需要手写的类型.
type test = number | string const b: test = 1 // ok const c: test = 'hello' // ok const d: test = true // 不能将类型"true"分配给类型"test"
-
类型别名与接口的区别
- interface 只能用于定义对象类型,而 type 的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛。
- interface 方式可以实现接口的 extends 和 implements
- interface 可以实现接口合并声明
-
-
-
-
高级类型
-
交叉类型/联合类型
-
交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性
interface A { name: string; } interface B { age: number; } const obj: A & B = { name: 'zhangsan', age: 18, };
A&B表示交叉类型,相当于A,B类型的并集。
-
联合类型
当你希望属性为多种类型之一,如字符串或数字。这就是联合类型所能派上用场的地方(它使用 | 作为标记,如 string | number)
let a: number | string; a = 123; a = '123';
-
-
索引类型
我们先看 一个例子
function foo(obj: any, arr: string[]) { return arr.map((key) => obj[key]); } const obj = { a: '123', b: 456, c: '789', }; console.log(foo(obj, ['a', 'b'])); ["123",456] console.log(foo(obj, ['e', 'f'])); [undefined,undefined]
上面的例子中"a","b"都是obj的属性,所有我们可以拿到foo的返回值,当arr的元素值不是对象的属性时,我们希望typescript给我们做出提示,即我们希望arr的元素时对象的属性时,这时候就要用到索引 类型。用
keyof
来限定数组的取值范围。// 索引类型; const obj = { a: 1, b: 2, c: 3, }; function foo<T, U extends keyof T>(obj: T, arr: U[]): T[U][] { return arr.map((item) => obj[item]); } console.log(foo(obj, ['a', 'b'])); console.log(foo(obj, ['e', 'f'])); // 报错
-
映射类型
我们有一个IUser接口,现在有一个需求是把IUser接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个
:
前面加上?
,有没有更便捷的方法// 映射类型 interface IUser { name: string; age: number; sex: string; }
这个时候映射类型就派上用场了,映射类型的语法是
[K in Keys]
:- K:类型变量,依次绑定到每个属性上,对应每个属性名的类型
- Keys:字符串字面量构成的联合类型,表示一组属性名(的类型)
首先,我们得找到
Keys
,即字符串字面量构成的联合类型,即keyof
操作符,假设我们传入的类型是泛型T
,得到keyof T
,即传入类型T
的属性名的联合类型。然后我们需要将
keyof T
的属性名称一一映射出来[K in keyof T]
,如果我们要把所有的属性成员变为可选类型,那么需要T[K]
取出相应的属性值,最后我们重新生成一个可选的新类型{ [K in keyof T]?: T[K] }
。即:
type partial<T> = { [K in keyof T]?: T[K] }
其实typescript已经为我们暴露了这些接口,我们可以直接使用:
// 映射类型 interface IUser { name: string; age: number; sex: string; } type PartialObj = Partial<IUser>; // 可选 type ReadonlyObj = Readonly<IUser>; // 只读
-
条件类型
所谓的条件类型,通过条件表达式进行类型关系检测,从而确定两种类型中的一个,形如A?A:B,和映射类型一样,typescript也为我们提供了很多条件类型接口,方便我们使用。
// 条件类型 type Diff<T, U> = T extends U ? never : T; type diff = Diff<'a' | 'b' | 'c', 'a'>; // 等价于 type diff = Exclude<'a' | 'b' | 'c', 'a'>;
-
-