Typescript 知识点汇总(一) 数据类型

337 阅读12分钟

一、前言

因为公司项目中还没有引入typescript,而且由于自己犯懒哈哈,感觉自己落下很久了,现在对于ts的理解还仅限于类型检查基础功能使用,对于一些高级用法一无所知,所以这次想通过笔记的方式对ts做一次学习总结。学习的时候一定看typescript英文文档,因为typescript中文网上有些东西已经过时了。

二、类型标注

: 用于类型标注

在下面这个例子中,使用了变量、函数参数以及函数返回值的类型标注:

let myName: string = "Alice";

function identity(num: number): number {
  return num;
}

三、数据类型

1. 原始类型

1.1 字符串类型(string)

let name: string='xiaoming';

1.2 数字类型(number)

let age: number=10;

1.3 布尔类型(boolean)

let flag: boolean=false;

注意:

The type names StringNumber, and Boolean (starting with capital letters) are legal, but refer to some special built-in types that will very rarely appear in your code. Always use stringnumber, or boolean for types. 类型名字 String、 Number 和 Boolean (以大写字母开头)是合法的,但是引用一些特殊的内置类型,这些类型很少出现在代码中。对于类型,总是使用stringnumber, or boolean

2.数组类型

Array<T>,T代表数组中的元素类型。

let arr1: number[] = [1, 2, 3]; 
let arr2: Array<number> = [4, 5, 6];

3.Tuple 元组类型

Tuple 类型是另一种 Array 类型,它确切地知道它包含多少个元素,以及它在特定位置包含哪些类型,访问跨界元素时,ts会报错

const tuple: [number, string] = [2022, 'hello world']

4.Enum 枚举类型

4.1 普通枚举

enum Gender{
    FEMALE, // 0
    MALE  // 1
}

//简单的使用枚举类型
let User = Gender.FEMALE 
console.log(User) // 0  输出结果默认是数字0

注意: 默认情况下,第一个枚举值是 0,然后每个后续值依次递增 1。

可以通过特定的赋值来改变给任何枚举成员关联的数字:

enum Gender{
    FEMALE = 2, // 2
    MALE  // 3
}

枚举类型编译后是什么样呢?我们看下面的示例:

// 枚举类型
enum Gender{
    FEMALE,
    MALE
}

// 编译后
var Gender;
(function (Gender) {
    Gender[Gender["FEMALE"] = 0] = "FEMALE";
    Gender[Gender["MALE"] = 1] = "MALE";
})(Gender || (Gender = {}));

我们看 Gender[Gender["FEMALE"] = 0] = "FEMALE"这行,Gender["FEMALE"] = 0 的意思是将 Gender 对象里的 FEMALE 成员值设置为 0 ,因此得到 Gender[0] = "FEMALE"。这样你可以使用 Gender 变量进行反向映射,如下所示:

enum Gender{
    FEMALE,
    MALE
}

console.log(Gender[0]); // 'FEMALE'
console.log(Gender['FEMALE']); // 0
console.log(Gender[Gender.FEMALE]); // 'FEMALE' 因为 Gender.FEMALE == 0

4.2 常量枚举

使用 const 枚举

const enum Gender {
    FEMALE, 
    MALE
} 
let genders = Gender.FEMALE;

// 编译后
var genders = 0 /* FEMALE */;

对比上面普通枚举的编译,发现常量枚举编译后对于Gender没有了,只有下面声明的变量 genders = 0。

5. any 任意类型

any 类型在 TypeScript 类型系统中占有特殊的地位。它提供给你一个类型系统的「后门」,TypeScript 将会把类型检查关闭。在类型系统里 any 能够兼容所有的类型(包括它自己)。因此,所有类型都能被赋值给它,它也能被赋值给其他任何类型。

let power: any;

// 赋值任意类型
power = '123';
power = 123;

// 它也兼容任何类型
let num: number;
power = num;
num = power;

6.null 和 undefined

在类型系统中,JavaScript 中的 null 和 undefined 字面量和其他被标注了 any 类型的变量一样,都能被赋值给任意类型的变量。示例如下:

// strictNullChecks: false

let num: number;
let str: string;

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

注意: strictNullChecks 严格空检查,在tsconfig.json文件中进行配置,这里如果配置成true的话,上面示例就会报错,就不能把 nullundefined 赋值给其他类型,它们只能赋值给自己这种类型或者 any

7. never类型

never 代表不会出现的值。

  • 一个从来不会有返回值的函数(如:如果函数内含有 while(true) {});

    function loop():never{
        while(true){}
    }
    
  • 一个总是会抛出错误的函数(如:function error() { throw new Error('报错了') }error 的返回类型是 never);

    function error():never{ 
        throw new Error('报错了') 
    }
    

strictNullChecks

  • 在 TS 中, null 和 undefined 是任何类型的有效值,所以无法正确地检测它们是否被错误地使用。于是 TS 引入了 strictNullChecks 这一种检查模式

  • 由于引入了 --strictNullChecks ,在这一模式下,null 和 undefined 能被检测到。所以 TS 需要一种新的底部类型( bottom type )。所以就引入了 never。

    看一个示例:

    function foo(x: string | number) {
      if (typeof x === 'string') {
        console.log(x) // x 是 string类型
      } else if (typeof x === 'number') {
        console.log(x)  // x 是 number类型
      }else{
        console.log(x) // 这里的x是never类型,因为永远走不到这里
       // strictNullChecks 模式下,这里的代码将不会被执行,x 无法被观察
      }
    }
    

8. void 类型

void 来表示一个函数没有一个返回值

function log(message: string): void {
  console.log(message);
  
  // 返回值 void 的时候,它的非严格模式(strictNullChecks:false)下仅可以返回 null 和 undefined 
  //严格模式(strictNullChecks:true)下只能返回undefined 
  //return null;
  //return undefined;
}

严格模式下 (strictNullChecks:true),如果返回 null 会报错 不能将类型“null”分配给类型“void”

void 与 never 的区别

void 表示没有任何类型,never 表示永远不存在的值的类型。

当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。void 类型可以被赋值(在 strictNullCheckingfalse 时),但是除了 never 本身以外,其他任何类型不能赋值给 never

9.Symbol

  • Symbol 是在ES2015之后成为新的原始类型,它通过 Symbol 构造函数创建
  • Symbol 的值是唯一不变的
let sym2 = Symbol("key");
let sym3 = Symbol("key");
console.log(sym2 === sym3) // false, symbols are unique

10. BigInt

  • 使用 BigInt 可以安全地存储和操作大整数

    const max = Number.MAX_SAFE_INTEGER;// 最大数字 2**53-1
    console.log(max + 1 === max + 2); // true 因为最大数字再加溢出了
    
  • 我们在使用 BigInt 的时候,必须添加 ESNext 的编译辅助库

  • 要使用1n需要 "target": "ESNext",否则报目标低于 ES2020 时,bigInt 文本不可用。

    const max = BigInt(Number.MAX_SAFE_INTEGER);
    console.log(max + 1n === max + 2n); // false
    
  • number 和 bigint 类型不一样,不兼容。JS 里的类型Number BigInt,ts里的类型 number bigint

    let foo: number;
    let bar: bigint;
    foo = bar;
    bar = foo;
    

11.包装对象 Wrapper Object

  • JavaScript 的类型分为两种:原始数据类型(Primitive data types)和对象类型(Object types)。
  • 所有的原始数据类型都没有属性(property)
  • 原始数据类型
    • 布尔值
    • 数值
    • 字符串
    • null
    • undefined
    • Symbol

如上所述,在 JavaScript 中字符串被视为原始数据类型而不是对象类型,但是我们来看下面一段代码。

let name = 'lili'; 
console.log(name.toUpperCase()); // LILI

这里很明显可以看出 name 有一个 toUpperCase 的属性(property)。那么是否上述结论不正确呢?如果 name 不是对象类型,为什么它具有 toUpperCase,toLowerCase 等属性呢?

先说一下结论:

当调用基本数据类型方法的时候,JavaScript 会在原始数据类型和对象类型之间做一个迅速的强制性切换

当我们试图访问 name 的 toUpperCase 属性时,JavaScript 会通过 new String(name) 来强制将字符串的值转换为一个对象类型,这个对象就是包装对象(wrapper object)。

它继承了 string 的所有方法,并且被用于获取其属性(property)上的引用(reference)。一旦属性被调用后,这个包装对象就会被废弃。

let name = (new String('lili')).toUpperCase();

11.1 TypeScript 中类型声明时需要关注的包装对象问题

let isOK: boolean = true; // 编译通过 
let isOK: boolean = Boolean(1) // 编译通过 

// 不能将类型“Boolean”分配给类型“boolean”。
// “boolean”是基元,但“Boolean”是包装器对象。如可能首选使用“boolean”。
let isOK: boolean = new Boolean(1); // 编译失败 

第三行代码会编译出错,这是为什么呢?有了上面提到的包装对象概念,相信大家也就能理解了,因为通过 new Boolean() 生成的值实际上是一个包装对象,并非原始数据类型。此处我们期望的 isOK 是一个原始数据类型,所以 TypeScript 会编译失败。

12.对象类型(Object Types)

除了基本类型之外,最常见的类型是对象类型。这指的是任何带有属性的 JavaScript 值,几乎全部都是属性!要定义对象类型,只需列出其属性及其类型。

比如这里有一个函数,它接受一个点状对象:

function printCoord(pt: { x: number; y: number }) {
    console.log("The coordinate's x value is " + pt.x);
    console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });

上面用一个具有两个属性(x 和 y)的类型对参数进行注释,这两个属性都是 number 类型。

12.1 可选属性

对象类型还可以指定其部分或全部属性是可选的,在属性后面加?

function printName(obj: { first: string; last?: string }) {
    // ...
}
// Both OK
printName({ first: "Bob" });
printName({ first: "Alice", last: "Alisson" });

如果你访问一个不存在的属性,你会得到一个undefined,而不是一个运行时错误。

function printName(obj: { first: string; last?: string }) {
    console.log(obj.last.toUpperCase());// Error - might crash if 'obj.last' wasn't provided!
    
    if (obj.last !== undefined) {
        console.log(obj.last.toUpperCase()); // OK
    }
}

13.联合类型(Union Types)

  • 联合类型是由两个或多个其他类型组成的类型,表示可能是这些类型中任何一个类型的值

    function printId(id: number | string) {
        console.log("Your ID is: " + id);
    }
    
    // OK
    printId(101);
    // OK
    printId("202");
    // Error
    printId({ myID: 22342 });
    
  • 未赋值时联合类型上只能访问两个类型共有的属性和方法

    下面的例子,联合类型是 number | string,那不能使用只能 string 使用的方法:

    function printId(id: number | string) {
        console.log(id.toUpperCase()); // 报错
        // Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'.
    }
    

    解决方法通过窄化(narrow)将类型缩小

    function printId(id: number | string) {
        if (typeof id === "string") {
            // In this branch, id is of type 'string'
            console.log(id.toUpperCase());
        } else {
            // Here, id is of type 'number'
            console.log(id);
        }
    }
    

14.字面类型(Literal Types)

const someStr = "abc" 
// 相当于: 
const someStr: "abc"
// someStr的类型是 "abc",它的值只能是abc

// 同理
const foo = 1
// 相当于:
const foo: 1
// foo 的类型是1(而不是整数)。

// 如果用typeof 操作符
typeof someStr // 'string'
typeof foo // 1

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

字面类型配和联合类型:

type Direction = 'Up' | 'Down' | 'Left' | 'Right'; 
function move(direction: Direction) {
    // ...
} 

move("Up"); // 这里只能传 'Up' | 'Down' | 'Left' | 'Right' 这4个值中的一个

14.1 可能遇到的问题

定义一个 handleRequest 接口请求的方法,注意这里method参数

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"'.

会发现上面 值传 req.method 会报错,这是因为: req.method 被推断为 string,而不是 “GET”。因为代码可以在 req 的创建和 handleRequest 的调用之间进行计算,handleRequest 可以为 req.method 分配一个新字符串,比如“GUESS”,所以 TypeScript 认为这段代码有错误。

解决方式:

  • 可以通过在任一位置添加类型断言来更改推断:
    // 1
    const req = { url: "https://example.com", method: "GET" as "GET" };
    
    // 2 
    handleRequest(req.url, req.method as "GET");
    
  • 可以使用 const 将整个对象转换为文本类型
    const req = { url: "https://example.com", method: "GET" } as const
    

15. 类型别名(Type Aliases)

类型别名的语法是用type关键字

type Point = {
    x: number;
    y: number;
};

function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

printCoord({ x: 100, y: 100 });

也可以命名联合类型:

type StrOrNum = string | number;

// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';

// 会检查类型
sample = true; // Error

四、类型推断

TypeScript 能根据一些简单的规则推断(检查)变量的类型

1.变量的类型,由定义推断:

let foo = 123; // foo 是 'number'
let bar = 'hello'; // bar 是 'string'

foo = bar; // Error: 不能将 'string' 赋值给 `number`

2.函数返回类型

返回类型能被 return 语句推断,如下所示,推断函数返回为一个数字

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

3.赋值

函数参数类型/返回值也能通过赋值来推断。如下所示,foo 的类型是 Adder,他能让 foo 的参数 ab 是 number 类型。

type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => a + b;

4. 结构化

这些简单的规则也适用于结构化的存在(对象字面量),例如在下面这种情况下 foo 的类型被推断为 { a: number, b: number }

const foo = {
  a: 123,
  b: 456
};

foo.a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

数组同理:

const bar = [1, 2, 3];
bar[0] = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

5.解构

const foo = {
  a: 123,
  b: 456
};
let { a } = foo;

a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

数组同理:

const bar = [1, 2];
let [a, b] = bar;

a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

6.noImplicitAny

选项 noImplicitAny 用来告诉编译器,当无法推断一个变量时发出一个错误(或者只能推断为一个隐式的 any 类型),你可以:

  • 通过显式添加 :any 的类型注解,来让它成为一个 any 类型;
  • 通过一些更正确的类型注解来帮助 TypeScript 推断类型。

五、类型断言(Type Assertions)

类型断言告诉编译器,我们自己确切的知道变量的类型,而不需要进行类型检查

比如,TypeScript 只知道这会返回某种类型的 HTMLElement,但是您可能知道您的页面将始终有一个带有给定 ID 的 HTMLCanvasElement

const myCanvas = 
    // HTMLElement
    document.getElementById("main_canvas") as HTMLCanvasElement;

但是有的类型断言TS会拒绝,比如:

const x = 'hello' as number

TS会报一个这样的错误:

Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

当然有时候你可以用any as T来“欺骗”TS,这个方式也是双重断言

const a = (expr as any) as T;

参考文章