TypeScript-初学总结

米阳 2020-6-20 233 6/20

typescript(以下简称TS)出来也有好长时间了,最近一个月的时间抽出一点时间把ts简单学习了一下,下面记录一下学习心得。

首先学这门语言前,请确保有以下基础知识:

  • javascript
  • es6
  • 面向对象编程的概念(没有也可以,就当是重新学一遍了)

接下来看一下TS的一些概念:

基本类型

TS的基础类型有:字符串(string)、数字(number)、布尔值(boolean)、空(null)、未定义(undefined)、数组(array)、对象(object)、元组(tuple)、枚举(enum)、any、void、never等12种。

写法为在变量后加冒号然后跟变量类型的方式,例如:

let str: string = 'str'; //字符串
let num: number = 123; //数字
let bol: boolean = false; //布尔值
let n: null = null; //null
let u: undefined = undefined; //undefined
let arr: number[] = [1,23,4,];  //数组
let arr1: Array<number> = [1,2,3];  // 使用泛型的方式声明变量
let obj: object={};  //对象
let tuple: [number,string] = [12,'3'];  //元组
enum Num{   //枚举
  one=1,// 从几开始,默认为从0开始
  two,// 2
  three// 3
};

//any 写法
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
let anyArr: any = [1,2,'4',false,null];

//viod 写法
function warnUser(): void {
    console.log("This is my warning message");
}
let unusable: void = undefined;
let unuse: void;

//never 写法
function error(message: string): never {
    throw new Error(message);
}
// 推断的返回值类型为never
function fail() {
    return error("Something failed");
}
// 返回never的函数必须存在无法达到的终点
function infiniteLoop(): never {
    while (true) {
    }
}

接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名定义契约。

//写法 interface 接口名 { attribute: type }
interface LabelledValue {
  label: string;
}
function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}
//可选属性
interface SquareConfig {
    color?: string;
    width?: number;
}
//只读属性
interface SquareConfig {
   readonly color: string;
}
//只读数组
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
// a = ro as number[]; 用断言修改数组为可修改!

//跳过额外的属性检查
interface SquareConfig {
    color?: string;
    width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
    return {
        color: 'blue',
        area:23
    }
    // ...
}
(方法1)
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
(方法2)索引签名
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}
(方法3)将这个对象赋值给一个另一个变量: 因为squareOptions不会经过额外属性检查
let squareOptions = { color: "red", width: 100 };
let mySquare = createSquare(squareOptions);

//通过接口定义函数类型
interface SearchFunc {
    (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}
// or
// mySearch = function(src: string, sub: string): boolean {
//     let result = src.search(sub);
//     return result > -1;
// }

可索引的类型

TypeScript支持两种索引签名:字符串和数字。

可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

interface StringArray {
    [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
// 定义的StringArray接口,它具有索引签名,表示当用number去索引StringArray时会得到string类型的返回值。
interface NumberDictionary {
    [index: string]: number;
    length: number;    // 可以,length是number类型
    name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}
// 将索引签名设置为只读
interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

实现接口

TypeScript也能够用它来明确的强制一个类去符合某种契约

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

类静态部分与实例部分的区别

当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:

interface ClockConstructor {
  new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
  currentTime: Date;
  constructor(h: number, m: number) { }
}
// 这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
// 因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor为构造函数所用和ClockInterface为实例方法所用。 为了方便我们定义一个构造函数 createClock,它用传入的类型创建实例。
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick();
}
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("beep beep");
  }
}
class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) { }
  tick() {
    console.log("tick tock");
  }
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
// 因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

继承接口

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
  color: string;
}
interface Square extends Shape {
  sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
//继承多个接口
interface Shape {
  color: string;
}
interface PenStroke {
  penWidth: number;
}
interface Square extends Shape, PenStroke {
  sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}
function getCounter(): Counter {
  let counter = <Counter>function (start: number) { console.log(start) };
  counter.interval = 123;
  counter.reset = function () { };
  return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

ES 6开始,JavaScript就有了基于类的面向对象的方式。

声明类

class CreateClass {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter = new CreateClass('demo');

继承类

类继承:类从基类中继承了属性和方法。这里,Dog是一个派生类,它派生自ParentClass基类,通过extends关键字。派生类通常被称作子类,基类通常被称作超类。
class ParentClass {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}
class Dog extends ParentClass {
    bark() {
        console.log('Woof! Woof!');
    }
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

类私有属性

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
    constructor() { super("Rhino"); }
}
class Employee1 {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee1("Bob");
console.log(animal.name); // 错误

类受保护属性

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 `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

构造函数也可以被标记成 protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如:

class Person2 {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}
// Employee 能够继承 Person
class Employee2 extends Person {
    private department: string;
    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard2 = new Employee2("Howard", "Sales");
let john = new Person2("John"); // 错误: 'Person' 的构造函数是被保护的.

静态属性:类的静态成员,这些属性存在于类本身上面而不是类的实例上。

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

let grid1 = new Grid(1.0);  // 1x scale
let grid2 = new Grid(5.0);  // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

抽象类:抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化

abstract class AbstractClass {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象类中的抽象方法不包含具体实现且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract关键字并且可以包含访问修饰符。

abstract class Department {
    constructor(public name: string) {
    }
    printName(): void {
        console.log('Department name: ' + this.name);
    }
    abstract printMeeting(): void; // 必须在派生类中实现
}
class AccountingDepartment extends Department {
    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }
    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }
    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}
let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

类当做接口使用

class Point {
    x: number;
    y: number;
}
interface Point3d extends Point {
    z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};

泛型

泛型类型:与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: <T>(arg: T) => T = identity; // or
let myIdentity1: {<T>(arg: T): T} = identity;
// 这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口
interface GenericIdentityFn {
  <T>(arg: T): T;
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;function identity1<T>(arg: T): T {
  return arg;
}

泛型类

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
// GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

枚举

//数字枚举
enum Direction {
    Up = 1, // 使用初始值,递增,否则默认从0开始
    Down,
    Left,
    Right
}
//字符串枚举
enum Direction2 {
    Up = "UP", // 每个字符串枚举成员必须进行初始化
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}
//异构枚举(混合类型的)
enum NumAndStrEnum {
    No = 0,
    Yes = "YES",
}
//联合枚举与枚举成员的类型
enum ShapeKind {
    Circle,
    Square,
}
interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}
interface Square {
    kind: ShapeKind.Square;
    sideLength: number;
}
let c11: Circle = {
    kind: ShapeKind.Square, // 正确的为ShapeKind.Circle
    //    ~~~~~~~~~~~~~~~~ Error!
    radius: 100,
}

//运行时枚举
enum E {
    X, Y, Z
}
function f(obj: { X: number }) {
    console.log('X',obj.X);
    return obj.X;
}
f(E);

//反向映射
enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
// 生成的代码中,枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。 引用枚举成员总会生成为对属性访问并且永远也不会内联代码。
// 要注意的是 不会为字符串枚举成员生成反向映射,因为枚举成员不能具有数值名,所以数字枚举成员具有反射

常量(const)枚举

为了避免在额外生成的代码上的开销和额外的非直接的对枚举成员的访问,我们可以使用 const枚举。 常量枚举通过在枚举上使用 const修饰符来定义。

常量枚举注意点:

1.不会生成反向映射

2.不能直接访问值

const enum Order {
    A,
    B,
    C,
}

外部枚举

外部枚举用来描述已经存在的枚举类型的形状,简单理解就是方便用户编写函数时的提示

declare enum Enum {
    A = 1,
    B,
    C = 2
}

外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。

用来描述一个应该存在的枚举类型的,而不是已经存在的,它的值在编译时不存在,只有等到运行时才知道。

 

- THE END -
Tag:

米阳

3月19日17:04

最后修改:2025年3月19日
0

非特殊说明,本博所有文章均为博主原创。