类型系统是 typescript 最吸引人的特性之一,但它的强大也让咱们又爱又恨,每一个前端同窗在刚从 javascript 切换到 typescript 时都会有一段手足无措的时光,为了避免让编译器报错巴不得把全部变量都标注成 any 类型,然后在不断地填坑中流下悔恨的泪水。谨以此文记录我在学习 typescript 类型系统使用方法的过程当中遇到的一些问题,以供你们参考,避免大脑进水。javascript
(本文默认全部读者熟知活动全部 ES6 语法及特性)前端
class
在 .d.ts 文件中定义时使用了 export default
语法的问题直接将类型附在变量声明以后,并以冒号分隔。vue
const count: number = 3; const name: string = 'apples'; const appleCounts: string = count + name; assert.strictEqual(appleCounts, '3apples');
稍微复杂一点的,如数组 & 对象,按照其内成员结构进行声明。java
const object: { a: number } = { a: 1, }; const array1: number[] = [1,]; const array2: Array<number> = [2,];
写法极像箭头函数。node
const someFn: (str: string) => number = function (str: string) { return +str; }
啥都能放,但并不建议大量使用,若是全部变量都是 any
,那 typescript 跟 javascript 又有什么区别呢。typescript
const variable: any = '5';
空类型,通常用来表示函数没有返回值。npm
function someFn (): void { 1 + 1; }
当一个变量多是多种类型时,使用 |
进行多个不一样类型的分隔:数组
const nos: number | string = '1';
错误用法浏览器
必定,必定,必定 不要用 ,
做分隔符。app
const object: { a: [number, string] } = { a: 1, };
正确用法
const object: { a: number | string } = { a: 1, }; const array1: (number | string)[] = [ 1, ]; const array2: Array<number | string> = [ 2, ];
或
type numberOstring = number | string; const object: { a: numberOstring } = { a: 'yes', }; const array1: numberOstring[] = [ 'yes', ]; const array2: Array<numberOstring> = [ 'yes', ];
函数的入参类型跟声明变量的类型时差很少,直接在变量名后跟类型名称就能够了。
返回值的类型则跟在参数的括号后面,冒号后面跟一个返回值的类型。
function someFn (arg1: string, arg2: number): boolean { return +arg1 > arg2; }
参数与返回值的声明方法与普通函数无二。
setTimeout((): void => { console.log('six six six'); }, 50);
实例属性记得必定要初始化。
class SomeClass { a: number = 1; b: string; static a: boolean; constructor () { this.b = 'str'; } method (str: string): number { return +str; } }
当你在声明一个普通的对象(其余类型也有可能,此处仅使用对象做为例子)时,typescript 并不会自动为你添加上对应的类型,这会形成你在赋值时触发 TS2322: Type 'xxx' is not assignable to type 'xxx'.
错误,此时就须要使用显式类型转换来将两边的类型差别抹平。
类型转换的前提是当前类型是真的能够转换为目标类型的,任何须选属性的缺失,或是根本没法转换的类型都是不容许的。
并不推荐这种方法,由于有时编辑器会把它当成 jsx 处理,产生没必要要的 warning。
const object1: object = { a: 1, }; const object2: { a: number } = <{ a: number }>object1;
const object1: object = { a: 1, }; const object2: { a: number } = object1 as { a: number };
至关于联结多个不一样类型,并为他们创造一个假名,平时写多个类型并联实在是太累了的时候能够试试这个方法。
它的值能够是类型,也能够是具体的变量值。
type NumberOrString = number | string; type Direction = 'Up' | 'Right' | 'Down' | 'Left'; const num: NumberOrString = 1; const dir: Direction = 'Up';
注意:枚举内部赋值时若是为数字,能够只赋值第一个,后面几个会随之递增,若是为字符串,则须要所有赋值,不然就会报错。
enum Direction { Up = 1, Right, Down, Left } const dir: Direction = Direction.Up;
若是你的代码中准备使用 enum 做为右值,那请不要把 enum 声明在 .d.ts 文件中,这是由于 ts 在编译的时候 .d.ts 文件是做为类型文件用的,并不会生成实体输出,天然也就没有地方会定义这个枚举,这个时候用在代码里做为右值的枚举值就会由于找不到整个枚举的定义,从而触发 'xxx is undefined' 错误。
这也使得它在使用过程当中给咱们形成了各类各样的麻烦(在 .d.ts 的 interface 声明中使用 enum 真的是再正常不过的事情了),好比:
// my-types.d.ts declare const enum Direction { Up = 0, Right = 1, Down = 2, Left = 3, }
// usage.ts class SomeClass { dir = Direction.Up; }
编译后的结果是:
// .d.ts 文件当场消失
// usage.js function SomeClass () { this.dir = Direction.up; }
浏览器在运行时根本找不到 Direction 定义的位置,天然就报 'Direction' is not defined
的错了,但 type Direction = 'Up' | 'Right' | 'Down' | 'Left' 的方法就不会有这种问题,具体使用方式以下:
// my-types.d.ts type Direction = 'Up' | 'Right' | 'Down' | 'Left';
// usage.ts const dir: Direction = 'Up';
缺点是没有类型提示,不能定义枚举的内部值,判断的时候也必须用对应的字符串进行字符串比对(汗。
声明一种类型的对象,该类型的变量都必须知足该结构要求。
interface SomeInterface { str: string; num: number; } const object: SomeInterface = { str: 'str', num: 1, }; class SomeClass implements SomeInterface { num = 1; constructor () { this.str = 'str'; } }
同一个类能够实现多个不一样的接口,但前提是该类必定要实现每一个接口所要求的属性。
interface Interface1 { str: string; } interface Interface2 { num: number; } class SomeClass implements Interface1, Interface2 { num = 1; constructor () { this.str = 'str'; } }
在多个不一样文件,或是相同文件的不一样位置声明的同名接口,将会被合并成一个接口,名称不变,成员变量取并集。
interface SomeInterface { str: string; } interface SomeInterface { num: number; } // 必须所有实现 const someInterface: SomeInterface = { str: 'str', num: 1, };
interface InterfaceFn { (str: string): boolean; } const fn1: InterfaceFn = (str: string): boolean => { return 10 < str.length; };
interface InterfaceFn { (str: string): boolean; standard: string; someFn(num: number): string; } // 必须进行显式类型转换 let fn1: InterfaceFn = function (str: string): boolean { return 10 < str.length; } as InterfaceFn; fn1.standard = 'str'; fn1.someFn = function (num: number): string { return `${num}`; };
接口能够继承类或是另外一个接口,与 ES6 继承方法语法同样,在此再也不赘述。
当该参数为可选项时,能够在名称与类型表达式的冒号之间加一个问号 ?
用来表示该参数为 __可选项__。
function someFn (arg1: number, arg2?: string): void {} someFn(1);
当该参数在不传的时候有 缺省值 时,可使用 =
来为其赋予 __缺省值__。
function someFn (arg1: number, arg2: number = 1): number { return arg1 + arg2; } someFn(1); // 2
可选项与 缺省值 能够混搭。
function someFn (arg1: number, arg2: string = 'str', arg3?: string): void {} someFn(1);
但 可选项 参数后不可跟任何 非可选项 参数。(如下代码当场爆炸)
function someFn (arg1: number, arg2?: string, arg3: string = 'str'): void {} someFn(1);
可选项与 缺省值 不可同时使用在同一个值上。(如下代码当场爆炸)
function someFn (arg1: number, arg2?: string = 'str'): void {} someFn(1);
可选项 也可用在接口的声明中(__缺省值__ 不行,由于接口是一种类型的声明,并不是具体实例的实现)。
function someFn<T> (arg:T): T { return arg; } const str1: string = someFn<string>('str1'); const str2: string = someFn('str2');
function someFn<T, U> (arg1: T, arg2: U): T | U { return arg1; } const num1: string | number = someFn<string, number>('str1', 1); const str2: string | number = someFn('str2', 2);
const someFn: <T>(arg: T) => T = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
interface InterfaceFn { <T>(arg: T): T; } const someFn: InterfaceFn = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
const someFn: { <T>(arg: T): T; } = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
interface InterfaceFn<T> { (arg: T): T; } const someFn: InterfaceFn<string> = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
原理与 接口泛型
同样。
class SomeClass<T> { someMethod (arg: T): T { return arg; } }
function someFn<T extends Date>(arg: T): number { return arg.getTime(); } const date = new Date();
keyof
、Record
、Pick
、Partial
太过复杂,真有需求还请自行查阅文档。
在实际编码过程当中,咱们常常会定义不少自定义的 接口 与 __类__,若是咱们在声明变量的类型时须要用到它们,就算咱们的代码中并无调用到它们的实例,咱们也必须手动引入它们(最多见的例子是各类包装类,他们并不会处理参数中传入的变量,但他们会在接口上强规范参数的类型,而且会将该变量透传给被包装的类的对应方法上)。
// foo.ts export default class FooClass { propA: number = 5; }
// bar.ts import FooClass from './foo.ts'; export class BarClass { foo?: FooClass; }
这种使用方法在调用数量较少时尚且能够接受,但随着业务的成长,被引用类型的数量和引用类型文件的数量同时上升,须要付出的精力便会随其呈现出 o(n^2) 的增加趋势。
这时咱们能够选择使用 .d.ts 文件,在你的业务目录中建立 typings
文件夹,并在其内新建属于你的 xxx.d.ts
文件,并在其中对引用较多的类和接口进行声明(.d.ts 文件是用来进行接口声明的,不要在 .d.ts 文件里对声明的结构进行实现)。
注意:.d.ts 文件只是用于分析类型声明及传参校验,若是须要进行调用,还请直接 import 对应模块。
// my-types.d.ts declare class FooClass { propA: number; methodA (arg1: string): void; }
foo.ts
文件略
// bar.ts // 不须要再 import 了 export default class BarClass { foo?: FooClass; }
其余类型的声明方式
// my-types.d.ts // 接口(没有任何变化) interface InterfaceMy { propA: number; } // 函数 function myFn (arg1: string): number; // 类型 type myType = number | string;
declare
关键字,且不要使用 export
语句(若是使用了 export
,该文件就会变成实体 ts 文件,不会被 ts 的自动类型解析所识别,只能经过 import 使用)原本想写一写常见的 TS 编译错误及形成这些错误的缘由来着,后来想了想,编译出错了都不会查,还写什么 TS 啊,
如下几点是我在使用 typescript 类型系统过程当中遇到的一些智障问题与未解的疑问,欢迎你们一块儿讨论。
错误的为 window 增添属性的姿式:
window.someProperty = 1;
会触发 TS2339: Property 'xxx' does not exist on type 'Window'
错误。
(window as any).someProperty = 1; (<any>window).someProperty = 1;
利用接口能够多处声明,由编译器进行合并的特性进行 hack。
interface Window { someProperty: number; } window.someProperty = 1;
下面的代码当场爆炸(由于 c: number,
最后的这个逗号)。
const someObject: { a: number, b: number, c: number, } = { a: 1, b: 2, c: 3, };
当一个函数在入参不一样时有较大的行为差距时,可使用函数重载梳理代码结构。
注意:参数中有回调函数时,回调函数的参数数量变化并不该该致使外层函数使用重载,只应当在当前声明函数的参数数量有变时才使用重载。
当同时声明多个重载时,较为准确的重载应该放在更前面。
重载的使用方法比较智障,须要先 声明 这个函数的不一样重载方式,而后紧接着再对这个函数进行定义。
定义时的参数个数取不一样重载方法中参数个数最少的数量,随后在其后追加 ...args: any[]
(或者更为准确的类型定义写法),用于接收多余的参数。
定义的返回值为全部重载返回值的并集。
然后在函数体内部实现时,经过判断参数类型,自行实现功能的分流。
神奇的是 typescript 并不会校验重载的实现是否会真的在调用某个重载时返回这个重载真正要求的类型的值,下方例子中即便不管触发哪一个重载,都会返回 number
,也不会被 typescript 检查出来。
猜测:屡次声明一次实现难道是受制于 javascript 既有的语言书写格式?
class SomeClass { someMethod (arg1: number, arg2: string, arg3: boolean): boolean; someMethod (arg1: number, arg2: string): string; someMethod (arg1: { arg1: number, arg2: string, }): number; someMethod (x: any, ...args: any[]): string | number | boolean { if ('object' === typeof x) { return 1; } else if (1 === args.length) { return 1; } else { return 1; } } }
function someFn (arg1: number, arg2: string, arg3: boolean): boolean; function someFn (arg1: number, arg2: string): string; function someFn (arg1: { arg1: number, arg2: string, }): number; function someFn (x: any, ...args: any[]): string | number | boolean { if ('object' === typeof x) { return 1; } else if (1 === args.length) { return 1; } else { return 1; } }
可使用 type
、interface
、class
对象 key
,可是使用方法十分麻烦,并且语法还不太同样(type
使用 in
,interface
与 class
使用 :
)。
注意:索引值只可使用数字与字符串。
其实就是放开了限制,让该类型的实例上能够添加各类各样的属性。
这里冒号 :
形式的不容许使用问号(可选项),但 in
形式的容许使用问号(可选项)。
但其实带不带结果都同样,实例均可觉得空。
type SomeType1 = { [key: string]: string; } type SomeType2 = { [key in string]?: string; } const instance1: SomeType1 = {}; const instance2: SomeType2 = {};
这里其中的 key
就成了必选项了,问号(可选项)也有效果了。
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType1 = { [key in audioTypes]: string; } type SomeType2 = { [key in audioTypes]?: string; } const instance5: SomeType1 = { 'ogg': 'ogg', 'mp3': 'mp3', 'wma': 'wma', }; const instance6: SomeType2 = {};
不能够用问号。
interface SomeInterface { [key: string]: string; } const instance: SomeInterface = {};
只能经过 extends
已定义的 type
来实现。
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType = { [key in audioTypes]: string; } interface SomeInterface extends SomeType {} const instance: SomeInterface = { ogg: 'ogg', mp3: 'mp3', wma: 'wma', };
一样也不可使用问号(可选值)。
class SomeClass { [key: string]: string; } const instance: SomeClass = new SomeClass();
经过 implements
其余的 interface
、type
实现(多重实现能够合并)。
请记得 interface
只是数据格式规范,implements
以后要记得在 class
里写实现
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType = { [key in audioTypes]: string; } interface SomeInterface { [key: string]: string; } class ClassExtended implements SomeInterface, SomeType { ogg = 'ogg'; mp3 = 'mp3'; wma = 'wma'; [key: string]: string; } const instance = new ClassExtended();
const someFn: (input: number, target: object) => SomeClass = (input: number, target: object): SomeClass => { // ... do sth };
咱们在平时使用一些类库时,某一辈子态环境下的多个包,可能会依赖同一个基础包。同一个生态环境下的包,更新节奏或快或慢,此时即可能会存在基础包版本不一样的问题,npm 的解决方案是多版本共存,每一个包引用本身对应版本的基础包。由于 typescript 的类型是基于文件进行定义的,内部结构彻底相同的两个同名类型,在不一样的文件中声明便成了不一样的类型。
此处以 @forawesome
项目组下的 fontawesome
库进行举例,具体示例以下:
当咱们在 vue 中使用 fortawesome 时,须要把图标文件从对应的包中导出(如免费基础包:@fortawesome/free-solid-svg-icons
、免费公司 logo 包:@fortawesome/free-brands-svg-icons
),并使用 @fortawesome/fontawesome-svg-core
模块的 library
方法导入到 vue 的运行环境中。
import { faVolumeUp, faPlay, faPause, } from '@fortawesome/free-solid-svg-icons'; import { faWeibo, faWeixin, } from '@fortawesome/free-brands-svg-icons'; library.add( faVolumeUp, faPlay, faPause, faWeibo, faWeixin );
但我再刚开始开发时只使用了基础包,公司 logo 包是我在开发途中用到时才引入的,但这时 fortawesome 官方对整个库进行了版本升级,具体功能并无什么改变,只是 fix 了一些 bug,版本号也只升级了一个小版本。
但在编译时 library.add 这里报告了错误:
TS2345: Argument of type 'IconDefinition' is not assignable to parameter of type 'IconDefinitionOrPack'. Type 'IconDefinition' is not assignable to type 'IconPack'.`
通过跟进发现:
@forawesome/fontawesome-svg-core
的 library.add
的参数所要求的 IconDefinition
类型来自顶层 node_modules
安装的公用的 @fortawesome/fontawesome-common-types
包的 index.d.ts
文件。
而 @fortawesome/free-brands-svg-icons
中字体的类型 IconDefinition
来自 @fortawesome/free-brands-svg-icons
自身内部 node_modules
里安装的高版本的 @fortawesome/fontawesome-common-types
的 index.d.ts
文件。
虽然两个类型的定义如出一辙,但由于不是同一个文件定义的,因此是彻底不一样的两种类型,于是形成了类型不匹配,没法正常编译。
遇到这种问题时,升级对应包的版本就能够了。
talk is cheap, show you the dunce.
节选自 vue/types/vue.d.ts
,我已经看晕了,调用方想要查错的时候到底怎么看呢。
export interface VueConstructor<V extends Vue = Vue> { new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>; // ideally, the return type should just contains Props, not Record<keyof Props, any>. But TS requires Base constructors must all have the same return type. new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>; new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>; extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>; extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>; extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; nextTick(callback: () => void, context?: any[]): void; nextTick(): Promise<void> set<T>(object: object, key: string, value: T): T; set<T>(array: T[], key: number, value: T): T; delete(object: object, key: string): void; delete<T>(array: T[], key: number): void; directive( id: string, definition?: DirectiveOptions | DirectiveFunction ): DirectiveOptions; filter(id: string, definition?: Function): Function; component(id: string): VueConstructor; component<VC extends VueConstructor>(id: string, constructor: VC): VC; component<Data, Methods, Computed, Props>(id: string, definition: AsyncComponent<Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; component<Data, Methods, Computed, PropNames extends string = never>(id: string, definition?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>; component<Data, Methods, Computed, Props>(id: string, definition?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; component<PropNames extends string>(id: string, definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>; component<Props>(id: string, definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; component(id: string, definition?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; use<T>(plugin: PluginObject<T> | PluginFunction<T>, options?: T): void; use(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): void; mixin(mixin: VueConstructor | ComponentOptions<Vue>): void; compile(template: string): { render(createElement: typeof Vue.prototype.$createElement): VNode; staticRenderFns: (() => VNode)[]; }; config: VueConfiguration; }
0 === san;