TypeScript / 前端 · 10月 7, 2020 0

TypeScript学习笔记1

1. TS 基础类型


1.1 类型声明方式:

let isDone: boolean = false

1.2 所有数据类型:

布尔值let isDone: boolean = false

数值let num: number = 1

字符串let str: string = 'string'

数组 let arr:number[] = [1, 2, 3]或者let arr: Array<number> = [1, 2, 3]

元组 Tuple,表示一个已知元素数量和类型的数组,各元素的类型不必相同:

let x: [string, number];
x = ['hello', 10]; // OK
x = [10, 'hello']; // Error

枚举

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

Any,任何类型:let notSure: any = 4;

Void,没有类型:

function warnUser(): void {
    console.log("This is my warning message");
}

Null 和 Undefined: let u: undefined = undefined; let n: null = null;

Never,永不存在的值:

function error(message: string): never {
    throw new Error(message);
}

Object,非原始类型:

declare function create(o: object | null): void;

1.3 类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。TypeScript会假设你,程序员,已经进行了必须的检查。

类型断言有两种形式:

  1. 尖括号法:
    let some: any = 'string';
    let length: number = (<string>some).length
  2. as语法:
    let some: any = 'string';
    let length: number = (some as string).length

    1.4 联合类型

    类型可以是其中一种,但不能是其他类型;

    let myFavoriteNumber: string | number;//联合类型
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;

    1.5 类型推论

    如果在初次定义时没有指定数据类型,ts就会按照类型推论的规则推断出该变量的数据类型。

  3. 定义并赋值
    let myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;//会报错,该变量被推断为是string
  4. 只定义
    let myFavoriteNumber;//被推断为any类型
    myFavoriteNumber = 'seven';
    myFavoriteNumber = 7;

    1.6 类型别名

    给基础类型或者其他类型起一个新名字,用type

    
    type Name = string;

type NameResolver = () => string;

type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}

### 1.7 对象的类型——接口interface
既可以对类的一部分行为进行抽象(React.Props),又可以对普通对象的结构进行描述。
```TypeScript
interface Person {//必须满足接口指定的结构。
    name: string;
    age: number;
}

let tom: Person = {
    name: 'Tom',
    age: 25
};</code></pre>
<h4>1.7.1 可选属性</h4>
<p>可有可无的属性,用<code>?</code>指定:</p>
<pre><code class="language-TypeScript">interface Person {
    name: string;
    age?: number;
}

let tom: Person = {//可以通过
    name: 'Tom'
};</code></pre>
<h4>1.7.2 任意属性</h4>
<p>允许接口有任意的属性,<strong>一旦有任意属性,确定属性和可选属性必须是它的子属性</strong>(能在任意属性的类型中满足):</p>
<pre><code class="language-TypeScript">interface Person {
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    name: 'Tom',
    gender: 'male'
};</code></pre>
<h4>1.7.3 只读属性</h4>
<p>字段只能在创建的时候被赋值:</p>
<pre><code class="language-TypeScript">interface Person {
    readonly id: number;
    name: string;
    age?: number;
    [propName: string]: any;
}

let tom: Person = {
    id: 89757,//就算没有该条字段,下面的赋值也会报错
    name: 'Tom',
    gender: 'male'
};

tom.id = 9527;//错误,无法修改</code></pre>
<h3>1.8 函数的类型</h3>
<p>为函数添加类型检查,应当这样定义:</p>
<pre><code class="language-TypeScript">function sum(x: number, y: number): number {
    return x + y;
}
//或者
let sum = function (x: number, y: number): number {
    return x + y;
};</code></pre>
<p>第二种方式有所区别,只对等号右侧的匿名函数进行了类型定义,而等号左边的 sum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样:</p>
<pre><code class="language-TypeScript">let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
    return x + y;
};</code></pre>
<p>在 TypeScript 的类型定义中,<code>=></code> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型,与ES6语法不同。 </p>
<h4>1.8.1 函数参数的可选参数和默认值:</h4>
<pre><code class="language-TypeScript">function buildName(firstName: string = 'lucy', lastName?: string) {
    if (lastName) {
        return firstName + ' ' + lastName;
    } else {
        return firstName;
    }
}</code></pre>
<h4>1.8.2 剩余参数</h4>
<p>剩余参数实际上是一个数组,所以可以这样写:</p>
<pre><code class="language-TypeScript">function push(array: any[], ...items: any[]) {
    items.forEach(function(item) {
        array.push(item);
    });
}

let a = [];
push(a, 1, 2, 3);</code></pre>
<h4>1.8.3 this参数</h4>
<p><code>this</code> 是JS中的一个特殊变量,那我们应该如何指定它的类型呢?</p>
<pre><code class="language-TypeScript">interface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    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();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);</code></pre>
<p>可以显式地定义好 <code>this</code> 所指向对象的类型接口,再在函数里第一个参数定义好 <code>this</code> 的类型即可</p>
<h4>1.8.4 重载</h4>
<p>js没有重载,但可以模拟实现,ts可以提供更直观的方式,但函数体依然与js一致。</p>
<pre><code class="language-TypeScript">function reverse(x: number): number;

function reverse(x: string): string;

function reverse(x: number | string): number | string {
    if (typeof x === 'number') {
        return Number(x.toString().split('').reverse().join(''));
    } else if (typeof x === 'string') {
        return x.split('').reverse().join('');
    }
}</code></pre>
<p>前几次都是函数定义,最后一次是函数实现。  </p>
<p>注意,TypeScript 会优先从最前面的函数定义开始匹配,所以多个函数定义如果有包含关系,需要优先把精确的定义写在前面。</p>
<h3>1.9 声明文件</h3>
<p>当使用第三方库时,我们需要引用它的声明文件。</p>
<p>第三方库通常要输出一些东西,比如 jquery 的 <code>jquery</code> ,但是 ts 并不知道这是什么东西,所以需要为每个库定义特定的声明文件来声明导出结果:</p>
<pre><code class="language-TypeScript">// jQuery.d.ts,约定声明文件使用 d.ts 结尾

declare var jQuery: (string) => any;</code></pre>
<p>然后在使用到的文件的开头,用「三斜线指令」表示引用了声明文件:</p>
<pre><code class="language-TypeScript">/// <reference path="./jQuery.d.ts" />

jQuery('#foo');</code></pre>
<p>常用库的声明文件有人已经帮我们写好了,推荐<code>@types</code>库。</p>
<h4>1.10 内置对象</h4>
<p>js 和浏览器的内置对象,ts 已经帮我们实现了接口。  </p>
<p>ts 写 node 需要我们引用额外的库:</p>
<pre><code class="language-TypeScript">npm install @types/node --save-dev</code></pre>
<h2>2. 类和泛型</h2>
<hr />
<h3>2.1 TypeScript中的类</h3>
<h4>2.1.1 修饰符</h4>
<p>类似于 java,TS 可以使用三种访问修饰符</p>
<ol>
<li><code>public</code> 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 <code>public</code> 的。</li>
<li><code>private</code> 修饰的属性或方法是私有的,不能在声明它的类的外部访问。</li>
<li><code>protected</code> 修饰的属性或方法是受保护的,它和 <code>private</code> 类似,区别是它在子类中也是允许被访问的。
<pre><code class="language-TypeScript">
class Animal {
private name;
public constructor(name) {
    this.name = name;
}
}</code></pre></li>
</ol>
<p>let a = new Animal('Jack');
console.log(a.name); // 类外部无法读取
a.name = 'Tom';//无法设置</p>
<p>class Cat extends Animal {
constructor(name) {
super(name);
console.log(this.name);//子类中也无法访问
}
}</p>
<pre><code>但是编译之后,并没有限制 <code>private</code> 属性在外部的可访问性。  

#### 2.1.2 抽象类
<code>abstract</code> 用于定义**抽象类**和其中的**抽象方法**。  

抽象类是不允许实例化的:  
```TypeScript
abstract class Animal {
    public name;
    public constructor(name) {
        this.name = name;
    }
    public abstract sayHi();
}

let a = new Animal('Jack');//报错,不允许实例化

抽象类中的抽象方法必须被子类实现:

abstract class Animal {
    public name;
    public constructor(name) {
        this.name = name;
    }
    public abstract sayHi();
}

class Cat extends Animal {
    public eat() {
        console.log(`${this.name} is eating.`);
    }
}

let cat = new Cat('Tom');//没有实现抽象方法会报错
//正确的继承
class Cat extends Animal {
    public sayHi() {
        console.log(`Meow, My name is ${this.name}`);
    }
}

2.1.3 类的类型

与接口类似:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    sayHi(): string {
      return `My name is ${this.name}`;
    }
}

let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack

2.1.4 把类当接口

类定义会创建两种东西:类的实例类型和构造函数。类既然可以创建出实例类型,所以你能够在允许使用接口的地方使用类。

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

当定义类的静态方法时,ts 并不能对其进行类型检查,只会检查其实例部分。

class Point {
    x: number;
    static y: number;
}

let point:Point//ts会提示你需要x属性,不会出现y

2.1.5 实现 implements

实现 implements,在面向对象中是一个重要的概念。在nodejs文档中,你也会看到很多关于实现的例子,比如 http 类既实现了 events 接口,又实现了 stream 流接口。

TS中你可以这么写:

interface Alarm {
    alert();
}

interface Light {
    lightOn();
    lightOff();
}

class Car implements Alarm, Light {
    alert() {
        console.log('Car alert');
    }
    lightOn() {
        console.log('Car light on');
    }
    lightOff() {
        console.log('Car light off');
    }
}

多个接口可以继承:

interface Alarm {
    alert();
}

interface LightableAlarm extends Alarm {
    lightOn();
    lightOff();
}

接口也可以继承类:

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

2.2 泛型

2.2.1 泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
首先,我们来实现一个函数 createArray,它可以创建一个指定长度的数组,同时将每一项都填充一个默认值:

function createArray(length: number, value: any): Array<any> {
    let result = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

这段代码编译不会报错,但是一个显而易见的缺陷是,它并没有准确的定义返回值的类型。

Array<any> 允许数组的每一项都为任意类型。但是我们预期的是,数组中每一项都应该是输入的 value 的类型。

这时候,泛型就派上用场了:

function createArray<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray<string>(3, 'x'); // ['x', 'x', 'x']

我们在函数名后添加了 <T>,其中 T 用来指代任意输入的类型(也可以是其他字母,约定为T),在后面的输入 value: T 和输出 Array<T> 中即可使用了。

定义泛型的时候,可以一次定义多个类型参数:

function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方法。

2.2.2 泛型接口

使用含有泛型的接口来定义函数的形状:

interface CreateArrayFunc {
    <T>(length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

或者进一步:

interface CreateArrayFunc<T> {
    (length: number, value: T): Array<T>;
}

let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

createArray(3, 'x'); // ['x', 'x', 'x']

2.2.3 泛型类

与泛型接口类似,泛型也可以用于类的类型定义中:

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

2.2.4 泛型参数的默认类型

在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用:

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}

3. 模块和命名空间

3.1 模块

3.1.1 导出

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加 export 关键字来导出:

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

导出语句:

导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };

重新导出:

我们经常会去扩展其它模块,并且只导出那个模块的部分内容。重新导出功能并不会在当前模块导入那个模块定义一个新的局部变量:

export class ParseIntBasedZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && parseInt(s).toString() === s;
    }
}

// 导出其他模块部分,但做了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";

一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"

export * from "./StringValidator"; // exports interface StringValidator

export * from "./LettersOnlyValidator"; // exports class LettersOnlyValidator

export * from "./ZipCodeValidator";  // exports class ZipCodeValidator

3.1.2 导入

导入和导出一样:

import { ZipCodeValidator } from "./ZipCodeValidator";//导入某个内容
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";//重命名
import * as validator from "./ZipCodeValidator";//导入到一个变量
import "./my-module.js";//有副作用,不推荐

3.1.3 export =import = require()

CommonJS 和 AMD 的 exports 都可以被赋值为一个对象,为了支持 CommonJS 和 AMD 的 exports, TypeScript提供了export =语法。
export = 语法定义一个模块的导出对象。 这里的对象一词指的是类,接口,命名空间,函数或枚举。
若使用 export = 导出一个模块,则必须使用 TypeScript 的特定语法 import module = require("module") 来导入此模块。

let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");

3.2 命名空间

我们先写一个简单的例子,用来检测字符串,熟悉 TS 的语法:

interface IsTrue {//定义一个接口
    test(s: string): boolean
}
class Length implements IsTrue {//实现接口的类
    test(s: string) {
        return s.length >= 4
    }
}
class FirstLetter implements IsTrue {//实现接口的类
    test(s: string) {
        return s[0] === 'z'
    }
}

let classObj: {//定义一个普通对象,每个属性都必须接口的实现
    [s: string]: IsTrue
} = {}

classObj['testLength'] = new Length();
classObj['FirstLetter'] = new FirstLetter();

const strings: Array<string> = ['zhao', 'zha', 'haoo']//定义一个数组

strings.forEach((item) => {//遍历数组检测字符串
    for (let i in classObj) {
        console.log(classObj[i].test(item));
    }
})

如果有更多检测字符串的方法,类,代码都会集中在全局变量中,难免产生冲突,为此,可以使用命名空间 namespace 来解决:

namespace test {
    export interface IsTrue {//定义一个接口
        test(s: string): boolean
    }
    export class Length implements IsTrue {//实现接口的类
        test(s: string) {
            return s.length >= 4
        }
    }
    export class FirstLetter implements IsTrue {//实现接口的类
        test(s: string) {
            return s[0] === 'z'
        }
    }
}

let classObj: {//定义一个普通对象,每个属性都必须接口的实现
    [s: string]: test.IsTrue
} = {}

classObj['testLength'] = new test.Length();
classObj['FirstLetter'] = new test.FirstLetter();

const strings: Array<string> = ['zhao', 'zha', 'haoo']//定义一个数组

strings.forEach((item) => {//遍历数组检测字符串
    for (let i in classObj) {
        console.log(classObj[i].test(item));
    }
})

注意看,一个 namespace 就好像一个模块一样,可以 export 变量。在之前,这叫内部模块,而现在已经更名为命名空间,如今的模块叫外部模块。

3.3 命名空间和模块

两者有相似之处,命名空间是位于全局命名空间下的一个普通的带有名字的JavaScript对象。像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖。
一个常见的错误是使用 ///<reference> 引用模块文件,应该使用 import

4. 高级类型

4.1. 交叉类型 Intersection Types

操作符 & 。这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

interface first{
    one():void
}
interface second{
    two():void
}
type newInterface = first & second

4.2. 联合类型

操作符 | 。联合类型表示一个值可以是几种类型之一。如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员(编译时提醒)。

interface Bird {
    fly();
    layEggs();
}

interface Fish {
    swim();
    layEggs();
}

function getSmallPet(): Fish | Bird {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors

4.3. 类型保护和区分

当我们想确切地了解是否为 Fish 时,使用类型断言:

let pet = getSmallPet();

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}

4.4. 索引类型

索引类型查询操作符 keyof 。对于任何类型 T , keyof T 的结果为 T 上已知的公共属性名的联合接口。 例如:

interface Person {
    name: string;
    age: number;
}
let personProps: keyof Person; // 'name' | 'age'

索引访问操作符

function getProperty(o: T, name: K): T[K] {
    return o[name]; // o[name] is of type T[K]
}

映射类型
TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性:

type Readonly = {
    readonly [P in keyof T]: T[P];
}
type Partial = {
    [P in keyof T]?: T[P];
}

像这样使用:

interface Person {
    name: string;
    age: number;
}
type PersonPartial = Partial;
type ReadonlyPerson = Readonly;

预定义的有条件类型:

  1. Exclude<T, U> — 从 T 中剔除可以赋值给 U 的类型。
  2. Extract<T, U> — 提取 T 中可以赋值给U的类型。
  3. NonNullable<T> — 从 T 中剔除 null 和 undefined 。
  4. ReturnType<T> — 获取函数返回值类型。
  5. InstanceType<T> — 获取构造函数类型的实例类型。
冀ICP备19028007号