TypeScript 报错汇总
在这篇文章中将记录我遇到的ts错误,应该会持续更新。
有时候从错误点入手学习似乎是一个不错的选择,所以也欢迎你私信我一些ts的问题。
一、内置工具
1.1 Pick & Partial
先看看Pick
和Partial
工具的源码:
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
从代码和注释来看,
- 通过
Pick
工具根据联合类型数据,筛选泛型T中的属性, - 通过
Partial
工具将接口属性都变为可选属性
比如:
interface User {
id: number;
age: number;
name: string;
};
// 相当于: type PartialUser = { id?: number; age?: number; name?: string; }
type PartialUser = Partial<User>
// 相当于: type PickUser = { id: number; age: number; }
type PickUser = Pick<User, "id" | "age">
现在实现一个需求:筛选出目标接口中的函数属性,删除其他属性。
// 目标接口
interface Part {
id: number
name: string
subparts: Part[]
firstFn: (brand: string) => void,
anotherFn: (channel: string) => string
}
首先遍历接口,将非函数类型的属性设置为never
,如果是函数类型,取其属性名,然后通过Pick
拿到函数类型成员集合:
type FunctionFilterNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
完整代码:
type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T]
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
interface Part {
id: number
name: string
subparts: Part[]
firstFn: (brand: string) => void,
anotherFn: (channel: string) => string
}
// 过滤出所有的函数key
// type FnNames = "firstFn" | "anotherFn"
type FnNames = FunctionPropertyNames<Part>
// 根据对象的key获取函数接口集合
// type FnProperties = {
// firstFn: (brand: string) => void;
// anotherFn: (channel: string) => string;
// }
type FnProperties = FunctionProperties<Part>
let func: FnProperties = {
firstFn: function (brand: string): void {
throw new Error("Function not implemented.")
},
anotherFn: function (channel: string): string {
throw new Error("Function not implemented.")
}
}
如果需要深 Partial 我们可以通过泛型递归来实现
type DeepPartial<T> = T extends Function
? T
: T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T
type PartialObject = DeepPartial<object>
1.2 Record
先看看Record
工具的源码:
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
从源码和注释来看,这个工具的目标是:以K中的每个属性作为key
值,以T作为value
构建一个map
结构
比如:
type pets = 'dog' | 'cat';
interface IPetInfo {
name: string,
age: number,
}
type IPets = Record<pets, IPetInfo>;
const animalsInfo: IPets = {
dog: {
name: 'Ryuko',
age: 1
},
cat: {
name: 'Ryuko',
age: 2
}
}
这个案例来源于这篇文章
现在实现一个需求,封装一个http请求:
首先思考请求方法一般需要哪些参数,比如请求类型、data数据、config配置
通过enum
枚举几个常见的请求类型,然后每个具体的方法返回值都是一个Promise:
enum IHttpMethods {
GET = 'get',
POST = 'post',
DELETE = 'delete',
PUT = 'put',
}
interface IHttpFn<T = any> {
(url: string, config?: AxiosRequestConfig): Promise<T>
}
// 以enum参数为key,每个key对应一种请求方法
// type IHttp = {
// get: IHttpFn<any>;
// post: IHttpFn<any>;
// delete: IHttpFn<any>;
// put: IHttpFn<any>;
// }
type IHttp = Record<IHttpMethods, IHttpFn>;
接下来设置一个methods数组,稍后通过reduce
方法遍历这个数组,目的是将所有的方法体放在一个对象httpMethods
中,形式如下:
httpMethods = {
get: [Function ()],
post: [Function ()],
delete: [Function ()],
put: [Function ()]
}
最后将httpMethods
暴露出去,那么外面就可以通过httpMethods.get(...)
等方法直接调用:
const methods = ["get", "post", "delete", "put"];
// map为total对象,method为当前遍历到的方法
const httpMethods: IHttp = methods.reduce(
(map: any, method: string) => {
map[method] = (url: string, options: AxiosRequestConfig = {...}) => {
const { data, ...config } = options; \
return (axios as any)[method](url, data, config)
.then((res: AxiosResponse) => {
if (res.data.errCode) {
//todo something
} else {
//todo something
}
});
}
},{}
)
export default httpMethods
完整代码:
enum IHttpMethods {
GET = 'get',
POST = 'post',
DELETE = 'delete',
PUT = 'put',
}
interface IHttpFn<T = any> {
(url: string, config?: AxiosRequestConfig): Promise<T>
}
type IHttp = Record<IHttpMethods, IHttpFn>;
const methods = ["get", "post", "delete", "put"];
const httpMethods: IHttp = methods.reduce(
(map: any, method: string) => {
map[method] = (url: string, options: AxiosRequestConfig = {...}) => {
const { data, ...config } = options; \
return (axios as any)[method](url, data, config)
.then((res: AxiosResponse) => {
if (res.data.errCode) {
//todo something
} else {
//todo something
}
});
}
},{}
)
export default httpMethods
1.3 Exclude & omit
先看看Exclude
和omit
工具的源码:
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
从代码和注释来看:
- Exclude可以选出T不存在于U中的类型
- Omit可以抛弃某对象中不想要的属性
比如:
// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
interface User {
id: number;
age: number;
name: string;
};
// 相当于: type PickUser = { age: number; name: string; }
type OmitUser = Omit<User, "id">
举个例子,现在我们想引入第三方库中的组件,可以这样做:
// 获取参数类型
import { Button } from 'library' // 但是未导出props type
type ButtonProps = React.ComponentProps<typeof Button> // 获取props
type AlertButtonProps = Omit<ButtonProps, 'onClick'> // 去除onClick
const AlertButton: React.FC<AlertButtonProps> = props => (
<Button onClick={() => alert('hello')} {...props} />
)
二、类型 “string” 没有调用签名 ts(2349)
函数返回元组的时候,在使用的时候,元素可能是元组中的任意一个类型,比如:
所以,在对返回的元组进行取值操作时,返回值内的类型顺序,可能和函数内的顺序不一致,需要多加一个条件判断:
function test<T>(name: T){
let myName = name
const setName = (newName: T): void => {
if(typeof newName === 'string'){
console.log(newName.length);
}
}
// console.log(typeof setName); // function
return [myName, setName]
}
const [myName, setName] = test<string>("Ryuko")
// 此表达式不可调用。"string | ((newName: string) => void)" 类型的部分要素不可调用。
// 类型 "string" 没有调用签名。ts(2349)
// setName("test")
// 编译器无法判断setName是string还是一个函数,所以需要通过typeof手动判断
if(typeof setName === 'function'){
setName("test")
}
console.log(myName); //Ryuko
export{}
在这个报错案例中,第四行的typeof newName === 'string'
判断也是很重要的知识点,面对联合类型传参的情况,我们常常需要通过类型判断来决定最后要执行哪个方法:
type Name = string
type NameResolve = (name: string) => string
type NameOrResolver = Name | NameResolve
function getName(param: NameOrResolver): Name{
if(typeof param === 'string'){
return param
}else{
return param("Ryuko")
}
}
console.log(getName("Ryuko")); // Ryuko
console.log(getName(
(p: string) => { return p + "si" }
)); // Ryukosi
三、类型 “string” 到类型 “number” 的转换可能是错误的ts(2352)
// 类型 "string" 到类型 "number" 的转换可能是错误的,因为两种类型不能充分重叠。
// 如果这是有意的,请先将表达式转换为 "unknown"
// 在那些将取得任意值,但不知道具体类型的地方使用 unknown,而非 any。
// let a = 'Ryuko' as number
// 更正:先将数据转化为unknown,再将数据转化为子类型的number
let a = ('Ryuko' as unknown) as number
export {}
这样的转换方式还可以用来定义html
元素,比如我们想要通过dom操作,来改变某个超链接的url路径地址:
可是在HTMLElement
元素节点中并不存在src
这一属性:
因此,我们可以将这个节点属性断言转化为子属性HTMLImageElement
,在子属性身上可以获取到src属性
let elem = document.getElementById('id') as HTMLImageElement
四、类型“string”的参数不能赋给类型“Method”的参数。ts(2345)
type Method = 'get' | 'post' | 'delete'
const requestConfig = {
url: 'localhost: 3000',
// config 中的 method 是string类型的菜蔬,而 request 方法中的Method参数
// method: 'get'
// 解决办法 通过断言进行转换
method: 'get' as Method
}
function request(url: string, method: Method){
console.log(method);
}
// 类型“string”的参数不能赋给类型“Method”的参数。ts(2345)
request(requestConfig.url, requestConfig.method)
export {}
4.1 相关案例
这里再介绍一种情况:
注意:这个用法并没有报错
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
console.log(event);
}
handleEvent(document.getElementById("app")!, "click")
handleEvent(document.getElementById("app")!, "mousemove")
在这个案例中,你可能会认为我传递过去的"click",以及"mousemove"是字符串,既然是字符串,就应该报错:类型“string”的参数不能赋给类型“EventNames”的参数。ts(2345)
。
事实上,这里的字符串参数会被推导为EventNames
类型,而在前面的错误案例中,method:get将会被推导为string
类型!这也是为什么在错误案例中,我们需要手动声明类型method: ‘get’ as Method:
五、对象可能为“未定义”。ts(2532)
function add(num1: number, num2?: number): number{
// 通过可选链提前知道:可能用不上num2这个变量
// 但是如果真的想要操作 num2 的值便会报错
return num1 + num2
}
console.log(add(10));
export {}
在这时就可以通过??
来设置默认值
function add(num1: number, num2?: number): number{
return num1 + (num2 ?? 0)
}
console.log(add(10));
export {}
六、“number”索引类型“number”不能分配给“string”索引类型“string”。ts(2413)
在设置索引的时候可能会出现这样的问题:
interface Person {
[name: string] : string
// “number”索引类型“number”不能分配给“string”索引类型“string”
[age: number] : number
}
// 而只要这样写就不会报错了
interface Person {
[name: string] : string | number
[age: number] : number
}
分析:
在报错的代码中,定义了一个Person接口,这个接口可以采用字符 & 数字
两种类型的索引:既要符合字符,也要符合数字类型
- number类型索引表示:类型规范的是一个数组
- string类型索引表示的是:接收一个对象
数组类型的数据一定可以转化为对象,例如:
['a','b','c']
// 等价于
{
1: 'a',
2: 'b',
3: 'c'
}
而对象类型数据不一定可以转化为数组,例如,如果对象的key值是字符串类型,就无法完成转换了
因此:数组类型可以看作是对象类型的一种子集,例如:
interface ok {
[name: string] : string | number
[age: number] : number
}
interface ok {
[name: string] : string | number | boolean
[age: number] : number
}
interface ok {
[name: string] : number
[age: number] : number
}
interface nope {
[name: string] : number
[age: number] : number | string
}
在这里同样也说明了,为什么可以通过字符串索引来表示json格式的数据:因为json数据的key本质上就是字符串
type Person{
name: string
age: number
}
interface IPerson{
[name: string]: Person
}
let p: IPerson = {
'Ryuko': {
name: 'Ryuko',
age: 1
}
}
总结:当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。也就是说用 1(一个number)去索引等同于使用”1″(一个string)去索引,因此我们可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
6.1 相关案例
// ok
interface Foo {
[index: string]: number;
x: number;
y: number;
}
// wrong
interface Bar {
[index: string]: number;
x: number;
// 类型“string”的属性“y”不能赋给“string”索引类型“number”。ts(2411)
y: string;
}
接口Bar中采用了string
类型索引,所以内部属性可以写为x,y,z,xxxx...
等字符串,他们的值都应该声明为number类型。
更正方法:
// ok
interface Bar {
[index: string]: number;
x: number;
// 保证和索引数据一致
y: number;
}
// ok
interface Bar {
[index: string]: number | string;
x: number;
y: string;
}
七、对象的类型为 “unknown”。ts(2571)
在一些兑换码场景,经常会需要将兑换码全部转为大写,之后再进行判断:
function isString(s: unknown): boolean {
return typeof s === 'string'
}
function toUpperCase(x: unknown) {
if(isString(x)) {
// 对象的类型为 "unknown"。ts(2571)
x.toUpperCase()
}
}
可是在上一行明明已经通过 isString()
函数确认参数 x 为 string 类型了啊?
原因在于:即使第六行进行了字符串判断,在初次类型检查的时候,编译器看到第七行的x.toUpperCase()
,仍会判定x是unkown
类型。
解决办法:采用is
关键字。
is
关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。通过 is 关键字将函数参数类型范围缩小为 string 类型
function isString(s: unknown): s is string {
return typeof s === 'string'
}
function toUpperCase(x: unknown) {
if(isString(x)) {
x.toUpperCase()
}
}
八、类型“T”上不存在属性“length”。ts(2339)
在遇到泛型的时候,有时候我们需要传递一个字符串给函数,但是对于函数来说,参数a仅仅是一个T类型数据,编译器并不知道参数a身上有length属性。
// 类型“T”上不存在属性“length”。ts(2339)
function test<T>(a: T){
console.log(a.length);
}
所以在这个时候可以考虑使用类型约束
来解决该问题:
interface lengthConfig{
length: number
}
// 类型“T”上不存在属性“length”。ts(2339)
function test<T extends lengthConfig>(a: T){
console.log(a.length);
}
九、成员 “T” 隐式具有 “any” 类型,但可以从用法中推断出更好的类型。ts(7045)
目前来看,遇到这种错误的时候只能说多试试…
以这个例子来说,Person接口的函数返回类型声明为T会报错,但ArrayFunc接口不会
interface Person<T,U> {
name: T,
age: U,
// 应为“=>”。ts(1005)
say: ()=> T
}
interface ArrayFunc<T> {
(length: number, value: T): T[]
}
// 如果已经声明了函数类型,在定义函数的时候不必声明参数类型
const createArrayFunc: ArrayFunc<string> = (length, value) =>{
return ['1','2']
}
十、不能将类型“() => string”分配给类型“Func”。ts(2322)
interface Func{
print(): string
}
// 不能将类型“() => string”分配给类型“Func”。ts(2322)
const helloFuncWrong: Func = function(){
return "Ryuko"
}
// 如果一定要添加函数名,那么就应该对照着接口中的模式来写
const helloFuncOk: Func = {
print() {
return "Ryuko"
}
}
一般情况下,函数式接口中,(其实type类型也是一样),不能添加具体的函数名:
interface Func{
(): string
}
const helloFunc: Func = function(){
return "Ryuko"
}
10.1 相关案例
其实关于2322报错,主要可以理解为:设定了某个类型的数据,但是你在使用的时候却为他赋值为其他类型的数据
let arr: {id: number}[] = []
// ok
arr.push({id: 1})
// wrong
arr.push([{id:2, age: 1}])
以这个来说,定义的arr为包含着id对象的数组,可是第七行却赋值为包含{ id,age }类型的对象数组,现在有两种解决办法:
-
改变第一行arr类型定义
-
由数据可以由大赋值给小的理念来看,我们可以将
any
类型的数据赋值给arr对象:// ok arr = [{id:2, age: 1} as any]
十一、对象的类型为 “unknown”。ts(2571)
在请求数据的时候,我们常常会有这样的操作:
<script lang='ts' setup>
import { axiosGet } from '@/utils/http'
import { reactive } from 'vue'
let state = reactive({})
async function getImgList() {
let result = await axiosGet("/api/getlunbo")
+result.status === 0 ? state.imgList = result.message : ""
}
getImgList()
</script>
如果这段代码没有采用ts类型检测,可以顺利运行,流程为:向接口发起请求,将接口返回的数据赋值给state.imgList。
但是,这里有ts类型检测,请求结果返回的promise.then的结果并没有任何类型声明,所以编译器并不知道result身上存在什么属性,于是发出报错:对象(result)的类型为unknown。
这时候我们就需要手动来为返回值设置类型了,先看看接口格式:
{
status: 0,
message: [
{
url: "http://www.baidu.com",
img: "http://img2.imgtn.bdimg.com/it/u=500808421,1575925585&fm=200&gp=0.jpg"
},
{
url: "http://www.qq.com",
img: "http://p16.qhimg.com/bdr/__85/d/_open360/fengjing34/ABC8cbd.jpg"
}
]
}
根据接口类型,在type.d.ts
类型声明文件中定义,然后在代码中为返回值设置类型转换:
interface ResultType {
status: number,
message: Array<any>
}
declare namespace HOME {
interface StateType {
lunboList: {
img?: string,
url?: string
}[]
}
}
接下来只需要为数据添加类型就可以了:
- 将result返回值数据手动断言为
ResultType
类型 - 为state对象中的数据绑定对象成员类型
<script lang='ts' setup>
import { axiosGet } from '@/utils/http'
import { reactive } from 'vue'
let state = reactive<HOME.StateType>({
lunboList: []
})
async function getImgList() {
let result = await axiosGet("/api/getlunbo") as ResultType
// 需要注意的是,这里的result.message是any类型的数组
// 可以直接将 any[] 赋值给 HOME.StateType 下的lunboList[]
// 这是因为 any 类型的数据可以赋值给任意类型,因为**多属性数据可以赋值给少属性数据**
result.status === 0 ? state.lunboList = result.message : ""
}
getImgList()
</script>
参考文章
TypeScript 高级技巧
TypeScript 联合类型