TypeScript 入门修炼至出关

340 阅读10分钟

learn ts

自己之前学 ts 的一些笔记,分享出来,希望也能帮助到大家!

相关代码辅助学习地址:github

1、搭建环境

1、node安装

node官网自行下载对应版本

检查版本:

node -v

2、typescript安装

npm install -g typescript

检查版本:

tsc -v

2、编译文件

tsc 01-hello.ts

执行上处命令,就会得到一个 01-hello.js 文件。

3、编译选项

  • --outDir 指定编译文件输出目录
tsc --outDir ./dist ./src/01-hello.ts
  • --target 指定编译的代码版本,默认为 ES3
tsc --outDir ./dist --target ES6 ./src/01-hello.ts
  • --watch 在监听模式下运行,当文件发生改变的时候自动编译
tsc --outDir ./dist --target ES6 --watch ./src/01-hello.ts

3.1、编译配置文件 tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es6",
    "watch": true,
    "allowJs": true
  },
  "include": ["./src/**/*"]
}

Implict: 隐式

explict: 显式

配置项:noImplicitAny,当你不为变量声明类型时,如果noImplicitAny=false,那么它是any。如果noImplicitAny=true,则报错。

tsc
  • -p 指定配置文件
tsc -p ./ts.json
tsc -p ./src

官方文档 tsconfig.json

编译选项 compilerOptions

4、基础类型

number类型

let binaryLiteral: number = 0b1010; // 二进制
let octalLiteral: number = 0o744;    // 八进制
let decLiteral: number = 6;    // 十进制
let hexLiteral: number = 0xf00d;    // 十六进制

string类型

let name: string = "typescript";
let words: string = `您好,我是 ${ name }`;

boolean类型

let flag: boolean = true;
let flag2: boolean = false;

null类型

表示对象值缺失。

let n: null = null;

undefined类型

用于初始化变量为一个未定义的值。

let u: undefined = undefined;

Typescript有一个配置项,叫做strictNullChecks,这个配置项设置为on 的时候,在使用有可能是null的值前,你需要显式的检查。

function doSomething(x: string | null) {
  if (x === null) {
    // do nothing
  } else {
    console.log("Hello, " + x.toUpperCase());
  }
}

另外, 可以用! 操作符,来断言某个值不是空值:

function doSomething(x: string | null) {
  console.log("Hello, " + x!.toUpperCase());
}

5、其他类型

any类型

声明为 any 的变量可以赋予任意类型的值,为ts的默认类型。

数组类型

let list: number[] = [1, 2, 3]; // 在元素类型后面加上[]
let list: Array<number> = [1, 2, 3]; // 或者使用数组泛型

元组(Tuple)类型

元组类型用来表示已知元素数量和类型的数组,各元素的类型不必相同,对应位置的类型需要相同。

let x: [string, number];
x = ['typescript', 1]; // 运行正常
x = [1, 'typescript']; // 报错
console.log(x[0]); // 输出 typescript

enum(枚举)类型

枚举类型用于定义数值集合。

enum Color {
    Red, 
    Green, 
    Blue
};
let c: Color = Color.Blue;
console.log(c);  // 输出 2

// 处理状态码
enum HTTP_CODE {
  OK = 200,
  NOT_FOUND = 404
}
if(res.code === HTTP_CODE.OK) {}

// 整合接口连接
enum URLS {
  USER_REGISETER = '/user/regiseter',
  USER_LOGIN = '/user/login'
}
enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

上面的含义, Down = 2, Left = 3, Right = 4

枚举类型最后会被翻译成整数,因此枚举的很多性质和整数相似。比如Down.toString()会返回2,而不是Down 。正因为如此,枚举类型的效率很高。

在运行时,Enum会被解释成对象,Enum的每项会被解释成常数。

enum E {
  X,
  Y,
  Z,
}

function f(obj: { X: number }) {
  return obj.X;
}

f(E)

可以用下面这个语法提取Enum中的字符串,这个也叫Reverse Mapping。

E[E.X] // X

void类型

用于标识方法返回值的类型,表示该方法没有返回值。

function hello(): void {
    alert("Hello typescript");
}

never类型

never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。

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

object类型

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

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

unknow类型

3.0版本中新增,属于安全版的any,但是与any有所不同。

  • unknow仅能赋值给unkonow,any
  • unknow没有任何属性和方法

函数类型

function add(x: number, y: number): number {
    return x + y;
}
console.log(add(1,2))

6、接口

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

接口是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的类去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法。

接口是一种类型,不能作为值去使用。

interface Person {
  name: string;
  readonly ID: number;
  age?: number;
  [propName: string]: any;
}

可选属性

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值,后续只能读取,不能修改。

额外的属性

索引签名参数类型必须为 string 或 number 之一,但是两者可同时出现

当同时存在数字类型索引和字符串类型索引的时候,数字类型的值必须是字符串类型的值类型或子类型

这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致

7、类型深入

联合类型

function css(ele: Element, attr: string, value: string | number) {
  //
}

let box = document.querySelector(".box");
if (box) {
  css(box, "width", "100px");
  css(box, "opacity", 1);
}

交叉类型

interface o1 {
  x: number;
  y: string;
}
interface o2 {
  z: boolean;
}

let o3: o1 & o2 = Object.assign({}, { x: 1, y: "2" }, { z: true });

字面量类型

function setPosition(
  ele: Element,
  direction: "left" | "top" | "right" | "bottom"
) {
  //
}

let box = document.querySelector(".box");
box && setPosition(box, "bottom");
// box && setPosition(box, 'leftTop') // 错误
const someStr = "abc"
// someStr的类型是 "abc",它的值只能是abc

const foo = 1
// foo 的类型是1(而不是整数)。

// 当然这只是ts的理解,如果用typeof 操作符
// typeof someStr // 'string'
// typeof foo // 1

// 对于let
let foo = 1 // foo : number

可以用字面类型来约束一些特殊的函数,比如:

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1;
}

当然下面是一个更加贴近真实场景的例子:

interface Options {
  width: number;
}
function configure(x: Options | "auto") {
  // ...
}
configure({ width: 100 });
configure("auto");
configure("automatic"); // Argument of type '"automatic"' is not assignable to parameter of type 'Options | "auto"'.

字面类型的一个坑:

function handleRequest(url: string, method: "GET" | "POST") {
    // do...
}

const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Error : Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.

// 3种处理办法
// 1
const req = { url: "https://example.com", method: "GET" as "GET" };

// 2 
handleRequest(req.url, req.method as "GET");

// 3 
const req = { url: "https://example.com", method: "GET" } as const

类型别名

type dir = "left" | "top" | "right" | "bottom";
function setPosition(ele: Element, direction: dir) {
  //
}

let box = document.querySelector(".box");
box && setPosition(box, "bottom");

类型推导

类型推导发生在:

  • 初始化变量
  • 设置函数默认参数值
  • 返回函数值
let x = 1; // 自动推导 x 为 number
x = 'a'; // 报错

function a(x = 1, y = 2) {
  return x + y;
}

function b(x: number, y: number) {
  return x + y;
}

类型断言(assertion)

let img = document.querySelector(".img"); // Element
img && img.src // 报错
// 要让类型更加精确
let img1 = <HTMLImageElement>document.querySelector(".img");
img1 && img1.src 
let img2 = document.querySelector(".img") as HTMLImageElement;
img2 && img2.src 

类型操作符

typeof

获取值的类型,注意:typeof操作的是值

let colors = {
  color1: "red",
  color2: "blue",
};

type tColors = typeof colors;
// 等同于
// type tColors = {
//   color1: "string",
//   color2: "string",
// }

let color3: tColors;

keyof

获取类型的所对应的key的集合,返回值是key的联合类型,注意:keyof操作的是类型

interface Person {
  name: string;
  age: number;
}

type a = keyof Person; // 等同于 type a = 'name' | 'age'

let data: a;
data = "name";
data = "age";
data = "gender"; // 报错
function css(ele: Element, attr: keyof CSSStyleDeclaration) {
  return getComputedStyle(ele)[attr];
}

let box = document.querySelector(".box");
box && css(box, "width");
box && css(box, "aaa"); // 报错
interface Person {
  name: string;
  age: number;
}
let p1: Person = {
  name: "zhangsan",
  age: 30,
};
function getPersonVal(k: keyof Person) {
  return p1[k];
}

let p2 = {
  name: "zhangsan",
  age: 30,
  gender: "Man",
};
function getPersonVal2(k: keyof typeof p2) {
  return p2[k];
}

in

操作符对值和类型都可以使用

针对值进行操作,用来判断值中是否包含指定的key

let a = "name" in { name: "zhangsan", age: 30 }; // true
let b = "gender" in { name: "zhangsan", age: 30 }; // false

针对类型进行操作的话,内部使用for...in对类型进行遍历

interface Person {
  name: string;
  age: number;
}
type personKeys = keyof Person;
type newPerson = {
  [k in personKeys]: string;
};

注意:in后边的类型值必须是 string 或 number 或 symbol

extends

类型继承操作符

interface type1 {
  x: number;
  y: number;
}

interface type2 extends type1 {
  z: string;
}

let a: type2 = {
  x: 1,
  y: 2,
  z: "3",
};

或者是这样:

type type1 = {
  x: number;
  y: number;
};
function fn<T extends type1>(args: T) {}
fn({ x: 1, y: 2 });
type type1 = {
  x: number;
  y: number;
}
// 合并
type type2 = type1 & {
   z: string;
}
let a: type2 = {
  x: 1,
  y: 2,
  z: "3",
};

接口的声明合并(Declaration Merging)

interface type1 = {
  x: number;
  y: number;
}
interface type1 = {
   z: string;
}

let a: type1 = {
  x: 1,
  y: 2,
  z: "3",
}

类型保护

typeof保护

function toUpperCase(str: string | string[]) {
  // str.length;
  // return str.toUpperCase(); // 报错

  if (typeof str === "string") {
    str.toUpperCase();
  } else {
    str.push();
  }
}

instanceof保护

function toUpperCase(str: string | string[]) {
  // str.length;
  // return str.toUpperCase(); // 报错

  if (str instanceof Array) {
    str.push();
  } else {
    str.toUpperCase();
  }
}

自定义类型保护

// data is Element[] | NodeList 是一种类型谓词,格式为: xx is type
function canEach(
  data: Element[] | NodeList | Element
): data is Element[] | NodeList {
  return (<NodeList>data).forEach !== undefined;
}

function fn2(elements: Element[] | NodeList | Element) {
  if (canEach(elements)) {
    elements.forEach(() => {});
  } else {
    elements.classList.add("box");
  }
}

8、泛型

泛型的概念

泛型,或者说提取了一类事物的共性特征的一种抽象。比如说,松树、柳树都是树,在程序里有3种表达:

  • 接口(Interface)
  • 继承(Inheritance)
  • 泛型(Generics)

继承是一种强表达。

松树继承于树,松树同时也是木材。这样关系的表达,要么让松树多重集成(树、木材),要么松树<-树<-木材。

无论哪种,增加程序设计复杂度,也加强了继承关系的维护成本(或者说耦合)。这么看,关系太强,反而不好!

接口是一种方面(Aspect)描述。比如松树可以生长,那么松树是:Growable;动植物都可以进化,那么它们是Evolvable。

一个类型可以拥有多个方面的特性。

**泛型(Generics)**是对共性的提取(不仅仅是描述)。

class BedMaker<T> {
    //....
    make(){
        
    }
}

const A = new BedMaker<红木>()
const B = new BedMaker<桃木>()
  • 木头可以制造床,但是不是所有的木头可以制造床
  • 制造床()这个方法,放到木头类中会很奇怪,因为木头不仅仅可以制造床
  • 同理,让木头继承于“可以制造床”这个接口也很奇怪

奇怪的代码展示:

class 红木 implements IMakeBed{
    makeBed(){...}
}

设计IMakeBed 的目标是为了拆分描述事物不同的方面(Aspect),其实还有一个更专业的词汇——关注点(Interest Point)。拆分关注点的技巧,叫做关注点分离。如果仅仅用接口,不用泛型,那么关注点没有做到完全解耦。

划重点:泛型是一种抽象共性(本质)的编程手段,它允许将类型作为其他类型的参数(表现形式),从而分离不同关注点的实现(作用)。

Array<T> 分离的是数据可以被线性访问、存储的共性。Stream<T>分离的是数据可以随着时间产生的共性。Promise<T>分离的是数据可以被异步计算的特性。

Hello泛型

// 一个identity函数是自己返回自己的函数
// 当然可以声明它是:number -> number
function identity(arg: number): number {
  return arg;
}

// 为了让identity支持更多类型可以声明它是any
function identity(arg: any): any {
  return arg;
}

// any会丢失后续的所有检查,因此可以考虑用泛型
function identity<Type>(arg: Type): Type {
  return arg;
}

let output = identity<string>("MyString")
// 不用显示的指定<>中的类型
// let output = identity("MyString")

output = 100 // Error
  • <>叫做钻石操作符,代表传入的类型参数

泛型类

泛型类的例子。

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

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
// (number, number) -> number
myGenericNumber.add = function (x, y) {
  return x + y;
};

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};

当然推荐将声明(Declaration)和定义(Definition)写到一起:

class GenericNumber<T> {
    zeroValue : T
    
    constructor(v : T){
        this.zeroValue = v
    }
    
    add(x : T, y : T) {
        return x + y
    }
}

泛型约束(Generic Constraints)

下面的程序会报错:

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length);
  // Property 'length' does not exist on type 'Type'.
  return arg;
}

考虑为arg增加约束:

interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); 
  return arg;
}

小技巧keyof 操作符

可以用keyof关键字作为泛型的约束。

type Point = { x: number; y: number };
type P = keyof Point;
// P = "x" | "y"

如下面这个例子:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

为什么可以这么做?

对TS而言所有对象的key是静态的。

const a = {x : 1, y : 2}
a.z = 3 // Error

因为是静态的,所以可以用keyof 操作符求所有的key。如果一个对象的类型是any ,那么keyof就没有意义了。

实例化泛型类型(将类作为参数)

function create<Type>(c: { new (): Type }): Type {
  return new c();
}

create(Foo) // Foo的实例

一个不错的例子:

class BeeKeeper {
  hasMask: boolean = true;
}

class ZooKeeper {
  nametag: string = "Mikle";
}

class Animal {
  numLegs: number = 4;
}

class Bee extends Animal {
  keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
  keeper: ZooKeeper = new ZooKeeper();
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;