TypeScript 的好处
让你的程序是确定性的,比如参数类型、返回类型
提升性能,因为没有了类型判断的 if 逻辑
强化数据结构的概念
对重构很友好
环境搭建 普通 TS 项目可以通过这个模板创建:
https://github.com/kbysiec/vite-vanilla-ts-lib-starter
其他类型的项目,可以参考 Vite 的各种模板:
https://github.com/vitejs/awesome-vite#templates
tsconfig.json To Be Read:
One Thing Nobody Explained To You About TypeScript
类型 基础类型 原始类型 number, string, boolean
数据结构的声明会自动提升,所以你可以先使用变量,后定义变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function dataType (userName: string ): string { let age : number = 12 ; let isStudent : boolean = true ; let name : string = userName; let u : undefined = undefined ; let n : null = null ; return 'Hello, ' + name; }function anyType ( ) { let a1 : any = 1 ; let a2 : any = 'abc' ; console .log (a1.what ); }function returnVoid ( ): void { let unusable1 : void = null ; let unusable2 : void = undefined ; }function unionType ( ) { let myFavoriteNumber : number | string | boolean; myFavoriteNumber = 3 ; myFavoriteNumber = 'hello' ; myFavoriteNumber = true ; }
数组类型 number[], Array
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 let ages : number[] = [1 , 2 ] ages.push (3 )let numbers : Array <number> = [1 , 2 , 3 ] interface NumberArray { [index : number]: number; }function argTest ( ) { let args : IArguments = arguments ; }let prices : string[][] = [];let prices : Array <Array <string>> = new Array <Array <string>>();export type ChartDataSet = Array <{ name : string; date : number; value : string; }>; interface DataPoint { x : string; y : number; date : string; }let dataPoints : DataPoint [][];let dataPoints : Array <{ x : string; y : number; date : string; }[]>;let dataPoints : Array <Array <{ x : string; y : number; date : string }>>;
元组类型 [number, string]
仅用于数组中存放不同数据类型。
因为数组的数据存储空间小一些,所以坐标会用到数组,比如:position: [1, 1]这种;其他场景,一般我们用键值对对象。
注意元组是限定了数组的长度的。
Sum Type 在编程语言和类型理论中,”Sum Type”(求和类型或和类型)是一种复合类型,它允许一个变量可以是多种类型中的一种。它通常与”Product Type”(乘积类型)相对比,后者允许一个变量同时具有多种类型的值。
Sum Type 的一个典型例子是枚举(enum)类型。在许多编程语言中,枚举允许你定义一个类型,它可以是一组预定义值中的任何一个。例如,在 C 或 C++中:
1 enum Color { RED, GREEN, BLUE };
这里的 Color 就是一个 Sum Type,因为它可以是 RED、GREEN 或 BLUE 中的任何一个值。
在函数式编程语言中,Sum Type 通常以代数数据类型(Algebraic Data Type,ADT)的形式出现,例如 Haskell 中的 Either a b 类型,它可以是类型 a 或类型 b:
1 data Either a b = Left a | Right b
这里的 Either a b 可以持有一个 Left 值,其类型为 a,或者一个 Right 值,其类型为 b。
Sum Type 在类型安全、模式匹配和错误处理等方面非常有用,因为它们允许程序员明确地表达一个值可能具有的不同形式,并且可以在编译时检查这些形式。
在 TypeScript 中,Sum Type 可以通过联合类型(Union Types)和枚举(Enums)来实现。以下是一些例子:
联合类型(Union Types)
联合类型允许你将多个类型组合成一个类型,表示一个值可以是几种类型之一。
1 2 3 4 5 6 7 8 9 10 type StringOrNumber = string | number ;function printId (id: StringOrNumber ) { console .log (`ID: ${id} ` ); }printId (101 ); printId ('202' ); printId (true );
枚举(Enums)
枚举是一种特殊的类型,它允许你为一组数值赋予更易读的名字。
1 2 3 4 5 6 7 8 9 10 11 12 13 enum Color { RED = 1 , GREEN = 2 , BLUE = 4 , }function printColorName (color: Color ) { console .log (`Color: ${Color[color]} ` ); }printColorName (Color .RED ); printColorName (3 );
代数数据类型(Algebraic Data Types,ADTs)
虽然 TypeScript 原生不支持代数数据类型,但你可以使用接口和类型别名来模拟它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 interface Success <T> { kind : 'Success' ; value : T; }interface Error { kind : 'Error' ; message : string ; }type Result <T> = Success <T> | Error ;function getResult (value: number ): Result <number > { if (value > 0 ) { return { kind : 'Success' , value }; } else { return { kind : 'Error' , message : 'Value must be positive' }; } }const result = getResult (-1 );if (result.kind === 'Success' ) { console .log (`Result: ${result.value} ` ); } else { console .log (`Error: ${result.message} ` ); }
可辨识联合(Discriminated Unions)
通过在联合类型中使用一个共有的属性来区分不同的类型,可以创建可辨识联合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 interface Square { kind : 'square' ; sideLength : number ; }interface Circle { kind : 'circle' ; radius : number ; }type Shape = Square | Circle ;function calculateArea (shape: Shape ) { switch (shape.kind ) { case 'square' : return shape.sideLength * shape.sideLength ; case 'circle' : return Math .PI * shape.radius * shape.radius ; } }const square : Square = { kind : 'square' , sideLength : 4 };const circle : Circle = { kind : 'circle' , radius : 5 };console .log (calculateArea (square)); console .log (calculateArea (circle));
枚举类型 enum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 enum DayOfWeek { Sunday , Monday , Tuesday , Wednesday , Thursday , Friday , Saturday , }enum FileState { Read = 1 , Write = 2 , ReadWrite = 3 , }
为什么不推荐用枚举? TypeScript —— 枚举类型 enum 的红与黑 - Wise.Wrong - 博客园
TypeScript = JavaScript + Types
TypeScript 设计的初衷是 JavaScript + Types,所有 TypeScript 的特性不改变运行时的行为
反过来说,如果在 TS 代码中去掉静态类型,应该得到一份完整有效的 JS 代码
这样的好处在于,我们可以通过 ESbuild 而不是 tsc 完成我们的 TS 代码到 JS 代码的转换
但实际上 TypeScript 中有一个特殊类型破坏了这种构想,它就是 Enum
比如这一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 enum Language { ZH_CN = 'zh_CN' , ZH_HK = 'zh_HK' , ZH_TW = 'zh_TW' , EN_US = 'en_US' , EN_GB = 'en_GB' , }function getLocals (lang: Language ) { return `hello ${lang} ` ; }getLocals (Language .ZH_CN );
1、由于 enum 可以当做对象使用,所以如果删掉 Language,这段代码就无法运行
2、在作为静态类型使用的时候,enum 还会带来额外的心智负担,上面的 Language 如果换成联合类型的写法,可能更符合直觉:
1 type Language = 'zh_CN' | 'zh_HK' | 'zh_TW' | 'en_US' | 'en_GB' ;
3、由于使用了 enum,我们不得不使用 tsc 而非 ESbuild 来编译项目,导致整个编译过程的开销巨大。
(精)联合类型 number | string
1 2 let a : string | number = '123' ; a = 123 ;
(精)基于联合类型实现的高级数据结构:
https://blog.csdn.net/dajuna/article/details/117958613
比如 Record、Partial、Required、Pick、Readonly、Exclude、Omit、ReadonlyArray
联合类型是让类型具备不确定性 ;而基于联合类型实现的高级数据结构,则是让数据类型具备(一定的)确定性 。
任意类型 any
未知类型 unknown
空值类型 void, null, undefined
字面量类型 如 ‘literal’, 42
交叉类型 TypeA & TypeB
将多个类型合并为一个类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface IPerson { id : string ; age : number ; }interface IWorker { companyId : string ; }type IStaff = IPerson & IWorker ;const staff : IStaff = { id : 'E1006' , age : 33 , companyId : 'EXE' , };console .dir (staff);
TypeScript 中的 & 运算符是什么 - 咱这个需求做不了 - 博客园
对象 定义任意属性 1 2 3 4 5 6 7 8 9 interface Person { name : string , [propName : string ] : any ; }let zhangsan : Person { name : 'zhangsan' , gender : 'male' };
注意这种情况:
1 2 3 4 5 6 7 8 9 10 interface Person { name : string , age : number , [propName : string ] : any ; }let zhangsan : Person { name : 'zhangsan' , age : 25 , gender : 'male' };
这时候你运行代码,会发现报错了:’Property ‘age’ of type ‘number’ is not assignable to string index type ‘string’.’,这报错的意思,我们从字面理解的话就是:’类型为“number”的属性“age”不能分配给字符串索引类型“string”。’,我们再深入理解,也就是属性 age 的 number 属性不是 string 类型的子属性。这样就很奇怪了,为什么会报这个错误?
我们可以看下官方文档,原来如果我们在定义接口对象时定义了任意属性,那么这个接口对象内其余的属性就必须是任意属性的子属性 ,因为我们定义了一个 age 属性,它的类型是 number 类型的,而 number 类型不是 string 类型以及它的子类型,所以编译时就报错了。 ———————————————— 版权声明:本文为 CSDN 博主「Crimaster·W」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Kreme/article/details/102699712
缺省属性 通过问号指定:
1 2 3 4 5 6 7 8 9 10 interface Person { name : string , age : number , gender ? : string }let zhangsan : Person { name : 'zhangsan' , age : 25 }
只读属性 通过 readonly 关键词来限定:
1 2 3 4 5 6 7 8 9 10 11 interface Person { name : string ; age : number ; readonly id : number ; }let zhangsan : Person = { name : 'Tom' , age : 25 , id : 1 , };
键名为多种类型 比如这样的数据(键名可能为 string,也可能为 number):
1 2 3 4 { 2021 : 123 , '2022' : 456 , }
可以这样定义:
1 2 3 4 {[key : string | number ]: number | string }Record <string |number , string >
高级类型 泛型 <T>
泛型就是当你定义的时候,不确定后面真正使用的时候会是什么数据类型。泛型可以极大增强程序的扩展性。
<T>这里的 T 是 type 的缩写
泛型函数的结构 泛型函数一般按照这样的顺序来编写:
1 2 3 4 5 6 7 function 函数名<定义泛型参数>(函数参数): 函数返回类型 { 函数主体; }const 函数名 = <定义泛型参数>(函数参数): 函数返回类型 { 函数主体; }
一个案例:
1 2 3 4 5 6 function functionName<T>(age : T): T { console .log (age); }const functionName = <T>(age : T): T { console .log (age); }
定义与使用 泛型的应用,包括了定义和使用两部分。在尖括号中的是定义,后面的是使用,比如:
1 2 3 4 5 6 7 // <T, U>是定义泛型参数 // [T, U]和[U, T]是使用泛型参数 function swap<T, U>(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; } swap([7, 'seven']); // ['seven', 7]
约束 这是为了限制参数类型,便于我们在函数内部可以使用特定类型的特定属性/方法(比如下面的 length 属性)。
泛型参数的类型,通过 extends 关键词进行约束:
1 2 3 4 5 6 7 8 9 10 interface Lengthwise { length : number ; }function loggingIdentity<T extends Lengthwise >(arg : T): T { console .log (arg.length ); return arg; }
再看一个例子:
1 2 3 4 function loggingIdentity<K extends keyof T>(arg : K): T { console .log (arg.length ); return arg; }
这个 K extends keyof T,应该是说 key 被约束 在 T 的 key 中,不能超出这个范围,否则会报错的。
在泛型中使用 extends 并不是用来继承的,而是用来约束类型 的。
PS: 泛型可以大量应用到我们的可视化组件中。
比如下面这样:
1 2 3 4 5 class BaseBarSeriesModel < Opts extends BaseBarSeriesOption <unknown > = BaseBarSeriesOption <unknown > > extends SeriesModel <Opts > {}
映射类型 { [K in keyof Type]: Transform<Type[K]> }
条件类型 T extends U ? X : Y
工具类型 内置的高级类型辅助工具
typescript 工具类型 Partial、Pick、Omit - Reminisce* - 博客园
Partial 将 Type 的所有属性设置为可选。
将已声明的类型中的所有属性标识为可选的。
1 const usePartial : Partial <Person > = {};
Required 将 Type 的所有属性设置为必须。
Readonly 将 Type 的所有属性设置为只读。
Record<Keys, Type> 构造一个对象类型,其属性键是 Keys,属性值是 Type。
Pick<Type, Keys> 从 Type 中选取一组属性 Keys。
从一个复合类型中,取出几个想要的类型的组合
1 2 3 4 5 6 7 8 9 10 11 12 13 interface TState { name : string ; age : number ; like : string []; }interface TSingleState { name : string ; age : number ; }interface TSingleState extends Pick <TState , 'name' | 'age'> {}
比如 ZRender 中的应用:
1 2 3 4 5 export type ElementStatePropNames = | (typeof PRIMARY_STATES_KEYS )[number ] | 'textConfig' ;export type ElementState = Pick <ElementProps , ElementStatePropNames > & ElementCommonState ;
Omit<Type, Keys> (忽略、删除) 从 Type 中排除一组属性 Keys。
从声明的类型中删除指定的属性生成新的类型。
1 type Omit <T, K extends keyof any > = Pick <T, Exclude <keyof T, K>>;
Omit 方法中使用了Exclude和Pick方法。结合一个小例子理解这个方法。
1 2 3 4 5 type OmitPerson = Omit <Person , 'name' >;const useOmit1 : OmitPerson = { age : 10 , hobby : 'swim' };
Exclude<Type, ExcludedUnion> 从 Type 中排除在 ExcludedUnion 中的类型。
排除不需要的属性,最后的到的是排除后的属性,常用来配合其他的方法使用。
和 Pick 的作用相反。
1 2 3 4 type PersonKeys = keyof Person ; type Age = Exclude <PersonKeys , 'name' | 'hobby' >"
从 Type 中提取所有可以赋值给 Union 的类型。
ReturnType 获取函数 Type 的返回类型。
InstanceType 获取构造函数类型 Type 的实例类型。
用户自定义类型 接口 interface
相比 Java,还能定义属性,也能定义方法
接口可以定义重名的,会自动合并,常用于(也仅用于)扩展现有接口;这个设计存疑
–TS 不让通过 Date.prototype.xxx 去扩展现有内容,只能通过接口扩展的方式实现
接口也可以继承接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 interface Person { name : string; age?: number; [propName : string]: any; readonly sex : string; }let zhou : Person = { name : "zhou" , score : 3 , sex : "male" }
类型别名 type
1 type pos = [number , number ];
这个非常有用,比如我们的可视化组件对入参的检查,就可以用 type。
一些常用的交叉类型:
1 2 3 type Text = string | { text : string };type Coordinates = [number , number ];type Callback = (data: string ) => void ;
类型操作符和关键字 typeof 类型操作符 typeof 操作符属于 类型查询 的一种。类型查询用于获取一个变量或对象的类型。这在 TypeScript 中是一种常用的方法,用于从已有的变量中推导出类型。
例如,如果你有一个已经定义的对象或变量,你可以使用 typeof 来获取它的类型,并将这个类型赋值给另一个变量或者用作其他类型的定义。 typeof 在 TypeScript 中主要用于类型推断和重用已有变量或对象的结构作为类型,这有助于减少冗余代码并提高类型系统的一致性。
keyof 类型操作符 keyof 类型操作符用于获取某个类型的所有键名作为联合类型。例如,如果你有一个类型 { a: number; b: string; },那么 keyof 这个类型会产生'a' | 'b'这样的联合类型。
instanceof 关键字 虽然 instanceof 主要用于运行时检查对象是否为特定类的实例,但在类型保护中,它也可以用来缩小类型的范围。
类型断言 类型断言(如 as 关键字)用于手动指定一个更具体的类型。例如,someVar as string 告诉 TypeScript someVar 应该被视为 string 类型。
类型保护函数 类型保护通过定义一个函数来检查一个变量是否属于某个特定类型。例如,function isString(test: any): test is string { return typeof test === “string”; }。
条件类型 条件类型(TypeA extends TypeB ? X : Y)用于根据类型关系选择类型。它们在高级类型构建时非常有用。
泛型类型参数:泛型(如 )用于定义灵活的类型,它们可以在多个位置以不同的方式被使用。
基本概念 类型推断 TS 可以自动推断类型,帮助我们简化编码;但是一般不推荐用类型推断,我们应该明确指定变量类型。
类型断言 应用第三方东西的时候很有用,可以作为 TS 和第三方衔接的一个桥梁。
可以用尖括号<>,也可以用as关键词:
1 const div = document .getElementById ('abc' ) as HTMLElement ;
1 2 3 4 5 6 7 function getLength (something: string | number ): number { if ((<string>something).length ) { return (<string>something).length ; } else { return something.toString ().length ; } }
function 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 function sum (a: number, b: number ): number { return a + b }let sum1 = function (a: number, b: number ): number { return a + b }let sum2 : (x: number, y: number ) => number = function (x: number, y: number ): number { return x + y } interface SearchFunc { (source : string, subString : string): boolean }let mySearch : SearchFunc mySearch = function (source: string, subString: string ): boolean { return source.search (subString) !== -1 }function sum3 (a: number, b: number, c?: number ): number { let sum : number = a + b if (c) { sum += c } return sum }function sum4 (a: number, b: number, c: number = 3 ): number { let sum : number = a + b if (c) { sum += c } return sum }function sum5 (a: number, b: number, ...numbers: number[] ): number { let sum : number = a + b if (numbers) { for (let i = 0 ; i < numbers.length ; i ++) { sum += numbers[i] } } return sum }function reverse (x: number ): number;function reverse (x: string ): string;function reverse (x: number | string ): number | string { if (typeof x === 'number' ) { return Number (x.toString ().split ('' ).reverse ().join ('' )); } else if (typeof x === 'string' ) { return x.split ('' ).reverse ().join ('' ); } }
命名空间 namespace
声明文件
如果程序中使用了第三方的库,比如 jQuery,也需要进行声明文件的编写,否则会提示变量 jQuery 不存在。
typeof typeof 可以获取一个具体数据实例的数据结构。
这在提取第三方数据或者接口的数据结构时很有用。
(TODO)类型查找 多用于第三方接口的使用;自己写的还是全部用定义
keyof 获取数据定义的键名。
比如 on()事件的案例:我们想根据传入的不同的事件类型(PC 端是 click,移动端是 move),进行不同的第二个参数的提示:
1 function on<K extends keyof EventParam >(key : K, callback (e :EventParam [K]) => void )
应用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 export type StateDefinition = { [k : string ]: unknown ; material : { [k : string ]: unknown }; };export default class Element < O extends Object3D = Mesh , S = { [k : string ]: StateDefinition } > { useState (stateName: keyof S ) { if (this .isNull ()) { return ; } const stateObject = this ?._stateProxy ?.(stateName, this .currentStates ) || this ._states [stateName] || ({} as S[keyof S]); this .currentStates = [stateName] as unknown as keyof S[]; this ._applyStateObject (stateObject); } }
缺省(?)与不可或缺(!) 变量后面加问号是缺省
变量后面加惊叹号是肯定存在
(TODO)装饰器模式 给第三方类扩展属性 参考这个文章:https://blog.csdn.net/palmer_kai/article/details/107687717
注意增加的.d.ts 文件,是不需要主动 import 的,直接就会识别加载。
另外这个文章顶部从顶级模块引入了需要扩展的子模块,实际上是不需要引入的,参考我扩展 three.js 的 Group 案例,就没有 import Group 进来:
1 2 3 4 5 6 7 8 9 10 import { Node } from './type/Data' ;declare module 'three' { export interface Group { node : Node ; } }
然后发现有新问题:我想使用原始的 Group 功能的时候,会报错:**”Group”仅表示类型,但在此处却作为值使用。**
1 2 const helpers = new Group ();
没找到好的扩展 Class 的方法,最终我选择重新定义 Group 这个类 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { Object3D } from 'three' ;import { Node } from './type/Data' ;declare module 'three' { export class Group extends Object3D { constructor ( ); type : 'Group' ; readonly isGroup : true ; node : Node ; } }
参考资料:JS 模块增强
一些 Q&A type 和 interface 的区别 TypeScript type 和 interface 区别_天渺工作室的博客-CSDN 博客_typescript 中 type 和 interface 的区别
type 和 interface 都用于定义类型,但它们之间存在一些关键的区别和使用场景:
扩展性 :
interface 可以被无限次地扩展(extended),并且可以在多个地方声明后合并为一个接口。
type 只能被扩展一次,不能在多个地方声明合并。
原始类型别名 :
type 可以用来给原始类型创建别名,例如 type Name = string;。
interface 不能用来给原始类型创建别名,它只能用于对象类型。
交叉类型 :
type 可以用来创建交叉类型,例如 type Person = { name: string } & { age: number };。
interface 也可以通过 extends 关键字实现类似的功能,但语法不同,例如 interface Person extends Name, Age { }。
重声明 :
interface 可以被重声明,并且重声明的接口会合并。
type 重声明会报错,因为 TypeScript 会将它们视为不同的类型。
索引签名 :
interface 可以包含索引签名,例如 interface StringArray { [index: number]: string; }。
type 也可以包含索引签名,但是使用 type 时,索引签名必须是对象类型的一部分。
构造签名 :
type 可以为函数或构造函数创建一个签名,例如 type Newable = new (...args) => Instance;。
interface 也可以定义构造函数签名,但是通常用于类或对象的上下文中。
类型守卫 :
type 可以用于类型守卫,例如使用 typeof 或 instanceof 操作符。
interface 通常不用于类型守卫。
命名约定 :
interface 通常用于定义对象的形状,遵循 PascalCase 命名约定。
type 可以用于更广泛的类型定义,包括联合类型、元组、枚举成员等,命名约定更灵活。
总结来说,interface 更适合用于定义“结构契约”,而 type 提供了更广泛的类型系统功能,包括原始类型别名、交叉类型、更复杂的类型操作等。在实际开发中,根据具体需求和场景选择合适的类型定义方式。
any VS. unknown https://www.zhihu.com/question/355283769/answer/2136229141
any 相当于直接暴力通过类型检查,打这张牌要警惕。
unknown 是所有类型的父类型(top type ),想强行 as 的时候更建议打这张牌。
as 原则上只能转换存在子类型关系的两个类型,且一次只能转换一层
any 能最简单暴力地解决问题,所以初学者普遍打 any 牌不打 unknown 牌。
在 TS 这样支持 interface 组合的语言里,业务逻辑中的类型所构成的应当不是树而是个有向无环图 ,子类型关系也可以视为一种偏序关系 。像这样:
TS 类型体操入门:
https://zhuanlan.zhihu.com/p/384172236
常见问题 编译报错 如何忽略编译报错? 有时候我们修改老项目,紧急发布的时候没时间一个一个编译问题都改掉,需要忽略编译报错。
单文件中,可以通过如下注释来处理:
如果想全局设置,可以修改 tsconfig.json,增加这 2 项:
1 2 3 4 5 6 7 { "compilerOptions" : { "strict" : true , "skipLibCheck" : true } }
TypeScript 版本过低导致的报错 datav-server 项目中,本地开发没问题,部署到服务器上,tsc 编译就报错了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 node_modules/@types/express-serve-static-core/index.d.ts:1163:17 - error TS1005: ';' expected. 1163 /** Enable `setting`. */ ~~~~~~~ node_modules/@types/express-serve-static-core/index.d.ts:1166:18 - error TS1005: ';' expected. 1166 /** Disable `setting`. */ ~~~~~~~ node_modules/@types/express-serve-static-core/index.d.ts:1170:31 - error TS1005: ';' expected. 1170 * Render the given view `name` name with `options` ~~~~ node_modules/@types/express-serve-static-core/index.d.ts:1170:48 - error TS1005: ';' expected. 1170 * Render the given view `name` name with `options` ~~~~~~~ node_modules/@types/express-serve-static-core/index.d.ts:1186:16 - error TS1005: ';' expected.
去这个包的 issue 中找了下,看到有类似问题:
https://github.com/DefinitelyTyped/DefinitelyTyped/issues/53397
可能和我们 ts 版本过低有关系,package.json 中是:
打印了版本看:
1 2 3 4 tsc -v# 输出 Version 3.9.10
然后通过升级项目下的 package.json 中的 TypeScript 的版本 进行了解决。
TS7006: Parameter ‘_‘ implicitly has an ‘any’ type. 修改 tsconfig.json,将noImplicitAny设置为false
Cannot find name ‘Map’. Do you need to change your target library? Try changing the ‘lib’ compiler option to ‘es2015’ or later. 第一种方法:
修改 tsconfig.json,添加 lib 配置项:
1 2 3 4 5 6 7 8 9 10 11 { "compilerOptions" : { "outDir" : "./dist/" , "sourceMap" : true , "noImplicitAny" : false , "module" : "CommonJS" , "target" : "es5" , "allowJs" : true , "lib" : [ "ESNext" ] } }
第二种方法:
1 npm install -D @types/node
TS2304: Cannot find name ‘HTMLDivElement/window/document’ 修改 tsconfig.json,给 lib 配置项添加 DOM 依赖:
1 2 3 4 5 6 7 8 9 10 11 { "compilerOptions" : { "outDir" : "./dist/" , "sourceMap" : true , "noImplicitAny" : false , "module" : "CommonJS" , "target" : "es5" , "allowJs" : true , "lib" : [ "ESNext" , "DOM" ] } }
为什么之前运行正常的 egg.js 项目,过一段时间就会编译报错? 一开始怀疑是因为项目提交了 package-lock.json,部分包过期了,后来发现不是这个原因(这个逻辑上也说不通)
和 node 版本有关, 报错的环境:v8.9.3
升级到:v12.18.3
TS 解释器在不同的 Node 版本下,行为/能力不一致。
循环依赖 在只需要使用定义的文件中,引入时加上type:
1 import type BarChart from '../chart/BarChart' ;
Error: ‘ChartOption’ is not exported by src/chart/util/structure.ts, imported by src/chart/spiral/Chart.ts 如果你看自己程序中已经确实 export 了,但是仍然报这个错误,那么可能也是因为循环依赖导致的,加上 type 即可。
未声明时使用类 将类名改为 this:
1 2 3 4 5 6 7 8 9 class BarChart extends BaseChart { xPeriodScale : XPeriodScale <BarChart >; }class BarChart extends BaseChart { xPeriodScale : XPeriodScale <this >; }
export 的 interface 找不到 报类似这样的错误:
“export ‘state’ (imported as ‘stateModel’) was not found in ‘../store/state.ts’
解决方案就是:单独用一个文件导出 interface:
https://github.com/angular/angular-cli/issues/2034#issuecomment-302666897
node_modules/@types/express-serve-static-core/index.d.ts:917:14 - error TS1005: ‘;’ expected. 完整报错类似这样:
1 2 node_modules/@types/express-serve-static-core/index.d.ts:917:14 - error TS1005: ';' expected. 917 * - `path` defaults to "/"
其实并不是因为缺少分号,而是因为项目引用的 TS 版本过低,无法识别模板字符串导致的。
解决方案:升级项目依赖的 TS 版本即可。
这是因为某一个语句之后缺少分号,导致无法正常识别语法,比如类似这样的:
1 2 3 4 5 6 7 const config = { name : '张三' , }( this .mysql as any ).query ();
解决方案很简单,加个分号即可。
error TS1192: Module ‘“http”‘ has no default export 一些系统自带的包报错,这是因为 http 模块没有默认导出。事实上,http 模块的确没有默认导出。因为 http 是遵循 cjs 规范写的。即类似于这种导出:
1 2 3 4 5 module .exports = http = { method1, method2, ... }
解决方案:
修改 TS 的配置,加上这个:
1 2 3 4 5 { "compilerOptions" : { "esModuleInterop" : true } }
给第三方类扩展属性时,提示“扩大中的模块名称无效。模块“THREE”解析到位于 处的非类型化模块,其无法扩大” 需要把 import 放在 declare 内部:
1 2 3 4 5 6 declare module 'THREE' { import * as THREE from 'three' ; export interface THREE { onEvent : () => void ; } }
延迟赋值的属性,在调用时提示可能为空的问题 通过**非空断言(!)**解决。
可以在定义属性类型的时候添加,比如这样:
1 2 3 4 5 6 class Chart { scene!: Scene ; constructor (dom: HTMLDivElement, option: ChartOption ) { } }
也可以在调用的地方使用,比如这样:
给部分属性赋予默认值 比如这样一个结构:
1 2 3 4 5 6 7 8 const data : { date : string ; open : number ; high : number ; low : number ; close : number ; change : number ; }[] = [];
在 Typescript 中,接口只是描述对象的外观,不能包含任何函数 。因此,您不必像使用类那样构造接口。正因为如此,你不能在接口中进行任何初始化。
子类重新定义属性后,constructor 中直接使用父类赋值的属性报 undefined 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Father { name : string ; constructor (name: string ) { this .name = name; } }class Child1 { name : string ; constructor (name: string ) { super (name); console .log (this .name ); } }class Child2 { constructor (name: string ) { super (name); console .log (this .name ); } }
所以想要使用父类的属性,可以有两个方法:
1、子类中不要定义该属性
2、父类定义时,使用泛型
子类属性的初始化时机-父类调用子类属性报 undefined 的问题
这个和 TS 没关系,是 JS 的特性。
父类:
1 2 3 4 5 6 7 class Father { constructor ( ) { this .init (); } init ( ) {} }
子类:
1 2 3 4 5 6 7 8 9 10 class Child extends Father { name = 'child' ; init ( ) { console .log (this .name ); } }new Child ();
目前采用的解决方法是将子类的该属性设置为static属性:
1 2 3 4 5 6 7 8 9 10 class Child extends Father { static name = 'child' ; init ( ) { console .log (Child .name ); } }new Child ();
引入的第三方库,明明声明了定义,但是仍然报错 比如这次用 llama index,就遇到这个问题:
1 2 3 4 const prompt = new PromptTemplate ({ template : raw_prompt[0 ].prompt , templateVars : raw_prompt[0 ].variable , });
报错:
error TS2554: Expected 0 arguments, but got 1.
问题原因尚未查明,然后临时先通过覆盖定义的方式解决了,即写了个/projectPath/typings/app/llamaindex.d.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import { PromptType } from 'llamaindex' ;declare module 'llamaindex' { interface PromptTemplateOptions < TemplatesVar extends readonly string [], Vars extends readonly string [], Template extends StringTemplate <TemplatesVar > > { template : Template ; templateVars : Vars ; promptType?: PromptType ; } class PromptTemplate < TemplatesVar extends readonly string [] = string [], Vars extends readonly string [] = string [], Template extends StringTemplate <TemplatesVar > = StringTemplate <TemplatesVar > > extends BasePromptTemplate <TemplatesVar , Vars > { #template : Template ; promptType : PromptType ; constructor (options: PromptTemplateOptions<TemplatesVar, Vars, Template> ); } }
装饰器的 return 类型和被装饰方法的 return 类型不匹配的问题 1 2 3 4 5 6 7 @wrapLLMEvent async chat ( params : LLMChatParamsStreaming | LLMChatParamsNonStreaming , ): Promise <AsyncIterable <ChatResponseChunk > | ChatResponse <object >> { if (params.stream ) return this .streamChat (params); return this .nonStreamChat (params); }
error TS1270: Decorator function return type ‘(this: LLM<object, object>, params: LLMChatParamsStreaming<object, object> | LLMChatParamsNonStreaming<object, object>) => Promise<…>’ is not assignable to type ‘void | TypedPropertyDescriptor<{ (params: LLMChatParamsStreaming<object, object>): Promise<AsyncIterable<ChatResponseChunk>>; (params: LLMChatParamsNonStreaming<…>): Promise<…>; }>’.
TS5069: Option ‘declarationMap’ cannot be specified without specifying option “declaration’ or option ‘composite’. 使用storybook开发KAmis组件的时候出现的问题。 据说是TS版本的问题,通过升级/降级解决:https://github.com/cypress-io/cypress/issues/30016 https://github.com/ng-packagr/ng-packagr/issues/1464
技巧 将复杂的数据类型,都定义为 interface 优化前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 formateData ( dataArr : [] ): { date : string ; open : number ; high : number ; low : number ; close : number ; change : number }[] { const data : { date : string ; open : number ; high : number ; low : number ; close : number ; change : number ; }[] = []; dataArr.forEach ((datum, index ) => { data.push ({ date : datum[0 ], open : parseFloat (datum[1 ]), high : parseFloat (datum[2 ]), low : parseFloat (datum[3 ]), close : parseFloat (datum[4 ]), change : index > 0 ? parseFloat (dataArr[index][4 ]) - parseFloat (dataArr[index - 1 ][4 ]) : parseFloat (dataArr[index][4 ]) - parseFloat (dataArr[index][1 ]), }); }); return data; }
优化后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 interface Daily { date : string ; open : number ; high : number ; low : number ; close : number ; change : number ; } formateData ( dataArr : [] ): Daily [] { const data : Daily [] = []; dataArr.forEach ((datum, index ) => { data.push ({ date : datum[0 ], open : parseFloat (datum[1 ]), high : parseFloat (datum[2 ]), low : parseFloat (datum[3 ]), close : parseFloat (datum[4 ]), change : index > 0 ? parseFloat (dataArr[index][4 ]) - parseFloat (dataArr[index - 1 ][4 ]) : parseFloat (dataArr[index][4 ]) - parseFloat (dataArr[index][1 ]), }); }); return data; }
定义文件单独编写
思考:这个是不是和 C++很像?是否有共通之处?
定义文件一般都比较长,放在具体类里面,会让类文件很长。可以考虑独立用一个文件存放。
包括:
文件名大小写问题 类大写,工具函数文件小写
不要定义可选类型| 如果定义了可选类型,每次都要加感叹号(!),很不优雅,编辑器也会出现黄色提示。
如果该属性是个数组,那就定义为数组,然后没数据的就给个空数组好了。
如果是简单对象,就给个默认值,比如 0 之类的。
原则:数据结构应该是固定的,不可变的。
工具 (精)可视化展示继承关系 https://tsdiagram.com/
这个的代码也值得一看
TypeScript类型图解 https://types.kitlangton.com/
参考资料 入门教程文档:
http://ts.xcatliu.com/
你不知道到的 TS 泛型:
https://segmentfault.com/a/1190000022993503
通过 webpack 编译构建 TS:
https://www.jianshu.com/p/f6917e257b7a
用 unknown 替代 any:
https://www.jianshu.com/p/516fe7cbc9e8
官方文档:
https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html
TypeScript 最近各版本主要特性总结:
https://blog.csdn.net/qq_42427109/article/details/130533706
TypeScript 5 与 TypeScript4 区别:
https://www.toimc.com/typescript-5-vs-4/
优秀的 TS 项目-React 状态管理库:
https://github.com/pmndrs/zustand/
(TODO)type-challenges https://github.com/type-challenges/type-challenges
在线版:https://typeroom.cn/problems/all
好玩的项目 太空射击游戏:
https://itnext.io/building-a-space-shooter-game-with-html5-canvas-typescript-part-1-20663025c7f5
代码案例 参数类型的继承 1 class EventEmitter <EvtDef extends EventDefinition = EventDefinition >
EvtDef 是泛型参数的名称,它后面跟着 extends EventDefinition 表示 EvtDef 可以是任何继承自 EventDefinition 的类型,或者直接就是 EventDefinition 类型。泛型允许你创建可重用和灵活的组件。
事件处理器映射 1 type EventDefinition = Record <string , (...args: unknown [] ) => unknown >;
EventDefinition 被定义为一个对象类型,其中键(key)是字符串类型,值(value)是一个函数。这个函数可以接收任意数量的参数(参数数组 args),每个参数的类型是 unknown,并且这个函数的返回值类型也是 unknown。
这种类型通常用于定义事件处理器映射,其中每个事件名称(字符串)映射到一个事件处理器函数。这个函数可以接收任意的参数,并执行一些操作,但其返回值的类型没有具体限制。
例如:
1 2 3 4 5 6 7 8 9 10 const eventHandlers : EventDefinition = { click : (x: number , y: number ) => { console .log (`Clicked at (${x} , ${y} )` ); return 'Event handled' ; }, hover : (item: any ) => { console .log (`Hovered over ${item} ` ); }, };
导出类型别名 1 2 3 4 5 6 7 8 export type Query <H extends EventDefinition [string ] = EventDefinition [string ]> = | { [Key in keyof Parameters <H>[0 ]]: | Parameters <H>[0 ][Key ] | Parameters <H>[0 ][Key ][]; } | ((...args: Parameters<H> ) => boolean );
这段代码是 TypeScript 中的一个导出类型别名 Query 的声明,它使用了高级的泛型和类型映射特性。让我们逐步解析这个声明:
export type Query<H extends EventDefinition[string] = EventDefinition[string]>: 这是类型别名的声明,Query 是别名的名称,它是一个泛型类型,其中 H 是一个泛型参数,它默认继承自 EventDefinition[string]。这里的 EventDefinition[string] 指的是 EventDefinition 类型中作为值的函数类型。
= EventDefinition[string]: 这是 H 的默认类型参数,意味着如果使用 Query 时没有显式指定 H,则默认使用 EventDefinition 中的函数类型。
|: 这是 TypeScript 中的联合类型 操作符,表示类型可以是左边类型 A 或者右边类型 B。
左边类型 { [Key in keyof Parameters<H>[0]]: ... }:
使用了高级类型 Parameters 和 keyof。Parameters<H> 获取 H 函数的所有参数类型组成的元组类型。
keyof Parameters<H>[0] 获取 Parameters<H> 中第一个参数类型的所有键(属性名)。
Parameters<H>[0][Key] | Parameters<H>[0][Key][] 表示每个属性 Key 的类型是 Parameters<H>[0] 中相应属性的类型或者是该类型的数组。
右边类型 ((...args: Parameters<H>) => boolean): 这是一个函数类型,接受一个参数列表 args,这个参数列表的类型与 H 函数的参数类型相同,函数返回一个布尔值。
综合来看,Query 类型别名定义了一个类型,可以是:
一个对象,其键是 H 函数第一个参数类型属性的名称,值是该属性类型的值或者该类型的数组。
或者是一个函数,接受与 H 函数相同的参数,并返回一个布尔值。
例如,如果 EventDefinition 像前面的例子一样定义,并且有一个事件处理器:
1 2 3 4 type EventDefinition = { click : (x: number , y: number ) => void ; hover : (item: string ) => void ; };
那么 Query<EventDefinition['click']> 可以是:
1 2 3 4 { x : number | number []; y : number | number []; } | (...args: [number , number ] ) => boolean ;
这意味着 Query 可以是一个对象,有 x 和 y 属性,属性值可以是数字或者数字数组;或者是一个接受两个数字参数并返回布尔值的函数。
以下是一些具体的例子,展示了 Query 类型在不同场景下的应用:
参数对象类型定义 假设我们有一个函数,它需要一个配置对象来设置不同的选项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type EventDefinition = { setup : (config: { width: number ; height: number } ) => void ; };export type SetupQuery = Query <EventDefinition ['setup' ]>;function setup (setupConfig: SetupQuery ): void { if ( typeof setupConfig.width === 'number' && typeof setupConfig.height === 'number' ) { console .log ( `Setting up with width: ${setupConfig.width} and height: ${setupConfig.height} ` ); } else { throw new Error ('Invalid configuration object' ); } }setup ({ width : 800 , height : 600 });
函数重载 使用 Query 实现函数重载,接受单个参数对象或多个参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type EventDefinition = { process : (data: { item: string ; value: number } ) => boolean ; };export type ProcessQuery = Query <EventDefinition ['process' ]>;function process (data: ProcessQuery ): boolean { if (typeof data === 'function' ) { return data (); } return data.value > 0 ; }process ({ item : 'test' , value : 10 });process ((item: string , value: number ) => value > 0 );
数据验证 使用 Query 来验证函数参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type EventDefinition = { validate : (data: { id: string ; content: string [] } ) => boolean ; };export type ValidateQuery = Query <EventDefinition ['validate' ]>;function validate (data: ValidateQuery ): boolean { if (typeof data.id === 'string' && Array .isArray (data.content )) { return true ; } return false ; }console .log (validate ({ id : '123' , content : ['item1' , 'item2' ] }));
事件处理 定义事件处理器,使用 Query 来确保事件数据的正确性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type EventDefinition = { onClick : (event: { x: number ; y: number } ) => void ; };export type ClickQuery = Query <EventDefinition ['onClick' ]>;function handleEvent (event: ClickQuery ): void { if (typeof event.x === 'number' && typeof event.y === 'number' ) { console .log (`Click at (${event.x} , ${event.y} )` ); } }handleEvent ({ x : 100 , y : 200 });
中间件或装饰器 定义一个中间件,使用 Query 来确保请求对象的类型正确:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type EventDefinition = { authenticate : (req: { userId: string ; token: string } ) => boolean ; };export type AuthenticateQuery = Query <EventDefinition ['authenticate' ]>;function authenticateMiddleware (req: AuthenticateQuery ): boolean { return req.token === 'valid-token' ; }console .log ( authenticateMiddleware ({ userId : 'user123' , token : 'valid-token' }) );
这些例子展示了 Query 类型在不同场景下的应用,包括参数验证、函数重载、事件处理等。通过使用 TypeScript 的高级类型特性,我们可以创建类型安全且灵活的代码。