本文系统梳理TypeScript类型声明的核心用法,涵盖基本类型、数组、接口、联合与交叉类型等基础概念,深入解析any、void等特殊类型。重点讲解实战中遇到的第三方库无声明文件时的处理方案,包括全局变量、非JS资源及强制类型转换的正确写法,并提供独立函数、索引属性及动态扩展属性的声明技巧,帮助开发者写出更严谨的TS代码。
TypeScript类型声明概述
本文旨在总结TypeScript类型声明的书写规范。在实际开发中,编写业务逻辑通常较为顺畅,但处理类型定义时往往令人纠结。本文整理了我在使用TypeScript过程中遇到的典型问题。当你对类型声明感到困惑时,建议参考lodash的类型声明文件,因为lodash对数据进行了大量变形操作,其中包含了你能遇到的各种类型处理示例。
基础类型与数组声明
基本类型
// 变量
const num: number = 1;
const str: string = 'str';
const bool: boolean = true;
const nulls: null = null;
const undefine: undefined = undefined;
const symbols: symbol = Symbol('symbal');
const any: any = 'any types'; // typescript的any类型,相当于什么类型约束都没有
数组
// 数组: 推荐使用T[]这种写法
const nums: number[] = [1, 2, 3, 4];
// 不推荐:Array<T>泛型写法,因为在JSX中不兼容,所以为了统一都使用T[]这种类型
const strs: Array<string> = ['s', 't', 'r'];
const dates: Date[] = [new Date(), new Date()];
关于数组的concat方法,有时会出现返回类型为never[]的问题。
// 数组concat方法的never问题
// 提示: Type 'string' is not assignable to type 'never'.
const arrNever: string[] = [].concat(['s']);
// 主要问题是:[]数组,ts无法根据上下文判断数组内部元素的类型
// @see https://github.com/Microsoft/TypeScript/issues/10479
const fixArrNever: string[] = ([] as string[]).concat(['s']);
接口与特殊类型定义
接口
接口是TypeScript的核心特性之一,它能够将多个类型声明合并为一个统一的类型声明:
此外,接口还可以用于声明函数、类以及对象等数据类型。
interface Name {
first: string;
second: string;
}
let username: Name = {
first: 'John',
second: 'Doe'
};
any、null、undefined、void类型
// 特殊类型
const any: any = 'any types'; // typescript的any类型,相当于什么类型都没写
let nobody: any = 'nobody, but you';
nobody = 123;
let nulls: number = null;
let bool: boolean = undefined;
// void
function printUsername (name: string): void {
console.log(name);
}
联合类型与交叉类型
联合类型
联合类型在option bags模式场景下非常实用,使用 **| **符号进行标记。
function options(opts: {
types?: string;
tag: string | number;
}): void {
}
交叉类型
其最典型的使用场景包括继承、mixin模式或者对象拷贝等操作。
// 交叉类型:如果以后遇到此种类型声明不会写,直接看Object.assign声明写法
function $extend<T, U>(first: T, second: U): T & U {
return Object.assign(first, second); // 示意而已
}
元组与Type关键字
元组 tuple
元组在实际开发中使用频率较少。
let nameNumber: [string, number];
// Ok
nameNumber = ['Jenny', 221345];
// Error
// nameNumber = ['Jenny', '221345'];
let tuple: [string, number];
nameNumber = ['Jenny', 322134];
const [usernameStr, uselessNum] = nameNumber;
type的作用
type用于创建新类型,也可以为已有类型重命名(即类型别名)。建议对简单类型、无嵌套或仅有一层嵌套的类型使用type;对于其他复杂类型,应优先使用interface,并结合implements和extends关键字实现。
type StrOrNum = string | number;
// 使用
let sample: StrOrNum;
sample = 123;
sample = '123';
// 会检查类型
sample = true; // Error
第三方库声明文件处理
实践中遇到的问题
第三方库没有提供声明d.ts文件
如果第三方库未提供声明文件,首先应前往微软官方的仓库https://github.com/borisyankov/DefinitelyTyped 查找,或者在npmjs.com上搜索@types/依赖的模块名,大多数情况下都能找到对应的类型定义。
手动添加声明文件
声明文件通常统一放置在types文件夹下。
// 例子: types/axios.d.ts
declare module 'axios'; // 这里的axios声明为any类型
全局变量与非JS资源声明
全局变量
例如,某些库会直接在window对象上添加全局变量。
// globals.d.ts
// 例子:jQuery,现实中jQuery是有.d.ts
declare const jQuery: any;
declare const $: typeof jQuery;
非JavaScript资源
在前端工程中,import导入大量非JS资源(如css、html、图片、vue文件等)时,TypeScript无法直接识别这些资源类型,此时需要告知TypeScript如何识别这些导入资源的类型。
// 看看vue怎么处理的:shims-vue.d.ts
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
// html
declare module '*.html';
// css
declare module '*.css';
强制类型转换与可选属性
强制类型转换
有时需要进行强制类型转换,特别是针对联合类型或可选属性时。
// 第一种:使用<>括号
const convertArrType: string[] = <Array<string>>[].concat(['s']);
// 第二种:使用as关键字
const fixArrNever: string[] = ([] as string[]).concat(['s']);
建议使用第二种方式,因为它兼容JSX;第一种方式官方已不再推荐,尽管它在语法上是合法的。
可选属性和默认属性
API提供的参数通常具有默认值或为可选属性,应如何书写类型声明?
class Socket {}
// 联合类型
export type SocketType = 'WebSocket' | 'SockJs';
export interface SocketOptions {
type: SocketType;
protocols?: string | string[]; // 可选
pingMessage: string | (() => string); // 联合类型,可以为string或者函数
pongMessage: string | (() => string);
}
// 默认值
export function eventHandler = (
evt: CloseEvent | MessageEvent | Event,
socket: Socket,
type = 'WebSocket' // 默认值
) => any;
独立函数与动态属性声明
独立函数怎么声明类型
起初我也对此感到困惑:作为一个独立的函数,如何声明其类型?特别是在编写事件处理函数的API时。
class Socket {}
// 函数的声明方式
export type SocketEventHandler = (
evt: CloseEvent | MessageEvent | Event,
socket: Socket
) => any;
const eventHandler: SocketEventHandler = (evt, socket) => {
}
// 可选参数和rest参数
let baz = (x = 1) => {};
let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};
索引属性类型声明
JavaScript中的对象可以通过字符串索引直接获取属性或调用方法,TypeScript中也提供了相应的类型声明方法。
type Hello = {
hello: 'world';
// key只是一个形式属性名(类似形参一样)
[key: string]: string;
};
const greeting: Hello = {
hi: 'morning'
}
console.log(greeting['hi'])
动态添加的属性声明
有时我们仅声明了基本类型结构,后续可能需要扩展,特别是在二次封装options时。
interface AxiosOptions {}
type AjaxOptions = {
axiosOptions: AxiosOptions;
// 额外扩展的放入到单独的属性节点下
extraOptions: {
[prop: string]: any
};
};
type AjaxOptions1 = {
axiosOptions?: AxiosOptions;
// 不要这样写,因为axiosOptions拼写错误时,ts不会提示
// 尽量把后续扩展的属性,移动到独立的属性节点下
[prop: string]: any
};
const ajaxOptions: AjaxOptions1 = {
axiosOptions1: {}; // 本意是axiosOptions,但是ts不会提示
}
断言符号与上下文绑定
!的使用
!标识符告诉TypeScript编译器,声明的变量没有问题,在运行期不会报错。
class BaseSelect extends Vue {
options: string[]; // 这里会提示没有在constructor中初始化
created() {
this.options = ['inited']
}
}
class BaseSelect extends Vue {
options!: string[]; // 使用 ! 告诉编译器,我知道自己在做什么
created() {
this.options = ['inited']
}
}
this的使用
对于独立使用的函数,可以声明指定的调用上下文。
class Handler {
info: string;
// 声明指定的this上下文
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
声明合并与总结
声明合并(扩展Vue声明)
来看看使用场景:扩展Vue,在Vue实例上添加全局属性。
// vue的声明在 vue/types/vue.d.ts
declare module 'vue/types/vue' {
// 相当于Vue.$eventBus
interface Vue {
$eventBus: Vue;
}
// 相当于在Vue.prototype.$eventBus
interface VueConstructor {
$eventBus: Vue;
}
}
总结
TypeScript声明还有很多高级用法,目前我也未涉及太多。当我对如何书写声明感到纠结时,我会查看他人的声明文件是如何编写的。
注意:尽量不要将解构和声明写在一起,这样可读性极差。
class Node {
onNodeCheck(checkedKeys: any, { // 解构
checked, checkedNodes, node, event,
} : { // 声明
node: any;
[key: string]: any;
}
) {
}
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。