TypeScript on the way:学习TypeScript

个人博客:Sekyoro的博客小屋
个人网站:Proanimer的个人网站

早该学学了.

之前写过Python的类型系统,如果对于写C++,Java,C#等这类语言来说,typing根本不成问题,所以理解TypeScript也不是问题.

特殊的类型

any,unknown与never

any,unknown是"顶层类型",never是"底层类型".never类型是所有类型共有的,any类型基本没有限制,unknown类型不能直接调用并且运算是有限的,只能进行比较运算.推荐使用unknown代替any然后使用as转换类型.

类型系统

String与string,Number与number

String与string是不同的,前者是可以包含后者的.但是在ts中,很多方法只能使用后者.

所以推荐只使用后者.

image-20240211171858237

let obj: Object;
let obj2:{};
obj = { name: "John" };
obj = true;

此外Object类型包括除了undefined和null的基本类型.所以这并不符合直觉,推荐使用object

let obj3:object;
obj3 = {name:"John"};
obj3 = 13; //报错 不能将number分配给类型object

object类型包含对象,数组,函数.

const ccx = { foo: 1 };
ccx.foo = 2;
let t = { foo: 1 };
t.foo = 3;
let hh:object = {foo:1}
// hh.foo 报错 类型不对

此外undefined和null也可以赋值为number,object等等.

TypeScript中单个值也是类型成为值类型

let t: "dfasdf";
const xy = "https";
console.log(xy);

将多个类型组合起来就是联合类型,如果严格检查也就是设置strictNullChecks,使得其他类型变量不能被赋值为undefined或null.这个时候就可以用联合类型

let setting: true | false;

let gender: "male" | "female";

let rainbowColor: "赤" | "橙" | "黄" | "绿" | "青" | "蓝" | "紫";
let name: string | null;

name = "John";
name = null;

对象的合成可以给对象添加新的属性,属于交叉类型.

let obj5: { foo: string } & { bar: number };

类型别名

type Age = number;
let age:Age =  55;

跟Python的typing和Go语言类似.

数组 元组

let arr: number[] = [];
let arr2: (number|string)[] = [];
let arr3: Array<number> = [];

const数组中的元素是可以改变的,所以在ts中增加了readonly,readonly数组是原本数组的子类型.

const arr5: number[] = [0, 1];
arr5[0] = 3;
let arr6: readonly number[] = arr5;

声明readonly数组

let aa: readonly number[] = [1, 2, 3];
let a1: ReadonlyArray<number> = [1, 2, 3];
let a2: Readonly<number[]> = [];
let a3 = [] as const;

TypeScript 推断类型时,遇到const命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。

const命令声明的变量,如果赋值为对象,并不会推断为值类型,这是因为 JavaScript 里面,const变量赋值为对象时,属性值是可以改变的(数组等同理)

元组tuple

const s: [string, string, boolean] = ["a", "b", true];

使用元组时必须声明类型不然会默认数组.

let ot: [number, string?] | undefined = [1];

使用扩展运算符可以不下成员数量的元组.

元组也有只读元组

let readonlyTuple: readonly [number] = [1];
let point = [3, 4] as const;

symbol类型

symbol主要用于类的属性.

ts增加了unique symbol作为symbol的子类型.

// 正确
const x: unique symbol = Symbol();

// 报错
let y: unique symbol = Symbol();
const x: unique symbol = Symbol();
// 等同于
const x = Symbol();

const a: unique symbol = Symbol();
const b: typeof a = a; // 正确

感觉平常可能用不上…

函数 对象 interface

function hello(txt: string): void {
  console.log("hello " + txt);
}

// 写法一
const hello = function (txt: string) {
  console.log("hello " + txt);
};

// 写法二
const hello: (txt: string) => void = function (txt) {
  console.log("hello " + txt);
};

函数声明与函数变量声明.前者需要声明参数类型,否则默认为any.后者可以在选择在赋值时写出类型或者在声明变量时添加类型.此外还有这种写法

let add: {
  (x: number, y: number): number;
};

add = function (x, y) {
  return x + y;
};
箭头函数
const repeat = (str: string, times: number): string => str.repeat(times);

另外使用?表示可选参数

function f(x?: number) {
  // ...
}

f(); // OK
f(10); // OK

默认值也类似.

function createPoint(x: number = 0, y: number = 0): [number, number] {
  return [x, y];
}

createPoint(); // [0, 0]

rest参数也可以用于将多个值包裹为数组或元组

function joinNum(...nums: [...number[]]): string {
  console.log(nums);
  return nums.join(" ");
}
joinNum(1, 2, 3, 4, 5);


function joinNumAndString(...args: [string, number]) {
  console.log(args);
}

joinNumAndString("a", 1);

参数也可以使用readonly进行修饰.

此外函数返回有void和never类型.前者表示没有返回值(或undefined)后者表示不会退出,常用于丢错误或循环.

函数重载

不同于其他语言重载,

有一些编程语言允许不同的函数参数,对应不同的函数实现。但是,JavaScript 函数只能有一个实现,必须在这个实现当中,处理不同的参数。因此,函数体内部就需要判断参数的类型及个数,并根据判断结果执行不同的操作。

function reverse(str: string): string;
function reverse(arr: any[]): any[];
function reverse(stringOrArray: string | any[]): string | any[] {
  if (typeof stringOrArray === "string")
    return stringOrArray.split("").reverse().join("");
  else return stringOrArray.slice().reverse();
}

重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明

构造函数

type AnimalConstructor = new () => Animal;

function create(c: AnimalConstructor): Animal {
  return new c();
}
create(Animal);

构造函数的类型写法,就是在参数列表前面加上new命令

此外也有对象形式写法

type F = {
  new (s: string): object;
}

针对对象,既可以使用type别名也可以使用interface

interface ReadOnlyPerson {
  readonly name: string;
  readonly age: number;
}

let w:ReadOnlyPerson = {
  name:"John",
  age: 22
}

空对象是 TypeScript 的一种特殊值,也是一种特殊类型。

TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。

const obj = {};
obj.prop = 123; // 报错

因为Object可以接受各种类型的值,而空对象是Object类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。

interface Empty {}
const b: Empty = { myProp: 1, anotherProp: 2 }; // 正确
b.myProp; // 报错
let d: {};
// 等同于
// let d:Object;

d = {};
d = { x: 1 };
d = "hello";
d = 2;

interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface 可以表示对象的各种语法,它的成员有 5 种形式。

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数

interface 与 type 的区别有下面几点。

(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。

(2)interface可以继承其他类型,type不支持继承。

可以在interface中写方法以及利用interface写函数,构造函数.

// 写法一
interface A {
  f(x: boolean): string;
}

// 写法二
interface B {
  f: (x: boolean) => string;
}

// 写法三
interface C {
  f: { (x: boolean): string };
}

interface Add {
  (x: number, y: number): number;
}

const myAdd: Add = (x, y) => x + y;

interface ErrorConstructor {
  new (message?: string): Error;
}

interface可以实现继承,而type不行.而且可以多继承.多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错

interface Shape {
  name: string;
}

interface Circle extends Shape {
  radius: number;
}

interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}

type Country = {
  name: string;
  capital: string;
};

interface CountryWithPop extends Country {
  population: number;
}

注意,如果type命令定义的类型不是对象,interface 就无法继承

多个同名接口会进行合并.

interface Box {
  height: number;
  width: number;
}

interface Box {
  length: number;
}

举例来说,Web 网页开发经常会对windows对象和document对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。

interface A {
  f(x: "foo"): boolean;
}

interface A {
  f(x: any): void;
}

// 等同于
interface A {
  f(x: "foo"): boolean;
  f(x: any): void;
}

如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型

interface Circle {
  area: bigint;
}

interface Rectangle {
  area: number;
}

declare const s: Circle | Rectangle;

s.area; // bigint | number

对于顶层声明的属性,可以在声明时同时给出类型,如果不给声明默认any.

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

TypeScript 有一个配置项strictPropertyInitialization,只要打开,就会检查属性是否设置了初值,如果没有就报错。

image-20240212174733858

如果打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。

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

泛型类

class Box<Type> {
  contents: Type;

  constructor(value: Type) {
    this.contents = value;
  }
}

const b: Box<string> = new Box("hello!");
class Pair<K, V> {
  key: K;
  value: V;
}

抽象类

abstract class A {
  foo: number;
}

abstract class B extends A {
  bar: string;
}

抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

泛型

function getFirst<Type>(arr: Type[]): Type {
  return arr[0];
}

不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断,有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出.

类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用T(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。

泛型主要用在四个场合:函数、接口、类和别名。

function id<T>(arg: T): T {
  return arg;
}

function id<T>(arg: T): T {
  return arg;
}
let myid: <T>(arg: T) => T = id;
interface Box<Type> {
  contents: Type;
}

let box: Box<string>;

类型别名

type Nullable<T> = T | undefined | null

type Container<T> = { value: T };
type Tree<T> = {
  value: T;
  left: Tree<T> | null;
  right: Tree<T> | null;
};

类型参数默认值

function getFirst_<T = string>(arr: T[]): T {
  return arr[0];
}

类型参数的约束条件

function comp<Type extends { length: number }>(a: Type, b: Type) {
  if (a.length > b.length) {
    return a;
  }
  return b;
}

type Fn<A extends string, B extends string = "world"> = [A, B];
type Result = Fn<"hello">

类型参数的约束条件如下

<TypeParameter extends ConstraintType>

泛型使用注意:

  1. 尽量少用泛型
  2. 类型参数越少越好
  3. 类型参数需要出现两次
  4. 泛型可以嵌套

Enum类型

enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

Enum 结构本身也是一种类型。比如,上例的变量c等于1,它的类型可以是 Color,也可以是number

多个同名的 Enum 结构会自动合并。

const enum MediaTypes {
  JSON = "application/json",
  XML = "application/xml",
}

const url = "localhost";

fetch(url, {
  headers: {
    Accept: MediaTypes.JSON,
  },
}).then((response) => {
  // ...
});

类型断言

// 语法一
let bar: T = <T>foo;

// 语法二
let bar: T = foo as T;

类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型

此外还有as const断言,s const断言只能用于字面量,as const也不能用于表达式

或者先断言为unknown.

expr as unknown as T;

对于那些可能为空的变量(即可能等于undefinednull),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号!

const root = document.getElementById("root")!;

断言函数

function isString(value: unknown): asserts value is string {
  if (typeof value !== "string") throw new Error("Not a string");
}

模块和namespace

TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。

export type Bool = true | false;

模块加载方式有classic和Node,也就是Command js和ES6.

namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。

它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。

namespace Utils {
  function isString(value: any) {
    return typeof value === "string";
  }

  // 正确
  isString("yes");
}

Utils.isString("no"); // 报错

如果要在命名空间以外使用内部成员,就必须为该成员加上export前缀,表示对外输出该成员

namespace Utility {
  export function log(msg: string) {
    console.log(msg);
  }
  export function error(msg: string) {
    console.error(msg);
  }
}

Utility.log("Call me");
Utility.error("maybe!");

装饰器

装饰器(Decorator)是一种语法结构,用来在定义时修改类(class)的行为。

在语法上,装饰器有如下几个特征。

(1)第一个字符(或者说前缀)是@,后面是一个表达式。

(2)@后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。

(3)这个函数接受所修饰对象的一些相关值作为参数。

(4)这个函数要么不返回值,要么返回一个新对象取代所修饰的目标对象。

装饰器函数和装饰器方法

type Decorator = (
  value: DecoratedValue,
  context: {
    kind: string;
    name: string | symbol;
    addInitializer?(initializer: () => void): void;
    static?: boolean;
    private?: boolean;
    access: {
      get?(): unknown;
      set?(value: unknown): void;
    };
  }
) => void | ReplacementValue;


type ClassDecorator = (
  value: Function,
  context: {
    kind: "class";
    name: string | undefined;
    addInitializer(initializer: () => void): void;
  }
) => Function | void;
function countInstances(value: any, context: any) {
  let instanceCount = 0;

  const wrapper = function (...args: any[]) {
    instanceCount++;
    const instance = new value(...args);
    instance.count = instanceCount;
    return instance;
  } as unknown as typeof MyClass;

  wrapper.prototype = value.prototype; // A
  return wrapper;
}

@countInstances
class MyClass {}

const inst1 = new MyClass();
inst1 instanceof MyClass; // true
inst1.count; // 1

declare关键字

declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。

它的主要作用,就是让当前文件可以使用其他文件声明的类型。举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用declare关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。

declare 关键字可以描述以下类型。

  • 变量(const、let、var 命令声明)
  • type 或者 interface 命令声明的类型
  • class
  • enum
  • 函数(function)
  • 模块(module)
  • 命名空间(namespace)

declare let x: number;
declare function sayHello(name: string): void;

sayHello("张三");
declare class Animal {
  constructor(name: string);
  eat(): void;
  sleep(): void;
}
declare namespace AnimalLib {
  class Animal {
    constructor(name: string);
    eat(): void;
    sleep(): void;
  }

  type Animals = "Fish" | "Dog";
}

// 或者
declare module AnimalLib {
  class Animal {
    constructor(name: string);
    eat(): void;
    sleep(): void;
  }

  type Animals = "Fish" | "Dog";
}

d.ts类型声明文件

可以为每个模块脚本,定义一个.d.ts文件,把该脚本用到的类型定义都放在这个文件里面。但是,更方便的做法是为整个项目,定义一个大的.d.ts文件,在这个文件里面使用declare module定义每个模块脚本的类型

使用时,自己的脚本使用三斜杠命令,加载这个类型声明文件。

/// <reference path="node.d.ts"/>

如果没有上面这一行命令,自己的脚本使用外部模块时,就需要在脚本里面使用 declare 命令单独给出外部模块的类型。

单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。

类型声明文件里面只有类型代码,没有具体的代码实现。它的文件名一般为[模块名].d.ts的形式,其中的d表示 declaration(声明)

/// <reference path="node.d.ts"/>
import { test } from "./test";
declare let x: number;
x = 1;
console.log(x);
console.log(test);
let p: Post = { id: 1, title: "title", content: "content" };

// node.d.ts
interface Post {
  id: number;
  title: string;
  content: string;
}

最后推荐两个练习网站:

参考资料

  1. https://typescript.p6p.net
  2. TypeScript: Handbook - The TypeScript Handbook (typescriptlang.org)

如有疑问,欢迎各位交流!

服务器配置
宝塔:宝塔服务器面板,一键全能部署及管理
云服务器:阿里云服务器
Vultr服务器
GPU服务器:Vast.ai

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

procoder338

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值