TS

153 阅读15分钟

ts

核心原则有哪些?

1、对值的所具有的结构进行类型检查(也被称为"鸭式辨型法"或"结构性子类型化")

基础类型

  • 布尔值 boolean

let flag:boolean=false;

  • 数字 number【都是浮点型,还支持二,八,十,十六进制字面量】

let a:number=1;

  • 字符串 string【可用单/双引号/字符串模板 来表示字符串】

let name:string='Lili';

  • 数组 【可以用两种方式】
  1. 元素类型[]

let list:number[]=[1,2,3]; 2) Array<元素类型> 【数组泛型的方式】

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

  • 元组【允许一个已知元素数量和类型的数组,各元素的类型不必相同】
//声明一对值分别为string和number类型的元组

let x:[string,number];
//初始化
x=['hello',10] // ok
x=[10,'hello'] // error


// 访问已知的索引元素,会得到正确的类型
x[0].substr(1); // ok
x[1].substr(1); // error,'number' does not have 'substr'

// 当访问一个越界的元素,会使用**联合类型**替代:【若是元素越界,只要是事先声明过的类型,都是对的】
x[3]='world'; //ok,z字符串可以赋值给(string | number)类型
x[5].toString();//ok,'string'和'number'都有toString

x[6]=true; // error ,布尔不是(string | number)类型

  • 枚举 【使用枚举类型可以为一组数值赋予友好的名字】
  1. 默认情况下,从0开始为元素编号
  2. 也可指定成员的数值
  3. 可以枚举出来的值得到它的名字【看下面第三点,Color[9] 会输出 Blue 】

// 1)不赋值的情况 默认输出结果为 0,1,2
enum Color{
    Red,
    Green,
    Blue
}
let c:Color=Color.Green;//输出结果 c=1


// 2)赋值时,未赋值的成员,会在前一个成员的数值上+1,即Color.Green=5
enum Color{
    Red=4,
    Green,
    Blue=9
}
let c:Color=Color.Green;//输出结果 c=5

// 3)第一个Red未赋值,默认还是为0
enum Color{
    Red,
    Green=3,
    Blue=9
}
let r:Color=Color.Red;//输出结果 r=0

// 4)通过枚举的值得到对应的名字
enum Color{
    Red=1,
    Green,
    Blue
}
let colorName:string =Color[2]

console.log(colorName); //输出结果为 'Green'
  • any
  1. 可用来标记不清楚类型的变量,或者只清楚一部分,或者说不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查

  2. 和Object有相似的作用,但是Object类型的变量只允许赋任意值,但是不能够调用它上面存在的方法

let notSure:any=3;
notSure.ifItExists();//ok
notSure.toFixed();//ok

let prettySure:Object=4;
prettySure.toFixed();//error,类型“Object”上不存在属性“toFixed”。

// 只知道部分数据类型,也可用any声明【e.g. 声明一个包含不同类型数据的数组】
let list:any[]=[1,true,"free"];
list[1]=100

  • void【与any类型相反,表示没有任何类型。通常用在没有返回值的函数上,声明变量没什么用,只能赋值为undefined和null】
//1)声明一个没有返回值的函数
function fc():void{
    console.log('这是一个没有返回值的函数')
}


//2)声明void类型的变量没什么用,只能赋予undefined和null
let unusable:void=undefined;
  • null和undefined
  1. 和void相似,作用不大
  2. 默认情况,null和undefined是所有类型的子集,e.g. 可以把null和undefined赋值给number类型的变量
  3. 然而指定了 --strictNullchecks标记,null和undefined只能赋值给void和他们各自。或者在某处想传入一个string或null或undefined ,可以使用联合类型 string | null | undefined 。
  • never 【永不存在的值的类型】
  1. never 类型是那些总会抛出异常,或者根本就不会有返回值的 函数/箭头函数表达式 的返回值类型
  2. 变量也可能是never类型。(当他们被永不为真的类型所约束时)
  3. never是任何类型的子集,可以赋值给任何类型。没有类型是never的子集或赋值给never类型(除了never本身)
// 返回never的函数必须存在无法到达的终点
function error(msg:string):never{
    throw new Error(msg);
}
function infiniteLoop():never{
    while(true){
    }
}

//

  • Object
  1. object 表示非原始类型(即number,string,boolean,symbol,null,undefined之外的类型)
  2. 使用object类型,可以更好的表示像Object.create 这样的api
declare function create(o: object | null):void;
create({prop:0}); // ok
create(null); //ok

create('string'); //error
create(false);//error
create(undefined);//error

  • 类型断言【类似其它语言里的类型转换,但是不进行特殊的数据检查和解构。不会影响运行,只在编译阶段起作用】

有两种语法【两种语法等效,但是在ts里使用jsx时,只有 as语法 断言可用】

  1. 尖括号语法: <元素类型>变量名
  2. as语法: 变量名 as 元素类型
let someValue : any = 'this is a string';

// 1) 尖括号语法
let strLength:number=(<string>someValue).length;

// 2) as语法
let strLength:number= (someValue as string).length;

变量声明 【var ,let const】

  • var 【函数作用域】
  1. 声明变量,或在函数内声明变量,或其他函数内部访问相同的变量【在for 循环中层层套用,用var声明相同名字的变量不会报错,这样子在代码审查的时候漏掉会引发麻烦】
  2. var 声明可以在包含它的函数,模块,命名空间或全局作用域内部的任意位置被访问,包含它的代码块对此没有什么影响。【由此称之为:var作用域/函数作用域】

捕获变量怪异之处

for (var i = 0; i < 10; i++) { 
    setTimeout(
        function() { 
        console.log(i); 
    }, 100 * i); 
}
// 会输出10个10,【我们理想的输出结果是0-9】

// 原因分析:【我们传给setTimeout的每一个表达式实际上都引用了相同作用域里面的同一个i】

//setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。for循环结束后,i的值为10.所以当函数呗调用时,就会打印出10

// 解决方式:
// 1)可以用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:

for (var i=0;i<10;i++){
    (function(){
        setTimeout(function(){
            console.log(i)
        },100*i);
    })(i);
}
// 结果输出 0-9

// 2) 用 let 声明变量i
function (let i=0;i<10;i++){
    setTimeout(function(){
        console.log(i);
    },100*i);
}
// 输出结果 0-9
  • let 【块作用域】
  1. 当用let 声明一个变量,它使用的是词法作用域/块作用域
  2. 不能在被声明之前读或者写(虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区
  3. 重定义,在一个作用域里面只能定义一个名字的变量,或者块级作用域变量需要在明显不同的块里声明
  4. 屏蔽,在一个嵌套作用域里面引入一个新名字的行为
  • const 【块作用域,不能被重新赋值,即它引用的值是不可变的,但是他的内部状态可修改】
const num=9;
const obj={
    objName:"lili",
    objNum:num
}


// error
obj={
    objName='张三',
    objNum:num
}

// ok
obj.objName='李四';
obj.objNum--;

解构

  • 解构数组
// 1、作用于常量
let arr=[1,2];
let [a,b]=arr;

console.log(a) // outputs 1
console.log(b) // outputs 2


// 2、作用于已经声明的变量
[a,b]=[b,a]

// 3、作用于函数参数
function f([a,b]:[number,number]){
    console.log(a);
    console.log(b);
}

f(arr);

【 ... 在数组中使用】

// 1、...语法创建剩余变量
let [a,...rest]=[1,2,3,4]
console.log(a); // 1
console.log(rest);// [2,3,4]

// 2、 忽略尾随元素 
let [a]=[1,2,3,4]
console.log(a); // 1

// 3、其他元素
let [,b,,d]=[1,2,3,4]

  • 对象解构

let o={
    a:'foo',
    b:12,
    c:'car'
};

let {a,b}=o;  // 【要用括号扩起来,因为js通常会以{起始的语句解析为一个块】

【 ... 在对象中使用】


// 1、...语法创建剩余变量
let {a ,...passthrought}=o;
let total=passthrought.b+passthrought.c.length;

  • 属性重命名
let {a:newName1,b:newName2}=o;//这里的newName1和newName2是重命名后的名字,并不是指示类型
  • 指示类型需要这样写
let {a,b}:{a:string ,b: number}=o;
  • 默认值,可以在属性为undefined时使用缺省值
function keepWholeObject(wholeOnject:{a:string,b?:number}){
    let {a,b=1001}=wholeOnject;
}
  • 函数声明
type C={a:string,b?:number }

function f({a,b}:C):void{

}



function f({a,b=0}={a:""}):void{

}

f({a:"yes"});// ok,默认 b=0

f(); // ok,默认{a:""},b=0

f({}); //error a需要被提供参数
  • 展开【... 展开操作符】,将一个数组展开为另一个数组,将一个对象展开为另一个对象

// 1、展开数组
let first=[1,2];
let second=[3,4];

let bothPlus=[0,...first,...second,5]; // 输出 [0,1,2,3,4,5]   

// 【展开操作创建了first和second的一份**浅拷贝**,不会被展开操作所改变】


// 2、展开对象
//【后面的对象会覆盖前面的属性】
let default={ a:'a',b:'b',c:'c',}
let search={ ...default,a:'d'}  // 这里 search 展开的值 为{ a:'d',b:'b',c:'c',}
let search={a:'d', ...default} // 这里 search 展开的值 为{ a:'a',b:'b',c:'c',} 
//【仅包含对象自身的可枚举的属性,即当展开一个对象实例时,会丢失其方法】

class C{
    p=12;
    m(){
    
    }
}

let c=new C();
let clone ={...C};

clone.p;//ok
clone.m();//error

接口

作用:为类型命名和自己写的代码或者第三方代码定义契约。

  • 对象类型
// 声明了一个名为LabelledValue的接口,【代表了有一个label属性且类型为string的对象】
interface LabelledValue{
    label:string;
}

function printLabel(labelledObj:LabelledValue){
    console.log(labelledObj.label);
}

let myObj={
    size:10,
    label:'size 10 obj'
};
// myObj对象满足接口LabelledValue中提到的【必要条件】(即上面【】里面的条件),就是被允许的

【btw,类型检查器不会检查属性的顺序,只要相应的属性存在并且类型是对的就可以】

printLabel(myObj);


  • 可选属性【接口里面的属性不全是必须的,只在某些条件下存在或者把根本不存在】

定义时,在可选属性名字定义的后面加一个?符号。

优势:

  1. 可以对可能存在的属性进行预定义
  2. 捕获引用了不存在的属性时的错误
interface SquareConfig{
    color?:string;
    width?:number;
}

function createSquare(config:SquareConfig):{color:string;area:number}{
    let newSquare={color:'red',area:100};
    
  //  if(config.color){
    //    newSquare.color=config.color;
   // }
    
  if(config.colr){
  // 这里会报错,SquareConfig不存在colr属性
        newSquare.color=config.colr;
   }
      
    if(config.width){
        newSquare.area=config.width*config.width;
    }
    
    return newSquare;
}
let mySquare=createSquare({color:'blue'}); // ok

let mySquare=createSquare({colour:'green',width:100}); // error

// 对象字面量会被特殊对待而且会经过【额外属性检查】,当将它们赋值给变量或作为参数传递的时候。如果一个对象字面量存在任何‘目标属性’不包含的属性时,就会报错。

// 1. 想要绕过【额外属性检查】可以使用【类型断言】来避免报错

let mySquare=createSquare({width:100,opacity:0.5} as SquareConfig); 

// 2. 最佳的方式是,可以添加以一个字符串索引签名来避免【使用条件,在自己知道这个对象可能具有某些做为特殊用途使用的额外属性】

interface Squareconfig{
    color?:string;
    width?:number;
    [propName:string]:any;
}

  • 只读属性

一些对象属性只能在对象刚刚创建的时候修改其值

1.用readonly来指定只读属性

interface Point{
    readonly x:number;
    readonly y:number;
}

// 可以赋值一个对象字面量来构造一个Point。赋值后,x和y再也不能被改变了。
let p1:Point={x:1,y=10};
p1.x=3;// error 不能被赋值

2.用 ReadonlyArray< T > 创建数组也不能被修改。但是可以用类型断言重写

let a: number[] =[1,2,3,4];
let ro: ReadonlyArray<number> =a;

ro[0]=11; // error
ro.push(6);// error
ro.length=100;// error
a=ro; //error

a=ro as number[] //ok 用断言重写

  1. readonly vs const

什么时候使用他们俩?

做为变量使用的const,做为属性使用readonly

  • 函数类型

// 创建一个函数类型的的接口:
// 定义一个调用签名:【参数必须有名字和类型】
// (参数: 参数类型):返回值类型;
interface SearchFunc{
    (source:string,subString:string):boolean;
}



let mySearch:SearchFunc;

//  这里的参数名字不一定要于接口里的参数名一致,也可以不用声明类型 function(sour,sub) ,这样写也可以,检查器会按照位置来检查与接口声明的类型是否匹配
mySearch=function(source:string,subString:string){
    let res=source.search(subString);
    return res>-1; 
}



  • 可索引的类型
// 创建一个索引类型的的接口:
// 定义一个索引签名:(描述了对象索引的类型,还有相应的索引返回值类型)

interface StringArray{
    [index:number]:string;
}

let maArray:StringArray;

maArray=['a','b'];

let myStr:string=maArray[0];


【ts支持两种索引签名: 字符串和数字】

1、两种类型的索引可同时使用 2、数字索引的返回值必须是字符串索引返回值类型的子类型(因为在使用number来索引时,js会把它转换成string然后再去搜索对象,e.g. 用100去索引等同于使用'100'去索引,所以两者要保持一致)。

class Animal{
    name:string;
}

class Dog extends Animal{
    breed:string;
}

interface NotOk{
// error 使用数值型字符串索引,有时候会得到完全不同的Animal
    [x:number]:Animal;
    [x:string]:Dog;
}




interface NumberDictionary{
    [index:string]:number;
    
    length:number; // ok
     
    name:string; // error,  name 的类型和索引类型返回值的类型不匹配
}


// 将索引签名设置只读,防止给索引赋值

interface ReadonlyStringArray{
    readonly [index:number]:string;
}

let myArray:ReadonlyStringArray=['a','b'];

myArray[2]='c';// error

  • 类类型

1.实现接口

interface ClockInterface{
    currentTime: Date;
    setTime(d:Date);
}

class Clock implements ClockInterface{
    currentTime:Date;
    setTime(d:Date){
        this.currentTime=d;
    }
    constructor(h:number,m:number){}
}


2.类静态部分和实例部分的区别

接口只会对实例部分进行检查

// 定义了一个名为ClockConstructor的接口,为构造函数所用
interface ClockConstructor{
    new (hour:number,minute:number):ClockInterface;
    
}

// 定义了一个名为ClockInterface的接口,为实例方法所用
interface ClockInterface{
    tick();
}
// 第一个参数是ClockConstructor类型,传递参数时,会检查参数是否符合构造函数签名
function createClock(ctor:ClockConstructor,hour:number,minute:number):ClockInterface{
    return new ctor(hour,minute);
}

class DigitalClock implements ClockInterface{
    constructor(h:number,m:number){}
    tick(){
        console.log('dig dig');
        
    }
}

let dig=createClock(DigitalClock,12,6);
  • 继承接口
interface Shape{
    color:string;
}
interface PenStroke{
    penWidth:string;
}
// 继承一个接口
//interface Square extends Shape{
//    sideLength:number;
//}
// 继承多个接口(合成接口)
interface Square1 extends Shape,PenStroke{
     sideLength:number;
}

let suqare =<Square>{};

suqare.color='red';
suqare.sideLength=2;
Square.penWidth=5;

  • 混合类型

什么时候使用?

需要一个对象上面同时具有多种类型的时候使用。

// e.g. 一个对象可以同时做为函数和对象使用,并且带有额外属性
interface Counter{
    (start:number):string;
    interval:number;
    reset():void;
}

function getCounter():Counter{
    let counter=<Counter>function(start:number){};
    counter.interval=10;
    counter.reset=function(){};
    return counter;
}

let c=getCounter();

c(10);

c.reset();

c.interval=5;


  • 接口继承类

1.只会继承类的成员,但不包括其实现 2.同样会继承到类的private和protected成员



class Control{
    // 因为是私有成员state ,所以只能是 Control的子类才能实现 SelectableControl接口(只有Control的子类才能拥有一个声明与Control的私有成员state,这对私有成员的兼容性是必须的)
    private state:any;
}

// SelectableControl 包含了 Control的所有成员,包括私有成员state 
interface SelectableControl extends Control{
    select():void;
}

// SelectableControl的子类(继承了Control并有select 方法)
class Button extends Control implements SelectableControl{
    select(){}
}

// SelectableControl的子类(继承了Control并有select 方法)
class TextBox extends Control{
     select(){}
}
// => 实际上,SelectableControl 接口  和 拥有select方法的Control类 是一样的
// error ,Image 类型缺少 state属性
class Image implements SelectableControl{
     select(){}
}
class Location{

}


// 声明一个类(Greeter类有三个成员)
class Greeter{

     // 一个属性
    greeting:string;
    // 一个构造函数
    constructor(msg:string){
        // 用this关键词访问成员
        this.greeting=msg;
    }
    // 一个方法
    greet(){
        return 'hello,'+this.greeting;
    }
}
// 使用new关键词构造了一个Greeter类的实例(它会调用之前定义的构造函数,创建一个Greeter类型的新对象,并执行构造函数初始化它)
let greeter=new Greeter('world');
  • 继承 extends

class Animal{
    name:string;
    constructor(theName:string){
        this.name=theName;
    }
    move(meters:number=0){
        console.log(`${this.name} move ${meters}m.`);
    }
}

// Dog 是一个派生类,派生于Animal基类 (派生类通常被称作子类,基类被称作超类)
class Dog extends Animal{
    bark(){
        console.log('wo wo');
    }
}

class Snake extends Animal{
    constructor(name:string){
       // 在派生类里面必须要用super() 来执行基类的构造函数,在构造函数里访问this之前,我们一定要调用super()
        super(name);
    }
    
    
    move(meters:number=5){
    // 子类重写了父类的方法
        console.log('Snake');
        
        super.move(meters)
    }
}

class Horse extends Animal{
    constructor(name:string){
        super(name);
    }
    move(meters:number=45){
        console.log('Horse');
        
        super.move(meters)
    }
}

// Dog 继承了Animal的功能,所以可以调用Animal的方法
const dog=new Dog();

dog.move(100);
dog.bark();

let sam=new Snake('sam');
let tom : Animal=new Horse('tom');

sam.move();
tom.move(555);

// 输出
// Snake
// sam moved 5m.
// Horse
// tom moved 34m.

  • 公共,私有与受保护的修饰符
  1. 默认为public 公共的
class Animal{
   public name:string;
   public constructor(theName:string){
        this.name=theName;
    }
   public move(meters:number=0){
        console.log(`${this.name} move ${meters}m.`);
    }
}

2.private 私有的 (当成员标记为private时,不能在声明它的类的外部访问)

一个类型里包含一个private(或protected)成员,只有另一个类型中也存在这样一个private常规能源,且来自同一声明,才认为是兼容的。

class Animal{
    private name:string;
    constructor(theName:string){
        this.name=theName;
    }
}

class R extends Animal{
    constructor(){
        super('Rname');
    }
}
class E {
    private name:string;
    constructor(theName:string){
        this.name=theName;
    }
}
new Animal('Cat').name; // error , name 是私有的


let animal=new Animal('Goat');
let r=new R();
let e=new E('Eee');

animal=r; //ok, Animal 和R共享了来自Animal里面的私有成员name,所以是兼容的
animal=e; //error ,Animal和E 类型不兼容,E里面的私有成员name,不是Animal里面的同一个
  1. protected 受保护的
// protected和private行为相似,但 protected成员在派生类中仍然可以访问

class Person{
    protected name:string;
    constructor(name:string){
        this.name=name;
    }
}

class Employee extends Person{
    private department:string;
    constructor(name:string,department:string){
        super(name);
        this.department=department; 
    }
    public getElevatorPitch(){
        return `name is ${this.name}, I work in ${this.department}`;
    }
    
    
    
}