Typescript类型记录(一)

103 阅读15分钟

基础类型

1.1 String类型

let name: string = 'name';

1.2 Number类型

let age: number = 18;

1.3 Boolean类型

let show: boolean = false;

1.4 Array类型

let list: number[] = [1,2,3];
//or
let list: Array<number> = [1,2,3];
//多维数组
let list: number[][] = [[1,2],[2,3]];
//任意类型数组
let list: any[] = [];
//复合类型数组
let list: (string|number)[] = ['1',2,3];

1.5 Tuple 类型

数组一般由同种类型的值组成,如果需要存储不同的类型,可以使用元组。 元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 stringnumber类型的元组。

let x: [string, number];
// 初始化
x = ['hello', 10]; // OK
//初始化的时候,类型不匹配的会报错
x = [10, 'hello']; // Error
//初始化的时候,必须提供每个属性的值
x = ['hello']; // Error

可以通过索引来访问元组的值:

console.log(x[0]); //hello

当访问一个越界的元素,会使用联合类型替代:

x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型
x[5] = false; //Error, boolean不属于(string | number)类型

在定义元组时,可以将某些元素定义为可选元素。定义元组可选元素的语法是在元素类型之后添加一个问号“?”

let x:[string,number?]
x = ['hello']; //ok 后面的number可选

在定义元组类型时,可以将最后一个元素定义为剩余元素,那么该元组的元素数量是开放的,它可以包含零个或多个指定类型的剩余元素。

let x:[string,...number[]]
//下面的都ok,后面的number类型元素可以是0到多个
x = ['hello']; 
x = ['hello',1,2,3]

当元组中包含了可选元素时,元组的长度不再是一个固定值。编译器能够根据元组可选元素的数量识别出元组所有可能的长度,进而构造出一个由数字字面量类型构成的联合类型来表示元组的长度。

若元组类型中定义了剩余元素,那么该元组拥有不定数量的元素。因此,该元组length属性的类型将放宽为number类型。

1.6 枚举类型

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字,TypeScript 支持数字的和基于字符串的枚举。

enum DateType {
  Year,
  Month,
  Day,
}

let date: DateType = DateType.Year;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。在 TypeScript 2.4 版本,允许我们使用字符串枚举。在一个字符串枚举里,每个成员都必须用字符串字面量,或另外一个字符串枚举成员进行初始化。

enum DateType {
  Year = 'year',
  Month  = 'month',
  Day = 'day',
}

除了纯数字和字符,还能是两者的混合,这称为异构枚举。

1.7 Any 类型

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量,在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。

1.8 Void 类型

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function sayHello(): void {
    console.log('hello');
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefinednull:

let unusable:void = undefined;

1.9 Unknown 类型

any相似,任何类型也可以赋值给unknown,但是unknown不能调用属性和方法,需要借助断言来调用:

let name: unknown = 'hello';
name.length;//error
(name as string).length; //ok

另外,如果联合类型有unknown,那么最终的类型都是unknown

type Name = unknown | string; //unknown

1.10 Undefined 和 Null 类型

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和 void相似,它们的本身的类型用处不是很大,默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

然而,当你指定了--strictNullChecks标记,nullundefined只能赋值给void和它们各自。 这能避免 很多常见的问题。

1.11 Never 类型

never类型表示的是那些永不存在的值的类型。 例如, never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never类型,当它们被永不为真的类型保护所约束时。

never类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外)。 即使 any也不可以赋值给never

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

1.12 对象 类型

对象类型有三种基本类型:Object类型(首字母为大写字母O);object类型(首字母为小写字母o);对象类型字面量。

1.12.1 Object类型

Object类型是特殊对象“Object.prototype”的类型,该类型的主要作用是描述JavaScript中几乎所有对象都共享(通过原型继承)的属性和方法。

Object类型有一个特点,那就是除了undefined值和null值外,其他任何值都可以赋值给Object类型。

1.12.2 object类型

object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。

object仅能够赋值给以下三种类型:

  • 顶端类型any和unknown
  • Object类型
  • 空对象类型字面量“{}”

对象类型字面量

对象类型字面量的语法与对象字面量的语法相似。在定义对象类型字面量时,需要将类型成员依次列出。

let point: {x:number;y:number} = {x:1,y:1}

在各个类型成员之间,不但可以使用分号;进行分隔,还可以使用逗号,进行分隔,这两种分隔符不存在功能上的差异。同时可以通过定义一个可选属性, 通过添加readonly修饰符能够定义对象只读属性。

1.13 Symbols 类型

let sym = Symbol(); 
let obj = { [sym]: "value" };
console.log(obj[sym]); // "value"

类型进阶

2.1 交叉类型

交叉类型是将多个类型合并为一个类型。 通过 & 运算符,我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。

type Year = {year:number};
type Month = {month:number};
type Date = Year & Month;

let date:Date = {
    year:2023,
    month:1
}
2.1.1 同名基础类型成员的合并

如果合并得多个类型中,存在相同的成员,同时它们又不是同一个基础类型,那么它们的类型就会合并成为 never类型:

interface A {
    a: number;
    c: number;
}
interface B {
    b: number;
    c: string;
}

type C = A & B;

let c:C = {a:1,b:2,c:3}; // error c不能同时是string & number类型,所以c是never类型

2.2 联合类型

联合类型由一组有序的成员类型构成,各成员类型之间使用竖线符号“|”分隔。联合类型表示一个值的类型可以为若干种类型之一。

联合类型的成员类型可以为任意类型,如原始类型、数组类型、对象类型,以及函数类型等。

const T = number | string[] | {x:number} | (()=>void);

2.2.1 联合类型的类型成员

若联合类型U中的每个成员类型都包含一个同名的属性签名M,那么联合类型U也包含属性签名M。

interface A {
    name: string;
    area: string;
}

interface B {
    name: number;
    address: string;
}

type C = A | B;

declare const c: C;
c.name; //string | number;
//area,address不属于类型A,B的共同属性
c.area; //错误
c.address; //错误

2.3 交叉类型与联合类型

当表示交叉类型的&符号与表示联合类型的|符号同时使用时,&符号具有更高的优先级。

A & B | C & D
//等同于
(A & B) | (C & D)

由交叉类型和联合类型组成的类型满足类似于数学中乘法分配律的规则。表示交叉类型的&符号如同数学中的乘法符号×,而表示联合类型的|符号则如同数学中的加法符号+

A & (B | C)
//等同于
(A & B) | (A & C)

const T = string & (number | 'a')
        = (string & number) | (string & 'a')
        = never | 'a'

2.4 索引类型

对于一个对象类型而言,我们可以使用属性名作为索引来访问属性类型成员的类型。

2.4.1 索引类型查询

通过索引类型查询能够获取给定类型中的属性名类型,语法如下:keyof Type

interface Point {
    x: number;
    y:number;
}
type T = keyof Point; // 'x'|'y' 这个就是类型Point属性名组成的联合类型

虽然在对象类型上使用索引类型查询更有意义,但是索引类型查询也允许在非对象类型上使用,如:


type T = keyof any; // 固定为 string | number | symbol

type T = keyof unknown; //never

//当对原始类型使用索引类型查询时,先查找与原始类型对应的内置对象类型,然后再进行索引类型查询。
type T = keyof boolean; // 'valueOf'

// 联合类型
type A = {a: string; c: number};
type B = {b:string,c:number};

type T = keyof (A | B); //'c',因为(A|B) = {c:number}

//交叉类型
type T = keyof (A & B); // 'a'|'b'|'c' 

2.5 映射对象类型

映射对象类型是一种独特的对象类型,它能够将已有的对象类型映射为新的对象类型。例如,我们想要将已有对象类型T中的所有属性修改为可选属性,那么我们可以直接修改对象类型T的类型声明,将每个属性都修改为可选属性。除此之外,更好的方法是使用映射对象类型将原对象类型T映射为一个新的对象类型T′,同时在映射过程中将每个属性修改为可选属性。

2.5.1 映射对象类型声明

映射对象类型是一个类型运算符,它能够遍历联合类型并以该联合类型的类型成员作为属性名类型来构造一个对象类型。

{ readonly [P in K]? : T }

在该语法中,readonly是关键字,表示该属性是否为只读属性,该关键字是可选的;?修饰符表示该属性是否为可选属性,该修饰符是可选的;in是遍历语法的关键字;K表示要遍历的类型,由于遍历的结果类型将作为对象属性名类型,因此类型K必须能够赋值给联合类型string | number | symbol,因为只有这些类型的值才能作为对象的键;P是类型变量,代表每次遍历出来的成员类型;T是任意类型,表示对象属性的类型,并且在类型T中允许使用类型变量P。如下:

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

type MappedObjectType = { [P in keyof Point]?: number };
// MappedObjectType = {x?:number;y?:number}

若当前遍历出来的类型成员P为 string/number 类型,则在结果对象类型中创建 字符串/数值 索引签名。


type MappedObjectType = { [P in string]?: string }; // {[x: string]: string}

type MappedObjectType = { [P in number]?: string }; // {[x: number]: string}

2.5.2 映射对象类型应用

通常将映射对象类型索引类型查询以及索引访问类型三者结合才能够最大限度地体现映射对象类型的威力。比如Typescript内置的Partial<T>,下面看看它的实现:

// 将T中的所有属性标记成可选属性
type Partial<T> = {
    [P in keyof T]?: T[P];
}

2.5.3 同态映射对象类型

如果映射对象类型中存在索引类型查询,那么TypeScript编译器会将该映射对象类型视为同态映射对象类型,语法如下:

{readonly [P in keyof T]?: X}

其中readonly关键字和?修饰符均为可选的。

type Point = {x:number};
type K = keyof Point;

// 同态映射对象类型
type HMOT = {[P in keyof Point]: Point[P]}

//非同态映射对象类型
type MOT = {[P in K]: Point[P]}

同态映射对象类型的一个重要性质是,新的对象类型会默认拷贝源对象类型中所有属性的readonly修饰符和?修饰符。如果想删除或者添加修饰符,可以使用+/-修饰符,可以精确控制添加或移除映射属性的?修饰符和readonly修饰符。

{ -readonly [P in keyof T]-?: T[P] } 
{ +readonly [P in keyof T]+?: T[P] }

TypeScript内置的工具类型Required<T>就是使用了-删除属性的可选修饰符:

 type Required<T> = { [P in keyof T]-?: T[P] };

条件类型

条件类型的定义借用了JavaScript语言中的条件运算符,语法如下所示:

T extends U ? X : Y

在该语法中,extends是关键字;T、U、X和Y均表示一种类型。若类型T能够赋值给类型U,则条件类型的结果为类型X,否则条件类型的结果为类型Y。条件类型的结果类型只可能为类型X或者类型Y

在实际应用中,条件类型通常与类型参数结合使用。例如:

type TypeName<T> = T extends string ? 'string'
                    : T extends number ? 'number'
                    : T extends boolean ? 'boolean'
                    : T extends undefined ? 'undefined'
                    : T extends Function ? 'function',
                    : 'object';
 type T0 = TypeName<'a'>; // 'string'
 type T1 = TypeName<true>; // 'boolean'
 type T2 = TypeName<0>; // 'number'
 type T3 = TypeName<undefined>; //'undefined'

如果类型参数T不是复合类型的组成部分而是独立出现,那么该类型参数称作裸类型参数,这个时候条件类型就可以展开,称为分布式条件类型

T = A|B

T extends U ? X : Y
= (A extends U ? X : Y) | (B extends U ? X : Y)

利用这个特性,可以巧妙地过滤联合类型。比如TypeScript内置的Exclude<T, U>,可以从联合类型T中删除符合条件的类型:

type Exclude<T, U> = T extends U ? never : T;

在分布式条件类型Exclude<T, U>中,若类型T能够赋值给类型U,则返回never类型;否则,返回类型T。这里巧妙地使用了never类型来从联合类型T中删除符合条件的类型。

never类型是尾端类型,它是任何其他类型的子类型。因此,当never类型与其他类型组成联合类型时,可以直接将never类型从联合类型中“消掉”

常用内置类型工具

Partial

Partial<T>能够构造一个新类型,并将实际类型参数T中的所有属性变为可选属性

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

type T = Partial<Point>; // {x?:number;y?:number};

Required

Required<T>能够构造一个新类型,并将实际类型参数T中的所有属性变为必选属性

interface Point {
    x?: number;
    y: number;
}

type T = Required<Point>; // {x:number;y:number};

Readonly

Readonly<T>能够构造一个新类型,并将实际类型参数T中的所有属性变为只读属性

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

type T = Readonly<Point>; // {readonly x:number;readonly y:number};

Record

Record<K, T>能够使用给定的对象属性名类型和对象属性类型创建一个新的对象类型,类型参数K提供了对象属性名联合类型,类型参数T提供了对象属性的类型。

type K = 'x'|'y';
type T = number;
type Point = Record<K,T>; // {x:number;y:number}

注意:因为类型参数K是用作对象属性名类型的,所以实际类型参数K必须能够赋值给“string | number | symbol”类型,只有这些类型能够作为对象属性名类型。

Pick

Pick<T, K>能够从已有对象类型中选取给定的属性及其类型,然后构建出一个新的对象类型,类型参数T表示源对象类型,类型参数K提供了待选取的属性名类型,它必须为对象类型T中存在的属性

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

type T = Pick<Point,'x'>; // {x:number}

Omit

Omit<T,K>能够从已有对象类型中剔除给定的属性,然后构建出一个新的对象类型,类型参数T表示源对象类型,类型参数K提供了待剔除的属性名类型,但它可以为对象类型T中不存在的属性

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

type T = Omit<Point,'x'>; // {y:number}

Exclude

Exclude<T, U>能够从类型T中剔除所有可以赋值给类型U的类型。

type T = Exclude<'x'|'y'|'z','x'|undefined>; //'y'|'z'

Extract

Extract<T, U>能够从类型T中获取所有可以赋值给类型U的类型。

type T0 = Extract<'x'|'y'|'z','x'|undefined>; //'x'
type T1 = Extract<string|(()=>void),Function>; //()=>void
type T2 = Extract<string|number,boolean>; //never

NonNullable

NonNullable<T>能够从类型T中剔除null类型和undefined类型并构造一个新类型,也就是获取类型T中的非空类型

type T = NonNullable<string|number|undefined|null>; //string|number

Parameters

Parameters<T>能够获取函数类型T的参数类型并使用参数类型构造一个元组类型。

type T = Parameters<(s:string)=>void>; //[string]

ConstructorParameters

ConstructorParameters<T>能够获取构造函数T中的参数类型,并使用参数类型构造一个元组类型。若类型T不是函数类型,则返回never类型。

type T = ConstructorParameters<new (x:string,y:number)=>object>; //[string,number]

ReturnType

ReturnType<T>能够获取函数类型T的返回值类型。

type T = ReturnType<(s:string)=>void>; //void