likes
comments
collection
share

TypeScript从0到1的学习之路(都这个年头了,你确定不学习ts卷一下吗?)8500字分享

作者站长头像
站长
· 阅读数 13

今天是2022年9月19日,也是我开始计划写这篇文章的日子。为什么要写这篇文章呢,其实在我去年下半年的时候,专门学过ts,但是实际工作中,用ts的地方其实用的很浅。平常工作也就定义个types啊,或者枚举enum啊。什么泛型之类的,工作中从来没有用过。以至于当我在看别人的工具包的时候,人家定义的函数参数类型,我有一些看不明白,所以才专门重新学了一遍ts,并且写一篇博客记录一下,这样当我往后忘记的时候,就只需要再看一遍自己的博客就好了。我也推荐大家用写博客的方式,来记录自己的学习之路。看 -> 做 -> 教 -> 是学习的三大阶段,你只看别人怎么写,基本是学不会的。如果边看,边做。可能当时记住了,但过段时间不用你也会忘,只有当你学会了,并且教会别人,时常巩固,才能收益最大化。

TypeScript是什么

TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。

简而言之,TypeScript是JavaScript的超集,具有可选的类型并可以编译为纯JavaScript。从技术上讲TypeScript就是具有静态类型的 JavaScript 。

TypeScript的优缺点

刚接触TypeScript的小伙伴可能会有点疑惑,为什么要使用typescript,我直接用js不香吗。其实我刚开始接触typescript也是这么想的。明明用js,半天就能搞定的事,用了ts,工作量反而要翻一倍甚至不止。如果你要写出漂亮的ts代码,人力成本肯定要大大增加的,这也是一些小型创业公司不用ts的原因,烧钱啊。但是,不知道大家有没有碰到过,一个用了ts的项目,和没用ts的项目,当你有一天去维护它的时候,用ts的项目维护起来简直如鱼得水,也不会犯因为类型错误而报的错,而反观没用ts的,你去用一些函数的时候,都不知道它接受几个参数,也不知道传什么类型。所以,学习ts是非常有必要的(ps:主要是不学ts,以后容易被淘汰啊,我也不想学啊)

优点

  • 增强代码的可维护性,尤其在大型项目的时候效果显著
  • 友好地在编辑器里提示错误,编译阶段就能检查类型发现大部分错误
  • 支持最新的JavaScript新特特性
  • 周边生态繁荣,vue3已全面支持 typescript

缺点

  • 需要一定的学习成本
  • 和一些插件库的兼容并不是特别完美,如以前在 vue2 项目里使用 typescript就并不是那么顺畅
  • 增加前期开发的成本(但是便于后期的维护)

TypeScript起步使用

安装TypeScript

有两种主要的方式来获取TypeScript工具:

  • 通过npm(Node.js包管理器)
  • 安装Visual Studio的TypeScript插件

Visual Studio 2017和Visual Studio 2015 Update 3默认包含了TypeScript。 如果你的Visual Studio还没有安装TypeScript,你可以下载它。

针对使用npm的用户:

> npm install -g typescript

构建你的第一个TypeScript文件

在编辑器,将下面的代码输入到greeter.ts文件里:

function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

编译代码

我们使用了.ts扩展名,但是这段代码仅仅是JavaScript而已。 你可以直接从现有的JavaScript应用里复制/粘贴这段代码。

在命令行上,运行TypeScript编译器:

tsc greeter.ts

输出结果为一个greeter.js文件,它包含了和输入文件中相同的JavsScript代码。 一切准备就绪,我们可以运行这个使用TypeScript写的JavaScript应用了!

安装ts-node

当然,如果只用tsc这个命令,每次都会生成一个新的js文件,很麻烦。所以我们来安装ts-node。这样每次运行都只需要运行一次命令就好 安装

npm i ts-node -g

运行

ts-node 文件名

有了ts-node,我们就能很方便的调试ts文件。一切准备就绪,让我们开始学习吧

TypeScript的基础类型

string

JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string表示文本数据类型。 和JavaScript一样,可以使用双引号( ")或单引号(')表示字符串。

如果给的类型不对,会直接报错,代码编辑器会显示出来。例如(后面我不会贴图了,知道编辑器会报错就行)

const str: string = 1234; // false 定义str为string类型 如果给别的类型会直接报错
let str1: string = '1234'; // true
str1 = '2345'; // true
str1 = 1234; // false

TypeScript从0到1的学习之路(都这个年头了,你确定不学习ts卷一下吗?)8500字分享

number

和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。

const num: number = '1234'; // false 不能将string分配给number
let num1: number = 1234; // true
num1 = '2345'; // false 不能将string分配给number
num1 = 1234; // true

boolean

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

const boo: boolean = '1234'; // false 不能将string分配给boolean
let boo1: boolean = true; // true
boo1 = '1234'; // false 不能将string分配给boolean
boo1 = false; // true

enum

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

enum Color {Red, Green, Blue}
let c: Color = Color.Green; // 1

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号,如果后面成员没设置编号,自动从你上一个设置的编号开始排序:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green; // 2

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // 2

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];

console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

array

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let list1: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>

let list2: Array<number> = [1, 2, 3];

tuple元祖

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。听上去可能高大尚,其他它只是在数组类型上,知道了一个数字的长度,所以给数组的每个元素都定义类型。 比如,你可以定义一对值分别为 stringnumber类型的元组。

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error

当访问一个已知索引的元素,会得到正确的类型:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

当访问一个越界的元素,会报错:

x[3] = 'world';  //false 不能将“world”分配给undefined

any

有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。我们不知道怎么定义类型合适的时候,可以先写一个any,代表任意类型

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

在对现有代码进行改写的时候,any类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object有相似的作用,就像它在其它语言中那样。 但是 Object类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];

list[1] = 100;

(any虽然可以阻止报错,但在我们知道类型的时候,一定要及时改过来,虽然any大法确实香,哈哈哈哈)

void

在 TS 中,void 和 undefined 功能高度类似,可以在逻辑上避免不小心使用了空指针导致的错误。

function foo() {}  	// 这个空函数没有返回任何值,返回类型缺省为void
const a = foo();	// 此时a的类型定义为void,你也不能调用a的任何属性方法

void 和 undefined 类型最大的区别是,你可以理解为 undefined 是 void 的一个子集,当你对函数返回值并不在意时,使用 void 而不是 undefined。举一个 React 中的实际的例子。

// Parent.tsx
function Parent(): JSX.Element {
  const getValue = (): number => { return 2 };   	/* 这里函数返回的是number类型 */
  // const getValue = (): string => { return 'str' };	/* 这里函数返回的string类型,同样可以传给子属性 */
  return <Child getValue={getValue} />
}
// Child.tsx
type Props = {
  getValue: () => void;  // 这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都可以
}
function Child({ getValue }: Props) => <div>{getValue()}</div>

Null 和 Undefined

TypeScript里,undefinednull两者各自有自己的类型分别叫做undefinednull。 和 void相似,它们的本身的类型用处不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

默认情况下nullundefined是所有类型的子类型。 就是说你可以把 nullundefined赋值给number类型的变量。

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

但是 undefined 可以给 void 赋值

let c:void = undefined // 编译正确
let d:void = null // 编译错误

never

是指没法正常结束返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。

function foo(): never { throw new Error('error message') }  // throw error 返回值是never
function foo(): never { while(true){} }  // 这个死循环的也会无法正常退出

还有就是永远没有相交的类型:

type human = 'boy' & 'girl' // 这两个单独的字符串类型并不可能相交,故human为never类型

不过任何类型联合上 never 类型,还是原来的类型:

type language = 'ts' | never   // language的类型还是'ts'类型

关于 never 有如下特性:

  • 在一个函数中调用了返回 never 的函数后,之后的代码都会变成deadcode
function test() {
  foo();  		// 这里的foo指上面返回never的函数
  console.log(111); 	// Error: 编译器报错,此行代码永远不会执行到
}
  • 无法把其他类型赋给 never:
let n: never;
let o: any = {};
n = o;  // Error: 不能把一个非never类型赋值给never类型,包括any

关于never类型,其实有很多人会疑惑,never类型表示一个没有可能的值,既然没有可能,那么为什么会有这么一个类型。在知乎上,有尤大大这么一串回答,其实挺有意思的,下面是尤大大原话。

举个具体点的例子,当你有一个 union type:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。

TypeScript从0到1的学习之路(都这个年头了,你确定不学习ts卷一下吗?)8500字分享

所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

unkonwn

unknown 指的是不可预先定义的类型,在很多场景下,它可以替代 any 的功能同时保留静态检查的能力。

const num: number = 10;
(num as unknown as string).split('');  	// 注意,这里和any一样完全可以通过静态检查

这个时候 unknown 的作用就跟 any 高度类似了,你可以把它转化成任何类型,不同的地方是,在静态编译的时候,unknown 不能调用任何方法,而 any 可以。

const foo: unknown = 'string';
foo.substr(1);   	// Error: 静态检查不通过报错
const bar: any = 10;
any.substr(1);		// Pass: any类型相当于放弃了静态检查

unknown 的一个使用场景是,避免使用 any 作为函数的参数类型而导致的静态类型检查 bug:

function test(input: unknown): number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: 这个代码块中,类型守卫已经将input识别为array类型
  }
  return input.length;      // Error: 这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险
}

类型别名

像我们上面定义的类型,其实他们只能被使用一次,如何能够多次使用达到复用,这就需要用到别名关键字去定义类型了

type

使用type去定义类型 可以多次复用,例如定义一个string类型的变量

type Str = number;
const str: Str = '1234' // 不能将类型分配给number

定义类型名的时候,一般采用大驼峰写法,能很好的区分变量名和类型名

type Obj = {
  a: boolean,
  b: number
};
const obj: Obj = {
  a: true,
  b: 1
} // true

interface

interface接口是专门给对象定义类型

interface Obj {
  a: true,
  b: boolean
}
const obj: Obj = {
  a: true,
  b: false
} // true

interface比type具有专门的针对性,扩展了extends继承关键字,可以继承类型,例如

interface Obj {
  a: true,
  b: boolean
}

interface Obj1 extends Obj {
  c: string,
  d: '我是小明'
}

const obj: Obj1 = {
  a: true,
  b: false,
  c: 'sss',
  d: '我是小明'
} // true

除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}

enum

其实枚举类型也可以服用,也相当于取了别名,但是在上面基础类型讲了,就不再叙述了

对象类型

object, Object 和 {} 类型

  • object object 类型用于表示所有的非原始类型,即我们不能把 number、string、boolean、symbol等 原始类型赋值给 object。在严格模式下,null 和 undefined 类型也不能赋给 object。
let object: object;
object = 1; // 报错
object = "a"; // 报错
object = true; // 报错
object = null; // 报错
object = undefined; // 报错
object = {}; // 编译正确
复制代码
  • Object

大 Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 null 和 undefined 不可以)

let bigObject: Object;
object = 1; // 编译正确
object = "a"; // 编译正确
object = true; // 编译正确
object = null; // 报错
ObjectCase = undefined; // 报错
ObjectCase = {}; // ok
复制代码
  • {}

{} 空对象类型和大 Object 一样 也是表示原始类型和非原始类型的集合

class类

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  say(): void {
    alert('nihao' + this.name)
  }
}

同样 也可以使用extends关键字实现继承

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  say(): void {
    console.log('nihao' + this.name)
  }
}

class Student extends Person {
  school: string;
  constructor(name: string, age: number, school: string) {
    super(name, age);
    this.school = school
  }
  say1(): void {
    alert('nihao')
  }
}
const student = new Student('xiaoming', 18, '大河小学') // {name: 'xiaoming', age: 18, school: '大河小学'}
student.say()

function函数

函数类型定义有两种方式,一种是函数表达式的,还有一种是函数声明的

函数声明

function fn(x: number, y: number): number {
  return x + y
}

函数表达式

// 函数表达式
const fnn = (x: string, y: string): string => {
  return x + y
}

函数参数可选

函数其实比较特殊,因为有一些参数,你可以传,也可以不传,那么如何去判断呢。就会用到我们的可选参数,这里有一个注意点,就是可选参数必须放到后面。这点应该不难理解:

// 可选参数
function fn1(x?: number, y?: number): number {
  return x + y
} // true

function fn2(x?: number, y: number): number {
  return x + y
} // false 必须参数不能位于可选参数后

重载

JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。如果一个函数,我们想传入string,有的时候又传入number,那该怎么定义呢,我们就可以定义多个函数类型。

function add(x: number, y: number): number;
function add(x: string, y: string): string;
function add(x, y) {
  return x + y;
}
const res = add("1", "2") // res1类型为string
const res1 = add(1, 2) // res1类型为number
const res2 = add('1', 2) // false 没有与此调用匹配的重载

注意 上面定义的三个add 其中两个都是函数类型,并不是函数声明,最终执行的是最下面这个函数声明。函数声明只能有一个,然而函数类型可以多个同名

为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

索引类型

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用"100"(一个string)去索引,因此两者需要保持一致。

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"]两种形式都可以。 下面的例子里, name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

你不能设置myArray[2],因为索引签名是只读的。

类型推论

基础

TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子

let x = 3; // x的类型会自动被推论为number

变量x的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。

大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。

let x = [0, 1, null]; // x的类型为 number[]
x = [undefined] // true

为了推断x的类型,我们必须考虑所有元素的类型。 这里有两种选择: numbernull。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。

交叉类型

交叉类型其实与js中的&&很相似,代表都有,符号用&表示,表示两个类型必须都存在

type Person = {
  name: string,
  age: number
}
type Stu = {
  scroll: string
}
type Student = Person & Stu
const student: Student = {
  name: '123',
  age: 16,
  scroll: '大河'
}

一般都是对象类型的才有交叉类型,像字面量类型的联合,就直接变成never类型了,因为不可能存在

type A = 'boy' & 'girl' // A的类型为never

联合类型

联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number或 string类型的参数。 例如下面的函数:

const fns = (x:string | number) => {
  return x
}

联合类型符号用|表示,代表或的意思。属性多,至少满足其中一种类型就成,像下面例子也是可以通过ts编译的

type Obj1 = {
  a: string,
  b: number
}

type Obj2 = {
  c: string,
  d: number
}
const obj: Obj1 | Obj2 = {
  a: '123',
  b: 456,
  c: '124'
} // true 虽然多了个c属性,但是该类型满足了Obj1类型,同时c属性也在Obj2的类型里面,所以能通过编译

类型断言

某些情况下,我们可能比typescript更加清楚的知道某个变量的类型,所以我们可能希望手动指定一个值的类型

类型断言有两种方式

  • 尖括号写法
let str: any = "to be or not to be";
let strLength: number = (<string>str).length;
复制代码
  • as 写法
let str: any = "to be or not to be";
let strLength: number = (str as string).length;

非空断言

这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是非 null|undefined 的

function onClick(callback?: () => void) {
  callback!();		// 参数是可选入参,加了这个感叹号!之后,TS编译不报错
}

ts操作符

键值获取keyof

keyof 可以获取一个类型所有键值,返回一个联合类型,如下:

type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  // PersonKey得到的类型为 'name' | 'age

keyof 的一个典型用途是限制访问对象的 key 合法化,因为 any 做索引是不被接受的。keyof 经常搭配in来使用,后面会讲

function getValue (p: Person, k: keyof Person) {
  return p[k];  // 如果k不如此定义,则无法以p[k]的代码格式通过编译
}

总结起来 keyof 的语法格式如下

类型 = keyof 类型

实例类型获取typeof

我们可以根据已有的数据,通过typeof,去获取它的类型

const person = {
 name: 'xiaomign',
 age: 15
}
type Person = typeof person // Person类型为{name: string; age: number}

typeof还可以搭配keyof使用,例如

const person = {
  name: 'xiaomign',
  age: 15
}
type Person = typeof person // Person类型为{name: string; age: number}
type Personkey = keyof typeof person // Person类型为 'name' | 'age'

typeof语法格式为

类型 = typeof 实例

遍历属性 in

in 只能用在类型的定义中,可以对枚举类型进行遍历,如下:

// 这个类型可以将任何类型的键值转化成number类型
type TypeToNumber<T> = {
  [key in keyof T]: number
}

keyof返回泛型 T 的所有键枚举类型,key是自定义的任何变量名,中间用in链接,外围用[]包裹起来(这个是固定搭配),冒号右侧number将所有的key定义为number类型。

于是可以这样使用了:

const obj: TypeToNumber<Person> = { name: 10, age: 10 }

总结起来 in 的语法格式如下:

[ 自定义变量名 in 枚举类型 ]: 类型

泛型

泛型在ts中,是最难学,也是最重要的一部分,如果说学了上面相当于会蹬自行车,那么学好泛型,相当于能开飞机了。重要性不言而喻。泛型的意义在于能使我们类型更加灵活,举个例子,一个函数传入一个参数,那个参数如果传的是string 那么返回个string出来,如果传number,则返回number。这个时候你该如何去定义这个类型。所以泛型就由此诞生了

基本使用

下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是 echo命令。

不用泛型的话,这个函数可能是下面这样:

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

或者,我们使用any类型来定义函数:

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

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。

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

泛型推导与默认值

上面提到了,我们可以简化对泛型类型定义的书写,因为TS会自动根据变量定义时的类型推导出变量类型,这一般是发生在函数调用的场合的。

type Dog<T> = { name: string, type: T }

function adopt<T>(dog: Dog<T>) { return dog };

const dog = { name: 'ww', type: 'hsq' };  // 这里按照Dog类型的定义一个typestring的对象
adopt(dog);  // Pass: 函数会根据入参类型推断出typestring
复制代码

若不适用函数泛型推导,我们若需要定义变量类型则必须指定泛型类型。

const dog: Dog<string> = { name: 'ww', type: 'hsq' }  // 不可省略<string>这部分

如果我们想不指定,可以使用泛型默认值的方案。

type Dog<T = any> = { name: string, type: T }
const dog: Dog = { name: 'ww', type: 'hsq' }
dog.type = 123;    // 不过这样type类型就是any了,无法自动推导出来,失去了泛型的意义

泛型默认值的语法格式简单总结如下:

泛型名 = 默认类型

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

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

let myIdentity: <T>(arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

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

let myIdentity: <U>(arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

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

let myIdentity: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity;

泛型约束

有的时候,我们想要约束泛型,如何能办到这事,就需要extends关键字。这里的extends并不代表继承,在这里反而是约束了泛型的类型

你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

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

loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

泛型条件

上面提到 extends,其实也可以当做一个三元运算符,如下:

T extends U? X: Y

这里便不限制 T 一定要是 U 的子类型,如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。

这块要搭配下面泛型推断一起来学

泛型推断 infer

infer 的中文是“推断”的意思,一般是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过你得先预定义好整体的结构。举个例子

type Foo<T> = T extends {t: infer Test} ? Test: string

首选看 extends 后面的内容,{t: infer Test}可以看成是一个包含t属性类型定义,这个t属性的 value 类型通过infer进行推断后会赋值给Test类型,如果泛型实际参数符合{t: infer Test}的定义那么返回的就是Test类型,否则默认给缺省的string类型。

举个例子加深下理解:

type One = Foo<number>  // string,因为number不是一个包含t的对象类型
type Two = Foo<{t: boolean}>  // boolean,因为泛型参数匹配上了,使用了infer对应的type
type Three = Foo<{a: number, t: () => void}> // () => void,泛型定义是参数的子集,同样适配

infer用来对满足的泛型类型进行子类型的抽取,有很多高级的泛型工具也巧妙的使用了这个方法。

泛型工具

泛型工具有很多,例如将类型全部变为只读,以及类型全部变为可选

1. Required

将类型的属性变成必选

interface Person {
  name?: string,
  age?: number,
  hobby?: string[]
}
type Required<T> = {
  [Key in keyof T]-?: T[Key]
}

const user: Required<Person> = {
  name: "树哥",
  age: 18,
  hobby: ["code"]
}

在这里 -?是一个非常有意思的写法,相当于把可选去掉

2. Partial

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

interface Person {
  name: string,
  age: number,
  hobby: string[]
}
type Partial<T> = {
  [K in keyof T]?: T[K]
}

const user: Partial<Person> = {
  name: "树哥",
  age: 18,
} // 编译正确

3. Exclude

Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉,剩余的属性构成新的类型

此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。这里使用never,是因为任何属性和never联合 都是自身

type Excluede<T, U> = T extends U ? never : T

const user: Excluede<'a' | 'b' | 'c', 'a' | 'b'> = 'c' //true

4. Extract

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

type Extract<T, U> = T extends U ? T : never

const user: Extract<'a' | 'b' | 'c', 'a' | 'b' | 'f'> = 'a'//true

适用于:并集类型

5. Readonly

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

interface Person {
  name?: string,
  age?: number,
  hobby?: string[]
}
type Readonly<T> = {
  readonly [Key in keyof T]: T[Key]
}

const user: Readonly<Person> = {
  name: "树哥",
  age: 18,
  hobby: ["code"]
}
user.age = 12 // false, 因为它是只读属性

6. Record

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

interface Person {
  name?: string,
  age?: number,
  hobby?: string[]
}
type Pe = 'key1' | 'key2'
type Record<T extends keyof any, U> = {
  [Key in T]: U
}

const user: Record<Pe, string> = {
  key1: "树哥",
  key2: '334',
} // true 所有类型都被转位string类型

7. Pick

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

interface Person {
  name: string,
  age: number,
  hobby: string[]
}

type Pick<T, U extends keyof T> = {
  [Key in U]: T[U]
}

const user: Pick<Person, 'age' | 'hobby'> = {
  age: 4,
  hobby: ['1']
} // true

8. Omit

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

interface Person {
  name: string,
  age: number,
  gender: string
}
// type Omit<T, K extends keyof T> = {
//   [Key in K]: T[K]
// }
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type P1 = Omit<Person, "age" | "gender">
const user: P1 = {
  name: '树哥',
}

9. NonNullable

去除类型中的 nullundefined

type NonNullable<T> = T extends keyof null | undefined ? never : T;
type P1 = NonNullable<string | number | undefined>; // string | number
type P2 = NonNullable<string[] | null | undefined>; // string[]
type P3 = NonNullable<string[] | number[] | { a: string } | undefined>; // string[] | number[] | {a: string}

10. ReturnType

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

type Func2 = (value: string) => string;
type ReturnType<T extends (...angs: any) => any> = T extends (...angs: any) => infer R ? R : any;
type off = ReturnType<Func2> // string;
const test: ReturnType<Func2> = '23'; // true

11. Parameters

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

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type P1 = Parameters<(a: number, b: string) => string>; // [number, string]

结语

到现在 整篇文章算结束了,其实ts远不止这些。还有ts的配置,以及更多工具的学习,但是我没写了。1是太多永远写不完,2是ts的配置的话,其实更多的创建项目,一年难得配一次。不用就会忘,与其现在记住,不如需要用的时候再去了解。

我相信大家能看到这里的时候,都是想学好ts。学习永无止境,大家一定要自己去敲代码学习。像我这篇文章也复制了别人很多话哈哈哈,但是代码我都是自己敲的,因为只有代码自己敲才能真正的学好。下面有借鉴文章地址,和原作者联系了,原作者同意借鉴。如有侵权联系我删除,我自己代码也放到git上了,需要的话可以下载。

参考文献

github代码地址

github仓库地址