一文快速拿下typescript~

993 阅读10分钟

PS:对前端技术感兴趣的朋友们,可以关注下我的《致力于前端的技术博客》哦!如果对你有帮助,欢迎赠个⭐️,会常更新内容,敬请期待!❤️❤️


基本配置(自动编译)

vscode 配置自动编译 ts 代码

  1. 运行 tsc --init,生成配置文件,改 outDir
  2. 运行任务,选择监视 tsconfig

ts数据类型

基本常用类型

布尔、数字、字符串

let flag: boolean = true;
let num: number = 123;
let str: string = "hello";

数组

let arr1: number[] = [1, 2, 3];
let arr2: any[] = [1, "ji", 3];
let arr3: Array<number> = [1, 2, 3];

元祖(属于数组的一种)

let tuple: [number, string] = [123, "hello"];

枚举

enum Status {
  success = 1,
  error = 2
}
enum Color {
  blue,
  red,
  white
} //如果未赋值,结果打印下标
//let s: Status = Status.success
//console.log(s);

对象

declare function create(o: object | null): void;
create({ prop: 0 });

特殊类型

任意类型any

let random: any = 123;
//null和undefined 其他类型的子类型
let num1: number | undefined; //定义未赋值就是undefined
let num2: number | null | undefined;

null和undefined

let num: number;
let str: string;

// 这些类型能被赋予
num = null;
str = undefined;

空类型void(方法没有返回值)

function run1(): void {
  console.log("void");
}
function run2(): number {
  return 1;
}

never 类型(表示从来不会出现的值,基本用不到)

一个从来不会有返回值的函数(如:如果函数内含有 while(true) {}); 一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') },foo 的返回类型是 never);

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


//使用场景(详细检查)
function foo(x: string | number): boolean {
  if (typeof x === 'string') {
    return true;
  } else if (typeof x === 'number') {
    return false;
  }

  // 如果不是一个 never 类型,这会报错:
  // - 不是所有条件都有返回值 (严格模式下)
  // - 或者检查到无法访问的代码
  // 但是由于 TypeScript 理解 `fail` 函数返回为 `never` 类型
  // 它可以让你调用它,因为你可能会在运行时用它来做安全或者详细的检查。
  return fail('Unexhaustive');
}

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

never 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 void,然而实际上,void 表示没有任何类型,never 表示永远不存在的值的类型。

泛型(在计算机科学中,许多算法和数据结构并不会依赖于对象的实际类型。然而,你仍然会想在每个变量里强制提供约束)

function reverse<T>(items: T[]): T[] {
  const toreturn = [];
  for (let i = items.length - 1; i >= 0; i--) {
    toreturn.push(items[i]);
  }
  return toreturn;
}

联合类型(在 JavaScript 中,你希望属性为多种类型之一,如字符串或者数组。这就是联合类型所能派上用场的地方)

function formatCommandline(command: string[] | string) {
  let line = '';
  if (typeof command === 'string') {
    line = command.trim();
  } else {
    line = command.join(' ').trim();
  }
  // Do stuff with line: string
}

交叉类型(在 JavaScript 中, extend 是一种非常常见的模式,在这种模式中,你可以从两个对象中创建一个新对象,新对象会拥有着两个对象所有的功能。)

function extend<T, U>(first: T, second: U): T & U {
  const result = <T & U>{};
  for (let id in first) {
    (<T>result)[id] = first[id];
  }
  for (let id in second) {
    if (!result.hasOwnProperty(id)) {
      (<U>result)[id] = second[id];
    }
  }

  return result;
}

const x = extend({ a: 'hello' }, { b: 42 });

// 现在 x 拥有了 a 属性与 b 属性
const a = x.a;
const b = x.b;

【tips】

  • 如果你需要使用类型注解的层次结构,请使用接口。
  • 它能使用 implements 和 extends 为一个简单的对象类型(像例子中的Coordinates)使用类型别名,仅仅有一个语义化的作用。与此相似,当你想给一个联合类型和交叉类型使用一个语意化的名称时,一个类型别名将会是一个好的选择。

ts函数

函数声明

function fn1(): string {
  return "run";
}
let fn2 = function(): string {
  return "run2";
};

方法传参

function getInfo1(name: string, age: number): string {
  return `${name}---${age}`;
}
let getInfo2 = function(name: string, age: number): string {
  return `${name}---${age}`;
};

可选参数

function get(name: string, age?: number): string {
  if (age) {
    return `${name}---${age}`;
  } else {
    return `${name}---年龄保密`;
  }
}

默认参数

function get1(name: string, age: number = 20): string {
  return `${name}---${age}`;
}

剩余参数

function sum(a: number, b: number, c: number): number {
  return a + b + c;
}

// 三点运算符接收形参
function sum1(...result: number[]): number {
  let sum = 0;
  for (let i = 0; i < result.length; i++) {
    sum += result[i];
  }
  return sum;
}
function sum2(initNum: number, ...result: number[]): number {
  let sum = initNum;
  for (let i = 0; i < result.length; i++) {
    sum += result[i];
  }
  return sum;
}

函数重载

es5 中同名方法会存在替换

function info1(name: string): string;
function info1(age: number): number;
function info1(str: any): any {
  if (typeof str === "string") {
    return `我叫${str}`;
  } else {
    return `我的年龄是${str}`;
  }
}
//console.log(info1('Amy'));
//console.log(info1(12));

function info2(name: string): string;
function info2(name: string, age: number): string;
function info2(name: any, age?: any): any {
  if (age) {
    return `我叫${name},我的年龄是${age}`;
  } else {
    return `我叫${name}`;
  }
}
// 重载
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d
  };
}
interface Overloaded {
  (foo: string): string;
  (foo: number): number;
}

// 实现接口的一个例子:
function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
  if (typeof foo === 'number') {
    return foo * foo;
  } else if (typeof foo === 'string') {
    return `hello ${foo}`;
  }
}

const overloaded: Overloaded = stringOrNumber;

// 使用
const str = overloaded(''); // str 被推断为 'string'
const num = overloaded(123); // num 被推断为 'number'

TypeScript 中的函数重载没有任何运行时开销。它只允许你记录希望调用函数的方式,并且编译器会检查其余代码。

ts中的类

es5 的继承(原型链+对象冒充实现继承)

function Person(name, age) {
  this.name = name;
  this.age = age;
}
function Student() {
  Person.call(this, name, age);
  //对象冒充(只能继承构造函数中的属性和方法,可以向父类传参)
}
Student.prototype = new Person();
//原型链继承(既可以继承构造函数中的属性和方法,也可以继承原型链上的属性和方法,但是实例化时无法向父类传参)
Student.prototype = Person.prototype; //另一种写法

定义类

class Person {
  name: string; //省略了public关键字
  constructor(name: string) {
    this.name = name;
  }
  run(): void {
    console.log(this.name);
  }
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}

实现继承 extends super

class Student extends Person {
​    constructor(name: string) {
​        super(name)
​    }
​    **子类扩展方法,子类也可以覆盖父类的方法
​    work(): void {
​        alert('在运动')
​    }
}

类修饰符

  • public:在类内部、子类、和外部均可以访问
  • protected:在类内部和子类可以访问,外部无法访问
  • private:私有,只有类内部可以访问

属性如果不加修饰符,默认是 public

静态属性&&静态方法

应用举例(jquery)

function $(element) {
  return new Base(element);
}
//静态方法
$.get = function() {};
function Base(element) {
  this.element = element;
  this.css = function(attr, value) {
    this.element.style.attr = value;
  };
}
class Person1 {
  name: string; //省略了public关键字
  constructor(name: string) {
    this.name = name;
  } //静态属性
  static sex = "男"; //静态方法,无法直接调用类的属性,只能调用静态属性
  static print(): void {
    alert("静态方法");
  } //实例方法
  getName(): string {
    return this.name;
  }
  setName(name: string): void {
    this.name = name;
  }
}

多态

多态是指父类定义方法不去实现,让继承它的子类去实现,每个子类有不同的表现

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat() {
    console.log("吃的方法");
  }
}
class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }
  eat() {
    return this.name + "吃肉";
  }
}
class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }
  eat() {
    return this.name + "吃鱼";
  }
}

抽象类

抽象类和抽象方法用来定义标准,要求子类必须实现抽象类中的所有方法,abstract 关键字,无法创造抽象类的实例

abstract class Animal1 {
  public name: string;
  constructor(name: string) {
    this.name = name;
  }
  abstract eat(): any;
}
class Dog1 extends Animal1 {
  constructor(name: any) {
    super(name);
  }
  eat() {
    console.log(`${this.name}吃肉`);
  }
}

typescript 接口

属性接口

//对传入地值进行约束
function print_label1(label: string): void {
  console.log(label);
}
function print_label2(labelInfo: { label: string }): void {
  console.log(labelInfo);
}
//接口(批量方法约束)
interface FullName {
  firstname: string;
  lastname: string; //可选属性 lastname?: string;
}
//传到的参数必须包含firstname、lastname
function printName(name: FullName) {
  console.log(`${name.firstname}---${name.lastname}`);
}

函数类型接口

interface encrypt {
  (key: string, value: string): string;
}

let md5: encrypt = function(key: string, value: string) {
  return key + value;
};
let sha1: encrypt = function(key: string, value: string) {
  return key + "---" + value;
};

可索引接口

interface UserArr {
  [index: number]: string;
}
let arr: UserArr = ["1", "2", "3"];

interface UserObj {
  [index: string]: string;
}
let obj: UserObj = { name: "zhangsan", age: "12" };

类类型接口

interface AnimalI {
  name: string;
  eat(str: string): void;
}
class a1 implements AnimalI {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat(food: string) {
    console.log(`我是${this.name},我吃${food}`);
  }
}

接口扩展

接口集成接口,类实现接口时需要实现所有的方法

interface Ani {
  eat(): void;
}
//接口也可以相互继承
interface Per extends Ani {
  work(): void;
}
class Programmer {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  coding(code: string) {
    console.log(this.name + code);
  }
}
//集成父类+实现接口
class Web1 extends Programmer implements Per {
  constructor(name: string) {
    super(name);
  }
  eat() {}
  work() {}
}

ts泛型

泛型函数

//泛型就是解决类\接口\方法的复用性 以及对不特定数据类型的支持
function getData(value: string): string {
  return value;
}
//如果要传入什么类型,返回什么类型,就要使用到泛型
function getData1<T>(value: T): T {
  return value;
}
getData1<number>(123);
getData1<string>("abc");

泛型类

//(原始)
class MinClass {
  list: number[] = [];
  add(value: number): void {
    this.list.push(value);
  }
  min(): number {
    let min = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
      if (min > this.list[i]) {
        min = this.list[i];
      }
    }
    return min;
  }
}
//(泛型)
class MinClass1<T> {
  public list: T[] = [];
  add(value: T): void {
    this.list.push(value);
  }
  min(): T {
    let min = this.list[0];
    for (let i = 0; i < this.list.length; i++) {
      if (min > this.list[i]) {
        min = this.list[i];
      }
    }
    return min;
  }
}

let m1 = new MinClass1<number>();
m1.add(1);
m1.add(2);
m1.add(3);
alert(m1.min());

let m2 = new MinClass1<string>();
m2.add("a");
m2.add("b");
m2.add("c");
alert(m2.min());

泛型接口

//(普通函数类型接口)
interface ConfigFn0 {
  (value1: string, value2: string): string;
}
let setData0: ConfigFn0 = function(value1: string, value2: string) {
  return value1 + value2;
};
//泛型接口(写法1)
interface ConfigFn1 {
  <T>(value1: T): T;
}
let setData1: ConfigFn1 = function<T>(value: T): T {
  return value;
};
setData1 < string > "abdc";
//泛型接口(写法2)
interface ConfigFn2<T> {
  (value1: T): T;
}
function myGetData<T>(value: T): T {
  return value;
}
let myFn: ConfigFn2<string> = myGetData;

其他补充

枚举类型

enum CardSuit {
  Clubs,
  Diamonds,
  Hearts,
  Spades
}
// 简单的使用枚举类型
let Card = CardSuit.Clubs;
// 类型安全
Card = 'not a member of card suit'; // Error: string 不能赋值给 `CardSuit` 类型

元组类型

let nameNumber: [string, number];
// Ok
nameNumber = ["Jenny", 221345];
// Error
nameNumber = ["Jenny", "221345"];
// 将其与 TypeScript 中的解构一起使用:
const [name, num] = nameNumber;

类型别名

type StrOrNum = string | number;
// 使用
let sample: StrOrNum;
sample = 123;
sample = "123";
// 会检查类型
sample = true; // Error

type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;

tips 如果你需要使用类型注解的层次结构,请使用接口。它能使用 implements 和 extends 为一个简单的对象类型(像例子中的 Coordinates)使用类型别名,仅仅有一个语义化的作用。与此相似,当你想给一个联合类型和交叉类型使用一个语意化的名称时,一个类型别名将会是一个好的选择。

类型断言

const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = "hello"; // Error: 'bas' 属性不存在于 '{}'
// 这里的代码发出了错误警告,因为 foo 的类型推断为 {},即是具有零属性的对象。因此,你不能在它的属性上添加 bar 或 bas
interface Foo {
  bar: number;
  bas: string;
}
const foo = {} as Foo;
foo.bar = 123;
foo.bas = "hello";

双重断言

function handler(event: Event) {
  const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一个都不能赋值给另外一个
}
//如果你仍然想使用那个类型,你可以使用双重断言。首先断言成兼容所有类型的 any,编译器将不会报错:
function handler(event: Event) {
  const element = (event as any) as HTMLElement; // ok
}

类型保护

function doSome(x: number | string) {
  if (typeof x === "string") {
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }
  x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}
function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  }
  if (arg instanceof Bar) {
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}
function doStuff(arg: Foo | Bar) {
  if (arg instanceof Foo) {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    // 这个块中,一定是 'Bar'
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}
function doStuff(q: A | B) {
  if ("x" in q) {
    // q: A
  } else {
    // q: B
  }
}
function doStuff(arg: Foo | Bar) {
  if (arg.kind === "foo") {
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    // 一定是 Bar
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

自变量类型

type CardinalDirection = "North" | "East" | "South" | "West";

function move(distance: number, direction: CardinalDirection) {
  // ...
}
move(1, "North"); // ok
move(1, "Nurth"); // Error

type OneToFive = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;

使用用例

TypeScript 枚举类型是基于数字的,你可以使用带字符串字面量的联合类型,来模拟一个基于字符串的枚举类型,就好像上文中提出的 CardinalDirection。你甚至可以使用下面的函数来生成 key: value 的结构: // 用于创建字符串列表映射至 K: V 的函数

// 用于创建字符串列表映射至 `K: V` 的函数
function strEnum<T extends string>(o: Array<T>): { [K in T]: K } {
  return o.reduce((res, key) => {
    res[key] = key;
    return res;
  }, Object.create(null));
}
// 创建 K: V
const Direction = strEnum(["North", "South", "East", "West"]);
// 创建一个类型
type Direction = keyof typeof Direction;
// 简单的使用
let sample: Direction;
sample = Direction.North; // Okay
sample = "North"; // Okay
sample = "AnythingElse"; // ERROR!

readonly

function foo(config: { readonly bar: number; readonly bas: number }) {
  // ..
}
type Foo = {
  readonly bar: number;
  readonly bas: number;
};
class Foo {
  readonly bar = 1; // OK
  readonly baz: string;
  constructor() {
    this.baz = "hello"; // OK
  }
}

这有一个 Readonly 的映射类型,它接收一个泛型 T,用来把它的所有属性标记为只读类型:

type Foo = {
  bar: number;
  bas: number;
};
type FooReadonly = Readonly<Foo>;
const foo: Foo = { bar: 123, bas: 456 };
const fooReadonly: FooReadonly = { bar: 123, bas: 456 };
foo.bar = 456; // ok
fooReadonly.bar = 456; // Error: bar 属性只读

设置为绝对的而不可变

你甚至可以把索引签名标记为只读:

interface Foo {
  readonly [x: number]: number;
}
// 使用
const foo: Foo = { 0: 123, 2: 345 };
console.log(foo[0]); // ok(读取)
foo[0] = 456; // Error: 属性只读

自动推断

在一些情况下,编译器能把一些特定的属性推断为 readonly,例如在一个 class 中,如果你有一个只含有 getter 但是没有 setter 的属性,他能被推断为只读:

class Person {
  firstName: string = "John";
  lastName: string = "Doe";

  get fullName() {
    return this.firstName + this.lastName;
  }
}
const person = new Person();
console.log(person.fullName); // John Doe
person.fullName = "Dear Reader"; // Error, fullName 只读

readonly 与 const 的不同点 const 用于变量;变量不能重新赋值给其他任何事物。 readonly 用于属性;用于别名,可以修改属性;

使用泛型

例如当你想创建一个字符串的队列时,你将不得不再次修改相当大的代码。我们真正想要的一种方式是无论什么类型被推入队列,被推出的类型都与推入类型一样。当你使用泛型时,这会很容易:

// 创建一个泛型类
class Queue<T> {
  private data: T[] = [];
  push = (item: T) => this.data.push(item);
  pop = (): T | undefined => this.data.shift();
}

// 简单的使用
const queue = new Queue<number>();
queue.push(0);
queue.push("1"); // Error:不能推入一个 `string`,只有 number 类型被允许

tip: 你可以随意调用泛型参数,当你使用简单的泛型时,泛型常用 T、U、V 表示。如果在你的参数里,不止拥有一个泛型,你应该使用一个更语义化名称,如 TKey 和 TValue (通常情况下,以 T 做为泛型前缀也在如 C++ 的其他语言里做为模版。)