typescript :一个基于Structural Type System(结构类型系统)的静态类型检查器,相对于javascript而言,在你运行代码前,捕捉到错误并修复。
从typscript 的定义得出2个关键点:
- 结构类型:一个对象从类型角度的去描述其结构,侧重的是
类型
和结构
, 如:一个数组的里面每一个元素是什么类型 - 预期目标:在代码运行前,就捕捉到错误,尽可能早的帮你发现错误
function print(name: string, age: number) {
console.log(name,age)
}
从类型,结构去分析函数 print的第一个参数name,必须是一个string 字符串,第二个参数age,必须是一个number数字,这2个组在一起就描述了函数的参数类型结构。
一个是类型,一个是结构,是typescript 进行静态类型检查的基础。
Basic Type 基本类型
typescript基本类型主是一些基本的数据类型,它是组合成复杂类型的基本单位。就像学习文字一样,他是笔画。
1.布尔值类型
// boolean 布尔值
let boo: boolean = true
2.字符串类型
// string 字符串
let str: string = 'hello world'
3.数字类型
// number 数字
let num: number = 0
4.null 类型 和 undefined 类型
// undefined 表示没有定义,声明一个变量,属性是undefined, 实际用处不大
let unde: undefined = undefined;
// undefined 实际开发中,比较多的用来表示一个变量,属性,参数的可选性,未定义
let num: undefined | number = 0;
// 同样,声明一个变量是null,实际用处也不大,更多是用在联合类型中
let maybe: null | string = null;
// 同时他们是所有其它类型的子类型
let str: string;
let a: undefined = undefined;
let b: null = null;
// 类型检查是通过的,因为 undefined, null 是其它类型的子类型
str = a;
str = b;
// 很明显是报错了,实际开发中不会单独定义一个变量是undefined ,null
// 更多是一个可选,联合类型,那为typescript 什么要这样设计呢?确实有点奇怪
str.toString(); // Uncaught TypeError: Cannot read property 'toString'
// 打开 --strictNullChecks 检查, null and undefined 只可以赋值给 unknownn, any
// 这样可以很好的规避上面的问题
let c: unknown = null;
5.never 类型
// 代表是永远不可能发生,不可能到达
// 它是任何类型的子类型
// 因为它是不可发生,不可到达的,任何其它类型都不可以作为它的子类型,any 也不可以,只能赋值给自己
// 这样做是就算赋值给never了,也没有实际意义了
// 返回是一个never 报错啦,不可能发生
function fail() {
return error("Something failed");
}
// 很明显是一个死循环,永远不可能到达
function infiniteLoop(): never {
while (true) {}
}
let nev1: never;
let nev2: never;
let anyType: any;
let str: string;
let num:number
// 因为它是不可以作为任何类型的子类型,只能是赋值给自己
str = nev1
num = nev1
// 任何类型都不可以作为 never 子类型的,因为没有实际意义
nev1 = anyType // error
// never 可以赋值给自己,那有什么实际用处呢?
nev1 = nev2
6.any 类型
// any 一个没有约束的推导,任何推导都成立
const word: any = 'any type will go'
// 声明 word的类型是 any, typescript 在进行类型检查时,就会认为类型推导都是成立
// 换一个讲法就是不做类型检查了,随便你怎么写都可以
// 但实际运行时,会报错 Uncaught TypeError: word.action is not a function
word.action()
7.空类型void
// void 为一个空值,用来表示一个函数没有返回值
// 如函数没有return,实际是返回一个undefined,
// 就是说undefined 是 void的子类型
function test():void {
// 没有return 任何值
}
// 声明一个变量是一个void 空值,实际用处不大
let unusable: void = undefined;
// 只有关闭 `--strictNullChecks`,null 才可以赋值给void
unusable = null;
8.unknown 类型
// unknown 暂时不知道,但给我更多的信息,就可以推断出来
// 像是一个侦探,逐步收窄范围,明确类型
let maybe: unknown;
// 收窄范围是一个number 数字类型
if (typeof maybe === "number") {
let num: number = maybe;
// 此时maybe 是数字类型,不可以赋值给string 类型,明显不兼容
let str:string = maybe; // error
}
9.数组类型
// 有2种表示方式
// 方式一,推荐这种方式,更加简洁
let list: number[] = [1,2,3]
// 方式二
let arr: Array<string> = ['a','b','c']
10.元组类型
// tuple 元组
let pairs: [ string, number ] = []
11.枚举类型
// enum 枚举
enum Status{
Draft = 1 // 如何指定,默认是从0 开始的
Audit = 3
}
let status:Status = Status.Draft
12. Object
// Object 对象
// anything that is not number, string, boolean, bigint, symbol, null, or undefined
// 换一个说法就是js 里面基本数据类型
// 建议开发时,尽量不要使用,因为提供不了任何有用的类型信息
const obj: object = null;
// 类型推导成立的,实际运行时报错
obj.toString();
enum 枚举
javascript 没有提供枚举这个数据类型,它是typescript 的提供数据类型之一。从枚举成员的值,可以分为3类:
- 数字枚举:每一个枚举成员的值是一个 number 数字
- 字符串枚举:每一个枚举成员的值是一个 string 字符串
- 混合枚举:每一个枚举成员的值,可能是一个number 数字,或者是一个 string 字符串,只是技术上支持,不建议使用
// 数字枚举
// 只初始化第一个,后面的会默认加 1
// 那么 Error 值是2,Warning 值是3,依此类推...
enum Status {
Success = 1,
Error, // 2
Warning, // 3
Info, // 4
}
// 字符串枚举
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
// 字符串枚举
// 技术上支持,违反单一职责,不建议使用
enum MixDirection {
Up = 1,
Down = "DOWN",
}
断言
为什么要使用断言?有时候根据目前掌握的类型信息,typescript进行推导时,并没有开发者都了解更多,推导出来的不一定准确。
const el = document.getElementById("id");
// 类型报错:属性 autocomplete 不一定在 'HTMLElement' 上
// 很明显,开发者是知道el 是什么类型,比如是一个textarea,想设置使用浏览器的记忆功能自动填充文本
el.autocomplete = 'on';
// 使用断言, 告诉typescript el 是一个 HTMLTextAreaElement 类型
const el = document.getElementById("id") as HTMLTextAreaElement;
el.autocomplete = "on";
// 断言有2种表示方式,一种是关键字 as,一种是尖括号
// as关键字方式
// 注意在jsx 中,只允许使用 as 这种方式
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;
// 尖括号方式
let otherValue: unknown = "this is a string";
let strLen: number = (<string>otherValue).length;
Inteface 接口
接口:是typesctipt一个核心概念,进行类型检查侧重于值的形状,兼容是基于结构子类型
的。理论上interface 可以表示任何类型,比如:object对象,fuction函数, class类
如何用接口描述一个对象的类型?
从类型角度去描述,一个object对象的属性?有以下的几个方面
- 有哪几个属性?指数量方面的
- 具体一个属性:可选,还是必须要提供的
- 属性赋值操作:是可读还是可写
interface ObjectShape {
// 必须要提供的属性:一个冒号 + 类型声明
name: string;
// 一个问号 '?':表示是一个可选属性,说明这个属性不一定存在
sex?: number;
// readonly 修饰符:表示只读属性,是不可以修改
readonly height: number;
// 可选的且只读的属性:通过readonly + '?', 2个修饰符进行组合
readonly hobby?: string;
// 索引签名:index type
// 有很多属性,不能一一列举,数量方面的一个泛指
[prop: string]: unknown;
}
如何用接口描述一个函数的类型?
// funciton shape
interface FuncShape {
(str: string, num: number): void;
}
// 第一个参数 str与s 名称不一样,第二个参数 num 与 n 名称不一样
// typescript 在进行类型检查时,侧重的是类型,形状结构
// 只要形状一致,具名参数名,可以不一样
const func: FuncShape = function (s: string, n: number) {};
如何用接口描述一个class的类型?
一个class类,要进行类型描述主要有3个部分:
- 构造函数:参数的类型
- 属性:可见性,访问的范围控制
- 方法:因为方法本质就是一个函数,其它就是对函数类型的一个声明
// 在描述一个class的类型时,在心中要明确记住一点:class 的类型分2侧:静态侧和实例侧的
// 静态侧是class 本身的,实例侧是new 操作生成实例的, 要分开处理
// 用interface 有2种方法描述一个class 的类型
// 构造函数是静态方法,要单独声明
interface ConstructorShape {
new (str: string): InstanceShape;
}
// 实例的类型
interface InstanceShape {
print(str: string): void;
}
// 方法一:以把构造函数以参数的形式传入到另外的一个函数中
function createInstance(ctor: ConstructorShape, str: string): InstanceShape {
return new ctor(str);
}
// 方法二:用使用类表达式,推荐你应该使用第二种,更加的直观简单
const InstacneClass: ConstructorShape = class InstacneClass
implements InstanceShape {
constructor(str: string) {}
print(str: string) {
// do something
}
};
const instance: InstanceShape = new InstacneClass("hello world");
上面的代码只对构造函数:参数的类型进行说明,第2,3点的是
实例侧
的,篇幅有限,不作详细说明。可以看后面的class 类型这一个章节子类,派生类,更贴近实际开发
interface 相互之间的继承
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
// 就这样Square 通过 extends 关键字,支持多重继承
// 从 Shape 中继承 color:sting 的类型声明
// 从 PenStroke 中继承 penWidth: number 的类型声明
interface Square extends Shape, PenStroke {
sideLength: number;
}
interface 继承 Class
接口继承一个类的class 成员类型声明, 注意的是class 中 private 声明的成员,只能通过继承声明类得到,就也是在为它的子类。
// unique 是这个类的私有属性,只能通过成为子类获得
class Animal {
private unique: string;
}
interface Dog extends Animal {
run(): void;
}
// Tendy 不是 Animal 的子类,因此它是没有 unique 这个私有属性的
// 但是Dog 继承了Animal 的成员声明,必须要提供 unique
// 综上 Tendy 没有实现 Dog 接口,类型报错
class Teddy implements Dog {
run() {
// do something
}
}
// Husky 是 Animal 子类,获得 unique 属性
class Husky extends Animal implements Dog {
run() {
// do something
}
}
class 类的类型声明?
一个class类型检查分为2部分:一部分是:class 本身,另一部分是:子类,派生类,new 生成的实例
- class 本身一个静态属性/方法:使用关键字 static 声明
- 子类,派生类,new 生成的实例:通过puublic, private, and protected 修饰符实现
class Production {
// 静态部分:
constructor(readonly type: string) {} // 构造函数静态方法,原生自带
// 静态属性
static uuid = "__hash__";
// 静态方法
static action() {
// do something
}
// 子类,派生类,new 生成的实例
// 只允许在Production类内部访问
private secret: string; // #name: string; 新的版本规范这样定义私有的
// 放宽一点,允许在Production类内部,子类中访问
protected serial: number;
// 实例中访问,不作限制了
public price: number;
// 支行修饰符组合
public readonly level: number = 0;
public qualified(str: string = "") {
console.log(`${Production.uuid === this.secret + str}`);
}
// 计算属性 accessors
get fullName() {
return "hello kitty";
}
set fullName(str) {
// do something
}
}
抽象类
要对一系列
类进行抽象,同定义各个类去实现,比如:对系列手机进行抽象,手机有3个核心的功能:1.打电话 2.发信息
abstract class Phone {
// 定义抽象的方法,要在子类中分别实现
abstract call(): string;
abstract message(): string;
}
class Nokia implements Phone {
call() {
return "Nokia is calling the number";
}
message() {
return "Nokia";
}
}
class Motorola implements Phone {
call() {
return "Motorola is calling the number";
}
message() {
return "Motorola";
}
}
使用一个类作为接口
// 请参考前面 interface 继承 Class
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };
Function 函数的类型
一个函数类型声明可以分为2个部分:函数的参数类型,还有return 的返回值类型
// 参数 str 类型是字符串且必须提供的
// 参数 num 是一个数字,可选参数
// 参数 sex 是一个数字,可选参数,不提供时使用默认值0
// 参数 restParams 是一个剩余参数,允许任何类型
// 函数的返回值是一个 字符串
function print(
str: string,
num?: number,
sex = 0,
...restParams: unknown[]
): string {
// do something
return "hello world";
}
this 执行上下文
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:
- 如果该函数是一个构造函数,this指针指向一个新的对象
- 在严格模式下的函数调用下,this指向undefined
- 如果该函数是一个对象的方法,则它的this指针指向这个对象
- 等等
箭头函数的上下文:
- 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。
- 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
const deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// 尽管实际运行时是指向 deck 这个对象,
// 但是 typescript 进行静态类型检查,要在编译成javascipt 前就要明确 this 的指向
// 则必须在函数参数中声明 this 的指向的类型
createCardPicker: function (this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
};
},
};
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
函数重载
// overloads 函数的重载
function pick(input: string): string;
function pick(input: number): number;
// 只允许sting , number
// 最后一个,只是一个兼容声明,
function pick(input: unknown): unknown {
// 输入的参数是字符串
if (typeof input === "string") {
return "我是一个字符串";
}
// 输入的参数是数字
if (typeof input === "number") {
return input + 10;
}
// 布尔值
return true;
}
let str: string = pick("hey");
let num: number = pick(2);
// 不在重载的类型范围中,类型报错
let other: boolean = pick(true); // error
泛型
一个'泛'字,足以说明泛指一个类型范围。实际上泛型就是一开始不指定具体的类型,之后通过类型参数明确具体的类型,然后通过类型关系处理明确类型范围。
泛型的2个关键点:
- 类型参数:类型以参数形式传递给对应泛型声明,有的泛型会提供默认参数类型,使用时可以不提供类型参数
- 类型关系:类型传给泛型,可以直接使用,也可以进一步处理,得到不一样的类型范围
泛型之所以被认为复杂,主要是类型关系的处理,类型参数的不一样,使类型发生变化。不过,明确上面的提供的2点,其实也很好把握。
// 在接口 interface 中使用泛型
/**
* 首先明确一点,T 是一个类型参数
* T extends string 表示:类型参数 T 满足于 string, 从而对输入的参数进行约束
*/
interface Animal<T extends string> {
// 每一个键的类型是string,
// 对应值是:每一个元素为T类型的数组
[k: string]: T[];
}
// 字符串 abc 是 string 类型中的一个子类型,符合对类型参数T约束
const dog: Animal<"abc"> = {
list: ["abc"],
};
// 在 class 类中使用泛型
/**
* T 是一个类型参数
*/
class GenericObject<T> {
// zeroValue 属性的类型是 T
zeroValue: T;
// 参数 x 的类型是 T
// 参数 y 的类型是 T
// 返回值类型也是 T
add: (x: T, y: T) => T;
}
// 给类型参数赋值为 number 类型
const numObj: GenericObject<number> = {
zeroValue: 0,
add(x: number, y: number): number {
return x + y;
},
};
类型别名
类型别名:本质上是为类型声明指定一个名称,是类型声明的一个引用。在官方文档中,叫别名
给人的感觉,就是一个类型的已经存在,只是另起一个新名称。的确,type alias 类型别名,在实际开发,对已有的类型的扩展,重新组合,声明联合或元组类型,更推荐使用别名。
// 给 number 类型指定一个声明
type numRef = number;
const num: numRef = 10;
// 给泛型类型声明指定一个名称
type GenericType<T = number> = T;
type Tree<T = string> = {
value: T;
left?: Tree<T>;
right?: Tree<T>;
};
// 给对已有类型的重新组合,指定一个名称
type CompositeType = GenericType & Tree;
接口 vs 类型别名?
接口和类型别名在语法上,比较相似。很的必要放在一起加以比较以加深理解。类型别名是本质上是类型引用,他们主要有2个方面的区别?
- 使用一个
新名称
,对已有类型的扩展,他们是一样的 - 直接在原类型上扩展,名称不变
区别一:
interface Animal {
name: string
}
// 定义了一个新接口 Bear
interface Bear extends Animal {
honey: boolean
}
const bear = getBear()
bear.name
bear.honey
type Animal = {
name: string
}
// 定义了一个新类型 Bear
type Bear = Animal & {
honey: Boolean
}
const bear = getBear();
bear.name;
bear.honey;
区别二:
interface Window {
title: string
}
// 接口进行合并,就是merge
interface Window {
ts: import("typescript")
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
type Window = {
title: string
}
// 不能进行合并,2个名称都叫 Window 发生命名冲突
type Window = {
ts: import("typescript")
}
// Error: Duplicate identifier 'Window'.
typscript 文档中指出:因为interface 更加接近javascript 对象对扩展的开放,如果可能,我们推荐使用interfade. 从另一方面,如果你不能只使用一个接口表达出类型的形状,你需要使用别名去处理联合类型,元祖类型,这也是我们经常使用的方式。
接口interface, 类型别名的使用建议
- 优先使用interface: 如果是单一接口,并可能在原基础上扩展,且符合单一职责
- 优先使用
类型别名
:对已有类型扩展,侧重的是类型关系重构实现扩展
结束语
有兴趣的可以看一下,typescript 基础知识,试写一下, 侧重的是对基础的练习。