TypeScript 学习笔记(一)

495 阅读5分钟

这是我参与更文挑战的第 16 天,活动详情查看:更文挑战

近期学习 TypeScript 中,顺便将学习笔记进行整理分享。

环境

首先搭建能够运行 TypeScript 的环境。

VS Code 运行

安装 Node.js,见这里。然后安装所需要的 npm 包typescriptts-node

npm install typescript -g 
npm install ts-node -g

打开 VS Code,安装 Code Runner 插件,如下图:

Snipaste_2021-06-16_22-33-05.png

初始化项目文件夹

npm init -y

直接点击右上角运行按钮即可运行 ts 文件,如图:

Snipaste_2021-06-16_22-39-53.png

如果遇到报错:Error: Cannot find module '@types/node/package.json',在项目中安装相关的包解决:

npm i --save @types/node

命令行运行

安装typescript,见上文。typescript可以将ts文件编译成js文件,然后直接node运行即可,如下:

tsc index.ts
node index.js

进行两步有点麻烦,可以安装ts-node然后直接运行ts文件,安装见上文。运行如下:

ts-node index.ts

Deno 运行

Deno 是由 node 之父开发的,另一个 js 运行时环境,通过 Deno 也可以直接运行 ts 文件。安装方式见deno.land。其实Deno 分发的就是一个执行文件,配置环境变量或者将 Deno 执行文件放在项目文件夹下也可以用,运行 ts 文件如下:

deno run index.ts

类型

类型例子备注
基本类型
booleanx: boolean = false
numberx: number = 10
stringx: string = '10'
undefinedx: undefined = undefined
nullx: null = null
引用类型以及其他类型
objectx: object = { age: '14', name: 'John' }
arrayx: array = [1, '2', 3.0]
let list: Array<number> = [1, 2, 3];
泛型
functionx: function = (args) => { console.log(args) }
symbolx: symbol = Symbol('id')
TypeScript 补充类型
anyx: any = null不进行类型检查
neverfunction error (msg): never => { throw new Error(msg) }永不存在的值的类型,是任何类型的子类型,也可以赋值给任何类型
voidlet unusable: void = undefined只能赋值 null 或 undefined,通常为没有返回值的函数
enumenum Color {Red = 1, Green, Blue}
let c: Color = Color.Green
let colorName: string = Color[2]
枚举,默认情况从 0 开始编号,也可以手动赋值
tuplex: [string, number] = ['name', 12]元组

类型断言

告诉编译器这里不需要进行数据检查和解构。在 JSX 中只支持 as 语法。

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
let strLength: number = (someValue as string).length;

接口

接口相当于自定义一个新类型,方便用来做类型检查。

可以设置必需属性、可选属性、只读属性。

interface LabelledValue {
    label: string;   // 必需属性
    name?: string;   // 可选属性
    readonly size: string;  // 只能在对象创建的时候修改该值
}

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

let myObj = { size: "10", label: "Size 10 Object", test: 'test' };
printLabel(myObj);

printLabel({ size: "10", label: "Size 10 Object", test: 'test' }); // error

可以看到当参数中有接口中未声明的属性时,如果直接传对象字面量,就会报错,TS 会做额外的检查。有几种方式可以避免,如下:

// 1、赋值给一个变量,上述未报错代码部分
let myObj = { size: "10", label: "Size 10 Object", test: 'test' };
printLabel(myObj);

// 2、使用类型断言
printLabel({ size: "10", label: "Size 10 Object", test: 'test' } as LabelledValue);

// 3、接口中定义索引签名
interface LabelledValue {
    //...
    [propName: string]: any;  // 表明有额外的属性
}

TypeScript 具有ReadonlyArray<T>类型,确保数组创建后再也不能被修改,但是可以用断言类型重写。

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 SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}

可引用的类型

上文我们提到过「索引签名」。可以通过在接口中定义索引签名来限制通过索引得到的数据的类型,如下所示:

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

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

只有两种索引签名:字符串和数字。如果同时使用两种类型的索引签名,那数字索引的返回值必须是字符串索引返回值类型的子类型。因为对象的key只能是字符串类型,即使用数字去索引,也会转成字符串。

其它属性也不能与索引签名冲突,如下所示:

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! 不能给只读类型赋值

混合类型

如果希望一个对象既可以当做函数也可以当做对象使用,就需要使用混合类型,如下所示:

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

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

类类型

通过接口来强制类去符合某种契约,但是只能约束实例的类型,不会约束静态类型。

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

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

不能用构造器签名去约束 constructor 属性,如果需要可以这样实现:

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);

继承接口

接口可以继承接口,实现多个接口的合成。

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;

接口继承类

接口还可以继承类,但是只继承类的成员,不包括实现。接口能够继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现。

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
    select() { }
}

参考资料

TypeScript 中文文档