TypeScript(二)

126 阅读14分钟

TypeScript(二)

本文摘自 公众号:深圳湾码农

一、联合类型

联合类型使用 |分隔,表示取值可以为多种类型中的一种。

let status: string | number;
status = 'this is str';
status = 1;

二、类型别名

    类型别名用来给一个类型起个新的名字。它只是起了一个新名字,并没有创建新类型。类型别名常用于联合类型。

type count = number | number[];
function hello(value: count) {};

三、交叉类型

交叉类型就是跟联合类型相反,用 & 操作符表示,交叉类型就是两个类型必须存在。

interface IPersonA {
    name: string,
    age: number
}


interface IPersonB {
    name: string,
    gender: string
}

let person: IPersonA & IPersonB = {
    name: 'jack',
    age: 23,
    gender: 'male'
};

上述代码中,person 即是 IPersonA 又是 IPersonB 类型。

注意:交叉类型取的多个类型的并集,但是如果 key 相同但是类型不同,则该 key

为 never 类型。

interface IPersonA {
    name: string
}

interface IPersonB {
    name: number
}


function testAndFn(params: IPersonA & IPersonB) {
    console.log(params);
}

// error TS2322: Type 'string' is not assignable to type 'never'.
testAndFn(name: 'this is string')

四、类型守卫

    类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。 换句话说,类型保护可以保证一个字符串是一个字符串,尽管它的值也可以是一个数值。类型保护与特性检测并不是完全不同,其主要思想是尝试检测属性、方法或原型,以确定如何处理值。

换句话说:类型守卫是运行时检查,确保一个值在所要类型的范围内。

目前主要有4种方式来实现类型保护:

1、in关键字

2、typeof关键字

3、instanceof

4、自定义类型保护的类型谓词

1、in关键字

interface InObj1 {
    a: number,
    x: string
}

interface InObj2 {
    a: number,
    y: string
}

function isIn(arg: InObj1  | InObj2) {
     // x 在 arg 打印 x
    if('x' in arg) {
        console.log('x');
    }

     // x 在 arg 打印 y
     if('y' in arg) {
        console.log('y');
    }
}

isIn({a: 1, x: 'xxx'})
isIn({a: 1, y: 'yyy'})

2、typeof关键字

function isTypeOf(val: string | number) {
  if(typeof val === 'number') return 'number';
  if(typeof val === 'string') return 'string';
  return '啥也不是';
}

console.log(isTypeOf(1));
console.log(isTypeOf('str'));
// 防止非严格模式下  传入null 和 undefined
console.log(isTypeOf(null));(null));

typeof只支持:typeof 'x' === 'typeName' 和 typeof 'x' !== 'typeName',x必须是 'number','string','boolean','symbol'。

3、instanceof

function createDate(date: Date | string) {
  console.log(date);

  if (date instanceof Date) {
    date.getDate();
  } else {
    return new Date(date);
  }
}
// 2022-12-19T04:21:32.000Z
console.log(createDate('2022-12-19 12:21:32'));

// 2022-12-19T14:58:19.456Z
createDate(new Date());:58:19.456Z

4、自定义类型保护的类型谓词

function isNumber(num: any): num is number {
  return typeof num === 'number'
}

function isString(str: any): str is string {
  return typeof str === 'string'
}

五、接口

    我们使用接口来定义对象的类型。接口是对象的状态(属性)和行为(方法)的抽象(描述)。

简单的理解就是:为我们的代码提供一种约定

我们使用关键字 interface来声明接口。

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


let tom: Person = {
    name: 'Tom',
    age: 25
}

    我们定义了一个接口Person,接着定义了一个变量 tom,它的类型是 Person。这样,我们就约束了 tom 的形状必须和接口 Person 一致。

    接口一般首字母大写。(当然挺多人也习惯 I 大写字母开头,用来表示这是一个接口)

设置接口可选 | 只读

interface IPerson {
    readonly name: string;
    age?: number;
}
  • 可选属性,我们最常见的使用情况是,不确定这个参数是否会传,或者存在。
  • 只读属性用于限制只能在对象刚刚创建的时候修改其值。此外 TypeScript 还提供了 ReadonlyArray 类型,它与 Array 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。

索引签名

    有时候我们希望一个接口中除了包含必选和可选属性之外,还允许有其他的任意属性,这时我们可以使用 索引签名 的形式来满足上述要求。

需要注意的是,一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集。

interface IPerson {
    name: string;
    age?: number;
    [prop: string]: any;    // prop字段必须是 string类型 or number类型。值是any类型,也就是任意的
}

const person1 = {
    name: 'jack',
    age: 23
}

const person2 = {
    name: 'rose'
}

const person3 = {
    name: 'jack',
    sex: 'male'
}

我们规定以 string 类型的值来索引,索引到的是一个 any 类型的值。

接口与类型别名的区别

    实际上,在大多数的情况下使用接口类型和类型别名的效果等价,但是在某些特定的场景下这两者还是存在很大区别的。

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

type(类型别名)会给一个类型起个新名字。type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。给基本类型起别名通常没什么用,尽管可以作为文档的一种形式使用。

接口和类型别名都可以用来描述对象或函数的类型,只是语法不同

type MyType = {
    name: string;
    say(): void;
}

interface MyInterface {
    name: string;
    say(): void;
}

都允许扩展

  • interface 用 extends 来实现扩展
interface MyInterface {
    name: string;
    say(): void;
}

interface MyInterface2 extends MyInterface {
    sex: string;
}

let person: MyInterface2 = {
    name: 'jack',
    sex: 'male',
    say() {}
}
  • type 使用 & 实现扩展
type MyType =  {
    name: string;
    say(): void;
}

type MyType2 = MyType & {
    sex: string
}

let person: MyType2 = {
    name: 'jack',
    sex: 'male',
    say() {}
}

不同点

  • type 可以声明基本数据类型别名 / 联合类型 / 元组等,而 interface 不行
// 基本类型别名
type UserName = string;
type Username = string | number;

type Pig = {};
type Dog = {};
type Cat = {};

// 联合类型
type Animal = Pig | Dog | Cat;

type List = [string, number, boolean];
  • interface 能够合并声明,而 type 不行
interface Person {
    name: string;
}

interface Person {
    age: number;
}

let person: Person = {
    name: 'Jack',
    age: 25
}

六、泛型

    泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而是使用的时候再指定类型的一种特性。

    举个例子:我们现在有个这样的需求,我们要实现一个这样的函数,函数的参数可以是任何值,返回值就是将参数原样返回,并且参数的类型是 string,函数返回类型就为 string ?

你很容易写下:

function getValue(arg: string): string {
  return arg;
}

现在需求有变,需要返回一个 number 类型的值,你会说,联合类型就完事了:

function getValue2(arg: string | number): string | number{
  return arg;
}

但是这样又有一个问题,就是如果我们需要返回一个 boolean 类型,string 数组甚至任意类型呢,难道有多少个就写多少个联合类型?

是的,我们直接用 any 就行了!

function getValue3(arg: any): any{
  return arg;
}

尽管 any 大法好,很多时候 any 也确实能够解决不少问题,但是这样也不符合我们的需求了,传入和返回都是 any 类型,传入和返回并没有统一

那么泛型出场: >>>

1、基本使用

解决上面问题:

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

// 或

function getValue4<HAHA>(arg: HAHA): HAHA{
  return arg;
}

泛型的语法是尖括号 <> 里面写类型参数,一般用 T 来表示第一个类型变量名称,其实它可以用任何有效名称来代替,比如我们用NIUBI也是编译正常的

泛型就像一个占位符一个变量,在使用的时候我们可以将定义好的类型像参数一样传入,原封不动的输出。

2、使用

我们有两种方式来使用:

  • 定义要使用的类型
getValue<string>('jack');    // 定义T 为 string 类型
  • 利用 typescript 的类型推断
getValue('jack');    // 自动推导类型为 string

3、多个参数

    其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U。

function getValue<T, U>(arg: [T, U]): [T, U] {
  return arg;
}

//使用
//鼠标放到方法上:ts给我们自动推断出输入、返回的类型
// function getValue<string, number>(arg: [string, number]): [string, number]
const result = getValue(['jack', 25]);
console.log(result); // [ 'jack', 25 ]
console.log(...result); // jack 25

4、泛型约束

    在函数内部使用泛型变量的时候,由于事先不知道它是哪种类型,所以不能随意的操作它的属性或方式:

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

因为泛型 T 不一定包含属性 length,那么我想 getLength 这个函数只允许传入包含 length 属性的变量,该怎么做呢?

这时我们可以使用 extends关键字来对泛型进行约束

interface Lengthwise {
  length: number;
}

function getLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);

  return arg;
}

getLength('abcd')
getLength([1,2,3])
getLength({ length: 5 })

这里可以看出,不管你是 string、array、object,只要具有length属性,都可以

5、泛型接口

在定义接口的时候指定泛型。

interface KeyValue<T, U> {
  key: T;
  value: U;
}

const person1: KeyValue<string, number> = {
  key: "jack",
  value: 25,
};

const person2: KeyValue<number, string> = {
  key: 25,
  value: "rose",
};

6、泛型类

class Test<T> {
  value: T;  //Property 'value' has no initializer and is not definitely assigned in the constructor.
  add: (x: T, y: T) => T;
}

let myTest = new Test<number>();
myTest.value = 0;
myTest.add = function (x, y) {
  return x + y;
};

console.log(myTest.add(1, 2));

在严格模式下,这种写法会报错的!!!

改进一下:

interface ITest<U> {
  value: U;
  add: () => U;
}

class Test<T> implements ITest<T>{
  value: T;
  constructor(value: T) {
    this.value = value;
  }

  add(): T {
    return this.value
  }
}

let testClass = new Test<number>(25);
console.log(testClass.add());

7、泛型类型别名

type Cart<T> = { list: T[] } | T[];

let c1: Cart<string> = { list: ["1"] };

let c2: Cart<number> = [1];

8、泛型参数的默认类型

    我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际参数中也无法推测出时,这个默认类型就会其作用。优点 js 里函数默认参数的意思。

function createArray<T = string>(length: number, value: T): Array<T> {
  let result: T[] = [];
  for (let i = 0; i < length; i++) {
    result[i] = value;
  }
  return result;
}

console.log(createArray(5, undefined));

9、泛型工具类型

1、typeof

2、keyof

3、in

4、infer

5、extends

6、索引访问操作符

  • typeof

    关键词除了做类型保护,还可以从实现退出类型

    // 先定义变量
    let p1 = {
      name: "jack",
      age: 18,
      gender: "male",
    };
    
    type People = typeof p1;
    
    function getName(p: People): string {
      return p.name;
    }
    
    console.log(getName(p1));
    
  • keyof

可以用来获取一个对象接口中的所有 key 值。

interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}

// type PersonKey = "name" | "age" | "gender"
type PersonKey = keyof Person;

function getValueByKey(p: Person, key: PersonKey): unknown {
  return p[key];
}

let person: Person = {
  name: "jack",
  age: 18,
  gender: "male",
};

let result = getValueByKey(person, "name");
console.log(result);
let result2 = getValueByKey(person, "age");
console.log(result2);
let result3 = getValueByKey(person, "gender");
console.log(result3);
  • in

用来遍历枚举类型

type Keys = "a" | "b" | "c";
type Obj = {
  [p in Keys]: any;
};

let res: Obj = {
  a: 1,
  b: '1',
  c: true
}
console.log(res);
  • infer

在条件类型语句中,可以用 infer 声明一个类型变量并且对它进行使用。

type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

    infer R 就是声明一个变量来承载传入参数签名的返回值类型,简单说就是用它取到函数返回值的类型方便之后使用。

  • extends

    有时候我们定义的泛型不想过于灵活或者想继承某些类等,可以通过 extends 关键字添加泛型约束。

interface Lengthwise {
  length: number;
}

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

现在这个泛型函数定义了约束,因此它不再适用于任意类型:

// Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
// Error, number doesn't have a .length property
// loggingIdentity(234)

loggingIdentity("123");    //编译正确

当我们传入合法的类型的值,即包含 length 属性的值时:

loggingIdentity({length: 10, name: 'jack'}); // 编译正确
  • 索引访问操作符

使用 []操作符可以进行索引访问:

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

type x = Person["name"]; // x 是 string

10、内置工具类型

1、Required

将类型的属性变成必选。

interface IPerson {
  fullName?: string;
  realAge?: number;
  hobby?: Array<string>; // hobby?: string[];
}

const person: Required<IPerson> = {
  fullName: "jack",
  realAge: 25,
  hobby: ["coding"],
};

console.log(person);  // { fullName: 'jack', realAge: 25, hobby: [ 'coding' ] }
2、Partial

与 Required 相反,将所有属性转换为可选属性。

interface IPerson {
  fullName: string;
  realAge: number;
  hobby: Array<string>; // hobby?: string[];
}

const person: Partial<IPerson> = {
  // fullName: "jack",
  // realAge: 25,
  hobby: ["coding"],
};
3、Exclude

将某个类型中属于另一个的类型移除掉,剩余的属性构成新的类型。

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
4、Extract

和 Exclude 相反,Extract 从  T 中提取出 U。

type T0 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () =>void

适用于并集类型。

5、Readonly

把数组或对象的所有属性值转换为只读的,这就意味着这些属性不能被重新赋值。

interface IPerson {
  fullName: string;
  realAge: number;
  gender?: "male" | "female";
}

let person: Readonly<IPerson> = {
  fullName: "rose",
  realAge: 25,
  gender: 'male'
};

// Cannot assign to 'gender' because it is a read-only property.
person.gender = "female";
6、Record

Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型。

type Property = "key1" | "key2";
type Persons = Record<Property, string>;

const per: Persons = {
  key1: "123",
  key2: "234",
};

console.log(per);
7、Pick

从某个类型中挑出一些属性出来。

type PersonType = {
  name: string;
  age: number;
  gender: string;
};

type p1 = Pick<PersonType, "name" | "age">;

const user1: p1 = {
  name: "jack",
  age: 25,
};
8、Omit

与Pick相反,Omit 从T中取出除去K的其他所有属性。

type PersonType = {
  name: string;
  age: number;
  gender: string;
};

type p1 = Omit<PersonType, "name" | "age">;

const user1: p1 = {
  gender: 'male'
};
9、NonNullable

去除类型中的 null 和 undefined

type P1 = NonNullable<string | number | undefined>; // string | number
type P2 = NonNullable<string[] | null | undefined>; // string[]
10、ReturnType

用来得到一个函数的返回值类型。

type Func = (value: string) => string

const testValue: ReturnType<Func> = 'str'
console.log(testValue);
11、Parameters

用于获得函数的参数类型所组成的元组类型。

type P1 = Parameters<(a: number, b: string) => void>; // [number, string]
12、InstanceType

返回构造函数类型T的实例类型。

class C {
  x = 0;
  y = 0;
}

type D = InstanceType<typeof C>;  // C

七、tsconfig.json

tsconfig.json 是 TypeScript 项目的配置文件。

tsconfig.json 包含 TypeScript 编译的相关配置,通过更改编译配置项,我们可以让 TypeScript 编译出 ES6、ES5、node 的代码。

重要字段

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。

compilerOptions 选项

{
  "compilerOptions": {

    /* 基本选项 */
    "target": "es5",                       // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
    "module": "commonjs",                  // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
    "lib": [],                             // 指定要包含在编译中的库文件
    "allowJs": true,                       // 允许编译 javascript 文件
    "checkJs": true,                       // 报告 javascript 文件中的错误
    "jsx": "preserve",                     // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
    "declaration": true,                   // 生成相应的 '.d.ts' 文件
    "sourceMap": true,                     // 生成相应的 '.map' 文件
    "outFile": "./",                       // 将输出文件合并为一个文件
    "outDir": "./",                        // 指定输出目录
    "rootDir": "./",                       // 用来控制输出目录结构 --outDir.
    "removeComments": true,                // 删除编译后的所有的注释
    "noEmit": true,                        // 不生成输出文件
    "importHelpers": true,                 // 从 tslib 导入辅助工具函数
    "isolatedModules": true,               // 将每个文件做为单独的模块 (与 'ts.transpileModule' 类似).

    /* 严格的类型检查选项 */
    "strict": true,                        // 启用所有严格类型检查选项
    "noImplicitAny": true,                 // 在表达式和声明上有隐含的 any类型时报错
    "strictNullChecks": true,              // 启用严格的 null 检查
    "noImplicitThis": true,                // 当 this 表达式值为 any 类型的时候,生成一个错误
    "alwaysStrict": true,                  // 以严格模式检查每个模块,并在每个文件里加入 'use strict'

    /* 额外的检查 */
    "noUnusedLocals": true,                // 有未使用的变量时,抛出错误
    "noUnusedParameters": true,            // 有未使用的参数时,抛出错误
    "noImplicitReturns": true,             // 并不是所有函数里的代码都有返回值时,抛出错误
    "noFallthroughCasesInSwitch": true,    // 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)

    /* 模块解析选项 */
    "moduleResolution": "node",            // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
    "baseUrl": "./",                       // 用于解析非相对模块名称的基目录
    "paths": {},                           // 模块名到基于 baseUrl 的路径映射的列表
    "rootDirs": [],                        // 根文件夹列表,其组合内容表示项目运行时的结构内容
    "typeRoots": [],                       // 包含类型声明的文件列表
    "types": [],                           // 需要包含的类型声明文件名列表
    "allowSyntheticDefaultImports": true,  // 允许从没有设置默认导出的模块中默认导入。

    /* Source Map Options */
    "sourceRoot": "./",                    // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
    "mapRoot": "./",                       // 指定调试器应该找到映射文件而不是生成文件的位置
    "inlineSourceMap": true,               // 生成单个 soucemaps 文件,而不是将 sourcemaps 生成不同的文件
    "inlineSources": true,                 // 将代码与 sourcemaps 生成到一个文件中,要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

    /* 其他选项 */
    "experimentalDecorators": true,        // 启用装饰器
    "emitDecoratorMetadata": true          // 为装饰器提供元数据的支持
  }
}