typescript then(TypeScript 报错汇总)
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 httpMethods1.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 联合类型创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!