- 持续更新
- 24.06.22
- 24.09.02
基础学习
掘金各大总结,随便搜搜包你满意
「1.9W字总结」一份通俗易懂的 TS 教程,入门 + 实战!
混淆概念
type VS interface
在 Typescript 里,这俩概念很容易混淆,因为都可以用来表示接口
相同点
- 都可以定义对象或函数
interface P1 {
name: string,
age: number,
getAge: (name:string) => number
}
type P2 = {
name: string,
age: number,
getAge: (name:string) => number
}
- 都可以相互合并继承
type exampleType1 = {
name: string
}
interface exampleInterface1 {
name: string
}
type exampleType2 = exampleType1 & {
age: number
}
type exampleType2 = exampleInterface1 & {
age: number
}
interface exampleInterface2 extends exampleType1 {
age: number
}
interface exampleInterface2 extends exampleInterface1 {
age: number
}
不同点
-
type可以定义 基本类型的别名,如
type myType = string -
type可以通过 typeof 操作符来定义,如
type myType = typeof someObj -
type可以申明 联合类型,如
type myType = myType1 | myType2 -
type可以申明 元组类型,如
type myType = [myType1, myType2] -
interface可以 声明合并,即支持多次声明,最后的interface会合并所有声明。type多次声明会报错。
选用时机
-
在定义公共 API(如编辑一个库)时使用
interface,这样可以方便使用者继承接口; -
在定义组件属性(
Props)和状态(State)时,建议使用type,因为type的约束性更强; -
type类型不能二次编辑,而interface可以随时扩展。
any VS unknow
- 不清楚用什么类型,可以使用
any类型。这些值可能来自于动态的内容。对any类型的指执行任何操作,都不需要事先的检查
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK
但这会导致一个问题,有可能你传入的值根本没有相应方法,但由于它是 any 类型,在运行时才会报错
- 而
unknown也可以赋值所有属性,不同的是,unknown上不能执行操作
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error
unknown类型只能被赋值给any类型和unknown类型本身,而any类型可以赋值给所有类型
let anyVal: any;
let unknownVal: unknown;
let strVal: string;
strVal = anyVal; // OK
strVal = unknownVal; // Error
疑难理解
断言
使用断言有两种格式
<类型>值
// 或者
值 as 类型
推荐以 as 方式,因为 JSX 这样的语法中只支持 as 方式
- 我们来看一段代码
function getLength(arg: number | string): number {
if(arg.length){
return arg.length
} else {
return arg.toString().length
}
}
这时候编译器会报错,因为当arg为number时,arg.length并不存在,虽然代码已经写明了,有这个方法的时候才执行
这时候就需要用到断言了,它就是告诉编译器 我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了”
- as 断言
function getLength(arg: number | string): number {
if((arg as string).length){
return (arg as string).length
} else {
return arg.toString().length
}
}
// 简写
function getLength(arg: number | string): number {
const str = arg as string;
if(str.length){
return (arg as string).length
} else {
const number = arg as number;
return number.toString().length
}
}
- <> 断言
function getLength(arg: number | string): number {
if ((<string>arg).length) {
return (<string>arg).length
} else {
return arg.toString().length
}
}
type & typeof
在下面的例子中,typeof tom 虽然是 "object",但 type Tom = typeof tom并不等同于type Tom2 = object
interface Person {
name: string;
age: number;
}
const tom: Person = { name: 'tom', age: 30 };
console.log(typeof tom); // -> object
type Tom = typeof tom; // -> Tom 是 Person 类型
type Tom2 = object; // -> Tom2 是 object 类型
const obj : Tom = {name: 's', age: 30} // 正确
// const obj : Tom = {name: 's'} // 报错
const obj2 : Tom2 = {name: 's'} // 正确
keyof
keyof 操作符可以用来一个对象中的所有 key 值:
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
// const k1:K1 = "age"; //正确
const k1:K1 = "name"; //正确
const k2:K2 = "toString" //正确
const k3:K3 = "string" //正确
映射类型
Typescript 允许将一个类型映射成另外一个类型。也就是说,其实最终都是定义 type
in实现对联合类型的遍历,例如:
type Person = "name" | "school" | "major"
type Obj = {
[p in Person]: string
}
PartialPartial<T>将T的所有属性映射为可选的,例如:
interface IPerson {
name: string
age: number
}
type IPartial = Partial<IPerson>
let p1: IPartial = {name: "only name"}
ReadonlyReadonly<T>将T的所有属性映射为只读的PickPick用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:
interface IPerson {
name: string
age: number
sex: string
}
type IPick = Pick<IPerson, 'name' | 'age'>
let p1: IPick = {
name: 'cc',
age: 18
}
RecordRecord<U,T>将创建新属性的非同态映射类型。不好理解?直接看例子
interface IPerson {
name: string
age: number
}
type IRecord = Record<string, IPerson>
let personMap: IRecord = {
person1: {
name: 'cc',
age: 18
},
person2: {
name: 'vv',
age: 25
}
}
类型保护
A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文档 好吧,管它是什么,看例子就对了。
注意与断言的区分!!!
typeof
typeof 用于判断
number,string,boolean或symbol四种类型
// 错误写法
function getLength(arg: number | string): number {
if(arg.length){ // 报错
return arg.length
} else {
return arg.toString().length
}
}
// 正确写法
function getLength(arg: number | string): number {
if(typeof arg === "string"){
return arg.length
} else {
return arg.toString().length
}
}
in
in 用于判断一个属性/方法是否属于某个对象
// 错误写法
interface Admin {
name: string;
privileges: string[];
}
interface Employee {
name: string;
startDate: Date;
}
function printEmployeeInformation(emp: Employee | Admin) {
if (emp.privileges) { // 报错
return emp.privileges;
}
if (emp.privileges) {
return emp.startDate;
}
return ""
}
// 正确写法
function printEmployeeInformation(emp: Employee | Admin) {
if ("privileges" in emp) {
return emp.privileges;
}
if ("startDate" in emp) {
return emp.startDate;
}
return ""
}
instanceof
instanceof 用于判断一个实例是否属于某个类
interface Padder {
getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) {}
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) {}
getPaddingString() {
return this.value;
}
}
let padder: Padder = new SpaceRepeatingPadder(6);
if (padder instanceof SpaceRepeatingPadder) {
// padder的类型收窄为 'SpaceRepeatingPadder'
}
泛型约束
默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性
function id<Type>(value: Type): Type {
console.log(value.length) // error
return value
}
这是 Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length 此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)
添加泛型约束收缩类型,主要有以下两种方式:
- 指定更加具体的类型
- 添加约束
- 指定更加具体的类型
可以将类型修改为Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性(不推荐这种做法)
function id<Type>(value: Type[]): Type[] {
console.log(value.length)
return value
}
- 添加约束
// 创建一个接口
interface ILength { length: number }
// Type extends ILength 添加泛型约束
// 解释:表示传入的 类型 必须满足 ILength 接口的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends ILength>(value: Type): Type {
console.log(value.length)
return value
}
首先创建描述约束的接口 ILength,该接口要求提供 length 属性。通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
该约束表示:传入的类型必须具有 length 属性
这时候,只要传入的实参(比如,数组)只要有 length 属性即可(类型兼容性)
多个泛型类型变量
泛型的类型变量可以有多个,并且类型变量之间还可以约束
下面是创建一个函数来获取对象中属性的值:
function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')
这里有两个泛型类型,Type 和 Key,用逗号分隔。
keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性
// Type extends object 表示: Type 应该是一个对象类型,如果不是 对象 类型,就会报错
// 如果要用到 对象 类型,应该用 object ,而不是 Object
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key]
}
泛型接口
接口也可以配合泛型来使用,以增加其灵活性,增强其复用性
interface IdFunc<Type> {
id: (value: Type) => Type
ids: () => Type[]
}
在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口
接口的类型变量,对接口中所有其他成员可见,他们都可以使用类型变量
使用泛型接口时,需要显式指定具体的类型
let obj: IdFunc<number> = {
id(value) { return value },
ids() { return [1, 3, 5] }
}
常用技巧
-
联合类型通常与
null或undefined一起使用 -
tsconfig.json作用
用于标识 TypeScript 项目的根路径;
用于配置 TypeScript 编译器;
用于指定编译的文件
-
只要
tsconfig.json中的配置包含了typing.d.ts文件,那么其他所有*.ts文件就都可以获得声明文件中的类型定义 -
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。
let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // 报错
- import引入类型声明需要
type关键字,因为import type是用来导入类型声明的语法,它不会在编译后生成实际的代码,而是只在编译期间进行类型检查。而import则是用来导入具体的值或模块的语法,会在编译后生成实际的代码。因此,如果只需要使用类型声明而不需要实际的值或模块,应该使用import type
import type { OwnType } from '@/type';
TS怎么给引入的第三方库设置类型声明文件
目前,几乎所有常用的第三方库都有相应的类型声明文件,第三方库的类型声明文件有两种存在形式:
1、 库自带类型声明文件(如axios)
这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明
2、 由 TS官方写
DefinitelyTyped 是一个TS官方提供的GIT仓,用来提供高质量 TypeScript 类型声明
可以通过 npm/yarn 来下载该仓库提供的 TS 类型声明包,这些包的名称格式为:@types/*,比如,@types/react、@types/lodash 等
3、基于库自带类型声明再自定义声明
假设原有的 axios 声明不满足当前条件,我们可以在 index.d.ts 下自定义部分声明,该值会与原有声明文件中的声明合并生效
declare module 'axios' {
interface AxiosRequestConfig {
mask?: boolean; // 自定义属性
repeat?: boolean;
}
}
在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode 会给出明确的提示
为什么会觉得难用
在实际使用中,吐槽Typescript的越来越多(我也在吐槽,我也是新手T.T)。这里简单分析一下
类型复用
这里主要有类型未复用问题和类型复用不够的问题。
-
前者的话,type通过交叉类型
&复用,interface通过继承extends,上面已经讲过了 -
后者场景是,一般的复用想到的都是新增属性,但如果某个interface已经大部分满足条件,唯有一个属性不满足,这时候很多人就不会复用了。
例如,有一个已有的类型A需要复用,但其中的属性c需要变成属性e
interface A {
a: string;
b: string;
c: string;
}
我们可以利用TypeScript提供的工具类型Omit来更高效地实现这种复用。
interface A {
a: string;
b: string;
c: string;
}
// 排除某些属性
interface B extends Omit<A, 'c'> {
e: string;
}
// 选择某些属性
interface B extends Pick<A, 'a' | 'b'> {
e: string;
}
处理含有不同元素的数组
有时候,一个数组可能包含不同类型,这时候可以使用元组
参数数量和类型不确定
一般这种情况,很多人选择使用any。但其实我们可以使用函数重载,针对不同参数定义多个重载函数(虽然更麻烦,毕竟多个函数)。