标签:服务 机制 上下文环境 枚举类 his less 函数声明 默认 规范
TypeScript是微软开发的一门编程语言,它是JavaScript的超集,即它基于JavaScript,拓展了JavaScript的语法,遵循ECMAScript规范(ES6/7/8+)。
TypeScript = Type + Script(标准JS),它可以编译成纯JavaScript,已经存在的JavaScript也可以不加改动地在TS的环境上运行。
目前, Angular 已经使用 TypeScript 重构了代码,另一大前端框架 Vue 的3.0版本也将使用 TypeScript 进行重构。在可预见的未来,TypeScript 将成为前端开发者必须掌握的开发语言之一。
npm install -g typescript // 安装ts编译器 tsc hello.ts // 手动编译ts文件,会生成同名js文件 tsc --init // 生成tsconfig.js文件
当然,我们可以配置webpack,开启node服务,进行热更新开发。
学习数据类型前,要先明白两个概念:
强类型指一个变量一旦声明,就确定了它的类型,此后不能改变它的类型。弱类型可以随便转换。TypeScript是强类型语言,JavaScript是弱类型语言。
静态类型语言:编译阶段检查所有数据的类型。动态类型语言:将检查数据类型的工作放在程序的执行阶段,也就是说,只有在程序执行时才能确定数据类型。
在ES6的基础上,新增了void、any、never、元组、枚举、高级类型。
布尔、数字、字符串
let bool: boolean = true; let num: number = 10; let str: string = "abc"; let abc: number | boolean | null; // 可以为一个变量声明多种类型,除非有特殊需求,一般不建议这样做。
另外,ts字符串有一些特性:
1.多行字符串
·aa
bb
cc·
反引号包着,直接换行即可,编译成js后,会加上\n换行符
2.自动拆分字符串
在调用函数时,可以将模板字符串拆分,并且不需要写小括号
function test(a:string,b:string,c:number){console.log(a,b,c)}
test`string...${name}...${age}...` // 注意调用方式,没有括号,直接反引号
会将字符串拆分为第一个参数,name拆分成第二个参数,age为第三个参数
数组
TypeScript的数组,所有元素只能是同一种数据类型。
let arr1: number[] = [1,2,3]; let arr2: Array<number> = [4,5,6]; // 数组的第二种声明方式,与前面等价 let arr3: string[] = ["hello","array","object"];
元组
元组是特殊的数组,限制了元素的个数和类型。
let tuple: [number,string,boolean] = [10,"hello",true]; // tuple.push(5); // 元组越界,但不会报错。原则上不能对tuple push,这应该是一个缺陷 // console.log(tuple[3]); // 新增的元素无法访问。
函数
// 方式一 箭头函数:声明和定义分开
let compute: (x:number, y:number) => number; // 函数声明,规定了传入参数、返回值的数据类型
compute = (a, b) => a+b; // 函数定义时,参数名称不必与声明时的相同
// 方式二:箭头函数:声明的同时定义
let add = (x:number, y:number) => { return x+y }; // 返回值的类型可省略,这利用了ts的类型推断功能
// 方式三:function关键字:声明和定义分开
function add (x: number,y: number): number;
// 方式四:function关键字:声明的同时定义
function add (x: number,y: number): number{ retrun x+y; }
//函数传参:
function add789(x: number, y?: number, z=1, ...rest: number[]) {
console.log(rest);
return y ? x+y : x
}
对象
// 正确的写法
let obj1: {x:number, y:number} = {x: 1, y: 2};
obj1.x = 3;
// 不建议的写法
let obj: object = {x: 1, y: 2};
obj.x = 3; // 报错,因为定义的时候绕过了声明检查,此时不知道是什么类型。
symbol
let s1: symbol = Symbol(); let s2 = Symbol();
undefind、null
let un: undefined = undefined; let nu: null = null; nu = null; // 这样是允许的,需要将tsconfig中“strictNullChecks”置为false un = undefined;
void
是一种操作符,可以让任意一个表达式返回undefined。之所以引进void,是因为undefined不是一个保留字,可以在局部作用域内将其覆盖掉。
let noReturn = () => {};
any
any 表示变量可以为任何类型,在TS中一般不用它。如果使用它,也便失去了使用TS的意义,与前面不建议为变量声明多种类型是一个道理。
let x: any; x = 1; x = "str";
never
表示永远不会有返回值的类型
let error = () => {
throw new Error("errors")
};
let endless = () => {
while(true)
{}
};
// 以上两个例子永远不会有返回值
枚举类型 enum
枚举主要来定义一些常量,方便记忆,减少硬编码,增加可读性。
基本使用:
// 数字枚举
enum Role {
Reporter, // 默认从0开始
Developer=5, // 也可指定某个值
Maintainer,
}
console.log(Role); // 可以看到数据结构,能够进行反向映射,即通过值来访问成员
console.log(Role.Developer); // 访问枚举成员
// 字符串枚举 不可以进行反向映射
enum message {
success = "成功了",
fail = "失败了"
}
console.log(message);
// 异构枚举,将数字和字符串混用
enum Answer {
N = 0,
Y = "yes"
}
※ 注意:不能修改枚举成员的值
枚举成员的分类:
枚举成员的分类:
(1)常量枚举成员
(2)对已有枚举成员的引用
(3)常量表达式
(4)非常量表达式。这种成员的值不会在编译阶段确定,在执行阶段才会有
例:
enum Char {
a,
b = 9,
c = message.success,
d = b,
e = 1 + 3,
f = Math.random(),
g = "123".length,
}
console.log(Char);
常量枚举和枚举类型
// 常量枚举 用const声明的枚举都是常量枚举。特性:在编译阶段被移除,编译后不会出现
// 作用:当我们不需要对象,只需要对象的值的时候
const enum Month{
Jan,
Feb,
Mar,
}
let month = [Month.Jan,Month.Feb,Month.Mar];
console.log(month);
// 枚举类型 枚举可以作为一种类型
let e: Role = 2;
let e1: Role.Developer = 12; // 数字枚举类型与number类型相互兼容,因此可以复制
console.log(e,e1); // 按照Role的类型去声明新变量
let g1: message.success = "hello"; // 报错,字符串枚举类型message.success与string类型不兼容
e === e1; // 可比较
e === g1; // 不可比较,因为类型不一样
接口可以用来约束对象、函数、类的结构和类型,是一种契约,并且声明之后不可改变。
interface List {
id: number;
name: string;
}
interface Result {
data: List[]; // 表示由List接口组成的数组
}
function render(result:Result) {
result.data.forEach((value)=>{
console.log(value);
})
}
let result = {
data:[
{id:1,name:"a"},
{id:2,name:"b"},
],
};
render(result);
通过上述例子,看到接口规范了成员名称、成员的的类型、值的类型。
此外,还可以规范成员属性。
可选属性和只读属性
interface List {
readonly id: number; // readonly表示只读属性
name: string;
age?: number; // ?表示可选属性
}
当不确定接口中有多少属性的时候,可以用索引签名。
格式:[x: attrType]: valueType 分别规定了成员的类型和值的类型,即通过什么来索引和访问的值的类型。
一般通过数字和字符串来索引,也可以两者混合索引。
// 用数字索引
interface StringArray {
[index: number]: string; // 表示,用任意的数字去索引StringArray,都会得到一个string。这就相当于声明了一个字符串类型的数组
}
let chars: StringArray = ["A","B"]; // 此时,chars就是一个字符串数组,我们可以用下标去访问每个元素
console.log(chars,chars[0]);
// 用字符串和数字混合索引
interface Names {
[x: string]: string; // 用任意的字符串去索引Names,得到的结果都是string。
// y: number; // 此时不能声明number类型的成员
// [y: number]: number // 报错,因为x和y的值string和number类型不兼容
[z: number]: any; // 两个签名的返回值类型之间要相互兼容。为了能保持类型的兼容性。
}
let names: Names = {"ming":"abc",1:"45"};
console.log(names[1],names["ming"]); // 通过数字索引、通过字符串索引
※ 注意值的类型要兼容
(1)索引签名和普通成员
如果设置了[x: string]: string,不能再设置y: number。如果设置了[x: string]: number不能再设置y: string
(2)索引签名和索引签名
如果多个索引签名的值不同,要注意相互兼容,比方any和string
如果在接收的后端数据中,比约定好的接口多了一个字段,能否通过类型检查?会不会报错?
let result = {
data:[
{id:1,name:"a",sex:"man"},
{id:2,name:"b"},
],
};
render(result); // 这样是不会报错的,只要满足接口约定的必要条件即可
render({
data:[
{id:1,name:"a",sex:"man"},
{id:2,name:"b"},
],
}); // 但如果这样调用,会报错,因为无法通过sex:"man"的类型检查。这时候需要用其他方法
我们有三种方法:
第一种方法已经在上面做了示例,我们看后面两种方法如何做:
// 类型断言
render({
data:[
{id:"b",name:3,sex:"man"},
{id:2,name:"b"},
],
}as Result); // 明确告诉编译器,数据符合Result,这样,编译器会绕过类型检查
render(<Result>{
data:[
{id:1,name:"a",sex:"man"},
{id:2,name:"b"},
],
}); // 与上等价,但在React中容易引起歧义。不建议使用
// 索引签名
interface List {
id: number;
name: string;
[x: string]: any; // 字符串索引签名。用任意字符串去索引List,可以得到任意的结果,这样List接口可以支持多个未知属性
}
在什么场景下用什么方法,需要我们熟知这三种方法的特性
接口可以用来定义函数的传参、返回值的类型
interface Add1 {
(x: number,y: number): number;
}
let add1: Add1 = (a,b) => a+b;
此外,还可以用类型别名来定义函数
type Add2 = (x: number,y: number) => number; let add2: Add2 = (a,b) => a+b; // 声明+定义
我们再来总结一下函数的声明定义方式:
另外,接口内也可以定义函数
// 混合类型接口
interface Lib {
abc(): void;
version: string;
doSomething(): void;
}
function getLib(){
let lib: Lib = {
abc: ()=>{},
version: "1.0",
doSomething: ()=>{}
};
// let lib: Lib = {} as Lib; // 定义的时候,这种方式更方便
lib.version = "1.0";
lib.doSomething = () => {};
return lib;
}
let lib1 = getLib();
console.log(lib1,lib1.version,lib1.doSomething());
1.属性必须有类型注解
2.属性必须有初始值
3.属性修饰符:
(1)公有 public
所有成员默认都是public。可以通过各种方式访问。
(2)私有 private
私有成员只能在类中被访问,不能被实例和子类访问。如果给构造函数加上私有属性,表示这个类既不能被实例化也不能被继承。
(3)受保护 protected
受保护成员只能在类和子类中访问,不能通过它们的实例访问。如果给构造函数加上受保护属性,表示这个类不能被实例化只能被继承。也就是声明了一个基类。
(4)静态 static
静态成员只能通过类名和子类名访问,不能被实例访问。
(5)只读 readonly
只读成员不能被修改。
4.以上属性除了可以修饰成员,也可以修饰构造函数中的参数(static除外)。这样可以省去构造函数之中外的类型注解,简化代码。
class Dog{
constructor(name: string){
this.name = name; // 属性必须赋初值
}
name: string; // 必须要为属性添加类型注解。
run(){
console.log("running");
this.pri(); // 只能在类内部访问私有成员
this.pro();
}
private pri(){ // 私有成员只能被类本身调用,不能被类的实例和子类调用
console.log("pri是dog类的私有属性");
}
protected pro(){ // 受保护成员只能在类和子类中访问
console.log("pro是dog类的受保护属性");
}
readonly logs: string = "new";
static food: string = "food"; // 静态修饰后,只能通过类名调用,不能被子类和实例调用。静态成员可以被继承
}
let dog = new Dog("dog1");
dog.run();
console.log(Dog.food); // 通过类名访问静态成员
所谓抽象类(abstract),是只能被继承,不能被实例化的类。
在抽象类中,不必定义方法的具体实现,这就构成了抽象方法。在抽象类中使用abstratct关键字修饰后,不能定义该方法的具体实现。
抽象方法的好处是:实现多态。
interface AnimalParam{
bigClass: string,
environment: string,
[x: string]: string;
}
abstract class Animal{ // 抽象类用abstract关键字修饰
constructor(params: AnimalParam) { // 构造函数参数使用接口是为了其子类在定义的时候方便传参
this.bigClass = params.bigClass;
this.environment = params.environment;
}
bigClass: string;
environment: string;
abstract sleep(): void;
}
class Dogs extends Animal{
constructor(props: AnimalParam){
super(props);
this.name = props.name;
}
name: string;
run(){
console.log("running");
}
sleep() {
console.log("dog sleep")
}
}
class Cat extends Animal{
sleep(): void {
console.log("cat sleep")
}
}
let dog1 = new Dogs({bigClass:"a",environment:"b",name:"xiaoqi"});
let cat = new Cat({bigClass:"a",environment:"b"});
let animals: Animal[] = [dog1,cat];
animals.forEach(i=>{
i.sleep();
});
我们可以在方法中返回this,可以进行链式调用,非常方便。
class WorkFlow{
step1(){
return this;
}
step2(){
return this;
}
}
class Myflow extends WorkFlow{
next(){
return this;
}
}
console.log(new WorkFlow().step1().step2());
console.log(new Myflow().next().step1().step2());
interface Human {
name: string;
eat(): void;
}
class Asian implements Human{
constructor(name: string){
this.name = name;
}
name: string;
eat(){}
sleep(){}
}
interface Man extends Human{
run(): void
}
interface Child {
cry(): void
}
interface Boy extends Man,Child{} // 多继承,将多个接口合并成一个接口
可以理解为,将类转化成接口。接口集成类的时候,不仅抽离了公有成员,也抽离了私有成员、受保护成员。
如何理解呢?这么做的目的是限定接口的使用范围,并不会真正为这个接口添加类的私有和受保护属性,而这个限定范围就是:只能由子类来实现这个接口。
class Auto{
state = 1;
private state2 = 0;
protected state3 = 3;
}
interface AutoInterface extends Auto{} // 接口继承类
class C implements AutoInterface{ // C在实现这个接口的时候,无法实现接口中的私有成员和受保护成员,因此报错
state = 1
}
class Bus extends Auto implements AutoInterface{ // Auto的子类Bus,遵循AutoInterface接口
showMsg(){
// console.log(this.state2);
console.log(this.state3);
}
}
let bus = new Bus();
bus.showMsg();
console.log(bus);
泛型的概念:不预先确定的数据类型,具体的类型要在使用的时候才能确定。咋一听,是不是觉得JavaScript本就是这样?这是由于理解有误。前面说“在使用的时候确定”,而非在程序执行的时候确定。
需求:一个被定义的函数原本输入字符串输出字符串,现在想让它同时支持输入输出字符串数组,如何实现?
1.通过函数重载
function log(value: string): string;
function log(value: string[]): string[];
function log(value: any): any{
console.log(value);
return value;
}
log("abc");
log(["10","abc"]);
2.使用联合参数
function logs(value: string | string[]): string | string[]{
return value
}
以上两种都OK,但是不够简洁,不够灵活。下面使用泛型。
function log1<T>(value: T): T{
return value
}
等价于
let log1 = <T>(value: T) => {
return value
};
log1<string>("hello"); // 调用的时候指定类型
log1<string[]>(["hi","ha"]);
// 要注意<T>的位置,前者在使用时必须指定类型,后者在使用时无须指定类型
interface Log<T> {
(value: T): T;
}
let log3: Log<number> = (v) => { console.log("必须指定类型",v);return v };
log3(12);
interface Log{
<T>(value: T): T;
}
let log3: Log = (v) => { console.log("无须指定类型",v);return v};
log3<number>(10); // 无须指定类型,如果要指定类型,在调用的时候指定
log3(5);
对类的成员进行约束,注意不能约束静态成员。
class Log<T> {
run(value: T) {
console.log(value);
return value
}
}
let log1 = new Log<number>(); // 可以进行约束
log1.run(1);
let log2 = new Log(); // 也可以不进行约束
log2.run("2");
function log<T>(value: T):T{
console.log(value.length); // 如果访问.length属性,TS编译器会报错,因为不知道value有没有这个属性
return value
}
此时使用泛型约束
interface Length {
length: number;
type?: string;
}
// extends Length表示允许value参数通过.操作符访问Length中定义的属性
function log<T extends Length>(value: T): T{
console.log(value, value.length,value.type);
return value
}
// 所有具有length属性的值,都可以被当做参数传入log函数
log([1,2,3]);
log("123");
log({length: 1});
使用泛型有什么好处?
指不需要指定变量的类型,TS编译器可以根据某些规则自动推断出类型。
什么时候会有类型推断?
let a; // 这时自动推断为any类型
let b = 1; // 推断为number类型
let c = []; // 推断为由any类型构成的数组
let d = (x=1) => x+1; // 函数传参时,默认参数被推断成number类型,返回值也会被推断
let e = [1,null]; // 推断出兼容所有数据的类型:number和null的联合类型
有时候TS类型推断不符合我们的预期,我们应该比编译器更有信心它应该是什么类型,类型断言就允许我们覆盖TS的推论。
interface Foo{
bar: number
}
let foo: Foo = {} as Foo; // 如果一个对象按照接口的约定,需要有很多的属性和方法,难以在声明的时候定义完全。这时候可以用断言
foo.bar = 1; // 具体的定义在这里
类型断言不能乱用,要对上下文环境有充足的预判,没有任何根据的断言会带来安全隐患!
接口兼容性
interface X {
a: any;
b: any;
}
interface Y {
a: any;
b: any;
c: any;
}
let x: X = {a:1,b:2};
let y: Y = {a:1,b:2,c:3};
x = y; // x兼容y 成员少的会兼容成员多的
TypeScript能够在特定的区块中保证变量属于某种确定的类型。可以在此区块中放心地访问此类型的属性和方法。
比如,我们要判断一个对象是否含有某个方法
interface OBJ {
name: string,
age: number,
sex: boolean
}
let obj: OBJ = {
name: "typescript",
age: 10,
sex: true,
};
if(obj.sex) // obj中有sex属性,所以OK
{
console.log("has sex");
}
// if(obj.bac) {} // obj中没有bac属性,此处报错。
我们有四种提供类型保护的方式:
class Java {
helloJava(){
console.log("hello java");
}
java: any;
}
class JavaScript {
hellloJavaScript(){
console.log("hello javascript");
}
javascript: any;
}
// 类型保护函数 注意参数类型 和 返回值类型 的关系
function isJava(lang: Java|JavaScript):lang is Java {
return (lang as Java).helloJava !== undefined
}
function getLanguage(type: number,x: string|number|boolean) {
let lang = type === 1 ? new Java() : new JavaScript();
// instanceof 判断lang是否属于Java类
if(lang instanceof Java)
{
lang.helloJava(); // 在这个区块中,能够保证lang一定是java的实例,调用自己的方法
}else
{
lang.hellloJavaScript(); // 这个区块中,一定能够保证lang是JavaScript的实例
}
// in “helloJava”方法是否属于lang对象
if("helloJava" in lang)
{
lang.java; // 在这个区块中能够保证,lang是java的实例
}else
{
lang.javascript // 这个区块中能够保证lang是JavaScript的实例
}
// typeof 用于判断基本类型
if(typeof x === "string")
{
x.length // 程序进入这个区块,能够保证x是string类型。可以调用字符串原生方法
}else if(typeof x === "number")
{
x.toFixed(2); // 在这里能够调用数字的原生方法
}else
{
x = !x;
}
// 类型保护函数 在定义的时候,注意返回值的类型和参数类型的关系
if(isJava(lang))
{
lang.helloJava();
}else
{
lang.hellloJavaScript();
}
}
getLanguage(2,"str");
将多个类型合并成一个类型,去两个类型的并集。与继承的区别是,继承可以有自己的属性,而交叉没有。
interface DogInterface {
run():void
}
interface CatInterface {
jump():void
}
let pet: DogInterface & CatInterface = { // 看上去和接口多继承很像,但有一点区别。继承可以有自己的属性,交叉不行。
run(){},
jump(){},
};
声明的类型并不确定,可以是多个类型中的一个。
let a: number | string = "a"; // 类型限定 let b: "a" | "b" | "c"; // 限定取值 let c: 1 | 2 | 3 | "v"; // 限定取值
可区分的类型保护:
// 现在有两种形状,area函数用来计算每种形状的面积。
interface Square{
kind: "square";
size: number;
}
interface Rectangle{
kind: "rectangle",
width: number,
height: number,
}
type Shape = Square | Rectangle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size; // 此区块内,确保只有size属性
case "rectangle":
return s.height * s.width;
}
}
console.log(area({kind:"square",size:10})); // 100
// 现在要添加一个形状:圆形。需要定义接口Circle、为Shape添加联合类型Circle,然后为area函数内增加一个case。但是,如果我们忘了修改area函数,会发生什么?
interface Circle{
kind: "circle",
r: number,
}
type Shape = Square | Rectangle | Circle;
console.log(area({kind:"circle",r:10})); // undefined,这里并不报错,并不符合我们的预期。我们希望bug能够及时暴露出来,增加程序的稳定性。
做如下改动:
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
case "circle":
return Math.PI * s.r;
default:
return ((e: any)=>{throw new Error(`没有定义 ${s} 的面积计算方式`)})(s) // 这一步很重要,一定要在这里抛出异常
}
}
当我们使用不存在的索引时,会返回undefined,没有约束。因此我们需要有对索引的约束。
let obj = {
a: 1,
b: 2,
c: 3,
};
function getValue(obj: any,keys: string[]){
return keys.map(key => obj[key]);
}
console.log(getValue(obj,["a","b"]));
console.log(getValue(obj,["c","f"])); // 会发现,‘f‘对应的输出是undefined,没有约束,需要用到索引类型
下面使用索引类型:
function getValue<T,K extends keyof T>(obj: T, keys: K[]): T[K][] { // T[k][]表示,返回值必须是obj中的值组成的列表
return keys.map(key => obj[key]); // 此时keys中的元素只能是obj中的键
}
console.log(getValue(obj,["a","b"]));
console.log(getValue(obj,["c","f"])); // 这时就会报错,有了约束 ‘f‘ is not in "a" | "b" | "c"
我们来解释一下:
这里会用到两个操作符,查询操作符 keyof T 和 访问操作符 T[k](看下面示例)。<T, K extends keyof T> 用到了泛型约束,表示K所约束的参数的值只能是T所约束参数数据中的“键”。
// keyof T
interface Obj{
a: number;
b: string;
}
let key: keyof Obj; // 此时key表示 ‘a‘ | ‘b‘
// T[k]
let value: Obj[‘a‘] // number
标签:服务 机制 上下文环境 枚举类 his less 函数声明 默认 规范
原文地址:https://www.cnblogs.com/V587Chinese/p/11455862.html