--------------更新于2020年7月20日-----------------
错误提示: TS2307: Cannot find module ***.vue
;
解决方案
在任意一个后缀名为.d.ts
的文件里面写入即可
declare module '*.vue';
tips: 发生这样的错误是因为TypeScript
识别不了vue
类型的文件,所以需要用户任意定义一个声明文件.d.ts
结尾的文件。以此类推,如果遇到*.svg 、*.png 、*.jpg 、*.jpeg 、*.gif 、*.bmp 、*.tiff 、*.json
后缀名的话,也这样定义即可。这里需要提示一下,定义了.d.ts
文件,放在项目任意位置都可以,typescript
会去全局识别,不过一般我们推荐放在src/typings/
下面。
补充:有读者留言说如果遇到找不到npm
上面的第三方包,怎么办。同样原理,不管你是echarts、jquery还是moment
,都可以通过达到目的。
declare module 'jquery'
declare module 'echarts'
declare module 'moment'
定义好你就可以在具体的组件里面通过
import JQ from 'jquery'
import Echarts from 'echarts'
import Moment from 'moment'
来使用啦
开发人员都知道,强类型语言能在编译的时候就发现问题,越早发现问题,付出修复的成本就越低,JavaScript是一门弱类型的语言,所以JavaScript编译器不能检测出类型错误。任何事物都有双面性,并不一定说强类型就可以避免开发的所有问题,而且强类型检测找到的错误并不是令我最头疼的错误。另一方面JavaScript是自由的,无需复杂的类层次,不用做强制转型,也不用疲于应付类型系统以得到想要的行为。
# JavaScript的继承与其它面向对象语言的继承区别
JavaScript有一个原型继承的概念,通过原型去继承属性 ex:fn.prtotype.xxx
面向对象语言则是通过类去继承属性 class Test {} class Demo extends Test {}
多态应用在JavaScript开发中是比较少的,但是TypeScript完美继承了面向对象的多态
派生类
所谓的派生类是相对于父类而言,可以理解为子类,或者更通俗点就是继承了父类的属性而且还保留自己本身的特性的类。
泛型约束
泛型出现的目的是解决函数只能传入某种类型的参数:
一般函数参数声明的写法,这样很限制功能的拓展,最起码我只能传固定类型的,那么有没有一种方法可以让我传不同类型,然后让编译器自己推断传入的参数类型进行校验呢?而且我希望以后传入的东西还可以是interface接口。泛型应运而生
function example1(arg:string):void { // 普通泛型写法
console.log(6666)
}
function example<T>(arg:T):T{
console.log(arg.length); // 会报错,因为参数传入可能是一个Number类型,不是数组
return arg;
}
这样编译器就可以根据传入的参数类型不同推论出返回值的类型,但是存在一个问题,就是我不需要那么宽泛的约束,我只需要在一定范围内的泛型约束,如果满足就允许进去,如果不满足那么就限制,泛型约束要进行一次升级至---泛型约束
interface MustHaveLength {
length: number;
}
function example<T extends MustHaveLength>(arg:T):T{
console.log(arg.length);
return arg;
}
多重泛型
ex: x<interface<T>> // 多重泛型解决一个对象多重嵌套问题
export interface ResponseData<T = any> {
code: number;
result: T;
message: string;
}
export function getUser<T>() {
return axios.get<ResponseData<T>>('url');
}
interface User {
name: string;
age: number;
}
async function test() {
const user = await getUser<User>();
}
上面就是为什么有时候会存在多重的尖括号,有时候就不存在。说白了就是泛型的对象层面应用。
类型别名也可以限制函数的参数以及返回值,其实本人觉得函数是一个特殊对象,也可以使用到泛型进行多重嵌套。
同样的,函数作为参数也可以使用别名进行约束,给解构赋值约束,给对象数组约束
noImplicitAny告诉编译器,当无法推断一个变量时发出一个错误。默认情况下都是true。
当给一个变量赋值为any的时候,也表明你告诉编译器不要做任何的类型检测。
类
在TypeScript中,如果构造函数参数仅存在x、y,那么new出来的实例是不会有东西的,只会是一个空对象;如果给参数加上public修饰符,那么new出来的实例就会有对应的属性。
constructor(x, y){} // new 出来的实例不存在this.x,this.y
constructor(public x, public y){} // new 出来的实例存在this.x, this.y
结构化
TypeScript对象是一种结构类型,意味着只要结构匹配,名称就无关紧要了。
如果函数的参数是一个对象,那么如果需要存在x, y两个属性,那么传入拥有x, y, z三个属性的对象也没问题
如果函数的参数是一个子函数,那么子函数的参数只可能小于等于最大的参数数量,超过会报错,跟对象参数有点不同。
函数的可选的和Reset参数都是兼容的 ex:
// strictNullChecks为false时均兼容
let foo1 = (x:number, y:number)=>{};
let foo2 = (x?:number, y?:number)=>{};
let foo3 = (...args:number[])=>{};
// foo1 = foo2;
// foo1 = foo3;
// foo3 = foo2;
// foo3 = foo1;
// foo2 = foo3;
// foo2 = foo1;
变体
对于类型兼容性来说,变体时一个利于理解和重要的概念。对一个简单类型Base和Child来说,如果Child时Base的子类,
Base类型的变量能赋值给Child的实例。这是多态
interface Event {
timestamp: number;
}
interface MouseEvent extends Event {
x: number;
y: number;
}
interface KeyEvent extends Event {
keyCode: number;
}
enum EventType {
Mouse,
keyboard
}
思考以下的两种方式
this.addEventListener(EventType.Mouse, <(e:Event)=>void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
this.addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + (<MouseEvent>e).y));
其他
枚举 - 枚举与数字类型互相兼容,但是来自不同枚举的枚举变量,被认为是不兼容的。
类 - 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查
泛型 - TypeScript类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。如下例子中,T对兼容性没有影响。ex:
interface Jack{}
let A:Jack;
let B:Jack;
x = y; // ok
如果尚未实例化泛型参数,则在检查兼容性之前将其替换为any:
let A = function(x:T):T{}
let B = function(y:U):U{}
A = B //ok
类:
class List {
add (val: T) {
}
}
class Animal {
name: string;
}
class Cat extends Animal {
meow () {
// ..
}
}
const animals = new List(); `注意:这里的Animal作为一个对象传进去的`
animals.add(new Animal()); // ok
animals.add(new Cat()); // ok
const cats = new List();
cats.add(new Animal()); // Error
cats.add(new Cat()); // ok
函数约束以及返回值约束
// 定义参数约束接口
interface Params {
name: string;
sex: 'male' | 'female';
age?: number
}
// 定义返回值约束接口
interface Info {
sex?: 'male' | 'female'
}
// 定义函数实现
function getInfo(params:Params):Info {
return {
sex: 'male'
}
}
上面这些都是对参数、返回值进行约束规范,在项目中有时候不得已需要用某种办法规避这种检测,那么怎么办?
`使用字符串索引签名,这样就可以传入一些未经定义的参数`
interface Params {
[key:string]: any;
}
`使用类型断言`
使用接口的时候这样操作即可getInfo({jack: 666} as Params) // 虽然传入的参数和接口定义的不一样,但是没报错
`使用覆盖`
let params1 = {jack:666}
getInfo(params1)
因为params1不会经过额外属性检查,所以编译器不会报错
interface 有个比较特别的地方就是,重复对同一个interface声明会让他们都联合起来
interface Jack {
name:string;
}
interface Jack {
sex: 'male' | 'female'
}
上面对Jack声明了两次,结果等同于下面的
interface Jack {
name: string;
sex: 'male' | 'female';
}
同一字面量存在多种情况的时候,使用type;如果是对象的某个key值存在多种状态的时候,可以使用type,也可以使用字面量声明
type Age = 50 | 80;
interface Sex {
type: 'male' | 'female',
age: Age
}
let sex: Sex = {
type: 'male',
age: 50
}
上面例子就是在interface里使用type
let age: Age = 50 // 这个例子是字面量使用type
而且需要注意的是,interface可以类似看作是对象,然后如果需要获取对象的key值作为内容的话,那么可以通过
keyof Sex // 这里目前只了解到可以作为value值,为 'type' | 'age'
关于!、?和as的用法
结论
1、as和!用于属性的读取,都可以缩小类型检查范围,都做判空用途时是等价的。只是!具体用于告知编译器此值不可能为空值(null和undefined),而as不限于此。
2、?可用于属性的定义和读取,读取时告诉编译器此值可能为空值(null和undefined),需要做判断。
对于引用类型的的值的使用,有时候编译器推断不出来类型,那么就需要断言了
const test = {
props: 'jack' as 'jack'
}
test(test.props); // 如果不加上jack,那么会出现Type string is not assignable to type 'foo'
# summary
# 类型注解
: xxx
其中xxx可以为对象、字符串
ex:
let A:{
name: string;
age: number
}
let B:string = ''; // 在这里面初始化值
# 柯里化
指的是将函数的参数拆成单参的形式传递进去
add(x, y) // 求出x+y的和
add(x)(y) // 柯里化之后求和方式
之前出现一个典型的对象类型推导问题,声明了一个对象,然后需要初始化,但是初始化的值是放在之后处理的,所以给对象赋值会报错,那么就需要给对象声明了,这样才可以给对象赋值
ex:
let foo = {};
foo.bar = 123; // Error Property 'bar' does not exit on type {}
foo.bas = 'str'; // Error 同上
# typeof 与 keyof
typeof 用于变量声明空间 -- 效果是整个对象
let T1 = {
name: 'jack',
age: 66
}
let D:typeof T1 = {
name: string,
age: number
}
keyof 用于类型声明声明空间 -- 效果是仅仅联合类型的key,类似Object.keys
interface T1 = {
name: 'jack',
age: 66
}
let D:keyof T1 = 'name' | age
PS:也可以使用类型别名
type ali = T1; // 这样ali就相当于T1了
# 全局变量
当在一个ts文件里面声明了某个变量
let/const T1 = 100; // 全局变量,且被推断出是value,要使用对应的类型需要使用typeof
# 局部变量
加上一个export即可
export let/const T1 = 100;
或者
let/const T1 = 100;
export {
T1
}
/*
* 注意了,如果当前文件里面存在export {},那么即使存在const xxx = xxx;xxx也仅仅属于局部变
* 量。而且可以在export的时候进行重命名。export { T1 as newName }
* 在import进来可以是一个变量,也可以是一个类型。不仅可以在导出重命名,在导入也可以。
* import { T1 as newName } from 'xxx';
* TypeScript使用的模块策略和node的模块策略一致。就是先从当前目录node_module查找,然后
* 找不到就跳上上一级,知道系统的根目录
*/
PS:这样就可以解释得通这里面的疑惑了
export type xxx = {}
export const xxx = xxx;
export let xxx = xxx;
# 模块查找
import/require默认导入类型,如果需要导入变量,那么需要在其它文件里面声明。同名的类型与变量,优先导出类型,但是也可以通过typeof使得导出的是变量的类型
# 为了放置全局变量的滥用,可以在tsconfig.json里面配置types
{
"compilerOptions": {
"types": [
"xxxx" // 这里放入@types里面你可能要使用到的全局声明
]
}
}
# 声明文件
通过`declare`关键字,告诉TypeScript,你正在试图表述一个其他地方已经存在的代码。
建议放在x.d.ts文件里面
# 标志
|= 添加标志
&= 和 ~ 清理标识
|= 和 | 合并标识
# lib.d.ts
当安装TypeScript时,会顺带安装lib.d.ts等声明文件。此文件包含了JavaScript运行时以及DOM中存在各种常见的环境声明
如果不需要的话可以在tsconfig.json中指定选项noLib: true;关闭掉
TS作为数据类型、函数参数、返回值的约束,它其实在编译后就不复存在,所以它就相当于海市蜃楼(虚幻),不能把他当作真正的JS进行使用,例如一个对象里面的keys值,使用keyof是可以获取到已经定义的interface的key,但是有个问题就是它不是真实的存在,所以还需要你本人去初始化它。
其次,对于对象的声明,有两种方式
- 使用interface进行声明,然后再初始化(先初始化在使用)
- 使用as断言,告诉编译器该对象出来的东西就是这里面的东西,不用初始化(先进行一波逻辑操作后才能声明使用)
使用Oject.assign虽然很爽,存在的弊端也不少,主要是它会忽略类型的检测,所以,为了又能享受类型的检测,又能获得更佳的开发体验,所以可以在使用assign前先对变量进行类型检测。
// 这段代码看似没问题,其实里面根本没有使用到类型检测,或者说即使你往对象里面塞更多东西也不会报错
let config= assign({}, {xxx}, options);
这里就需要对里面已经确定了的,经常改变的属性进行类型的判断
let newConfig:Config = {xxxx};
let config = assign({}, newConfig, options);
高阶TS
主要使用于模块定义、函数定义
# in 类似for循环 --- 更加详细
# person is interface --- 需要更加详细
# 函数重载
# Partail<T> 将T里面的属性都转换为可选参数
# & 表示并集,他可以解决多个并集, 常见使用在接口
# Omit表示从某个接口里面剔除某元素
# Extract<T, K> 表示从T里面抽取出K,相当于交集
# Exclude<T, K> 表示T里面剔除K,相当于剔除操作
# Pick<T, K> 表示从T里面取得K的元素作为本次类型
# 使用( { Obj1 } | {Obj2} ) 可以达到一个或的效果
# 使用泛型进行类型别名的重构
type Api<T> = {
name: T
}
type UserApi = Api<string>
type OtherAPi = Api<string[]>
# 引入一个模块并使用里面的API
那么就要告诉TS这个模块里面存在什么,如果是函数的话,那么参数是什么,返回值又是什么。
ex:
declare module 'other-module' {
export function fn(params:string):string;
}
- 如果您不想在使用新模块之前花时间写声明,则可以使用简写声明来快速入门,相当于给一个值声明为any类型
declare module "hot-new-module";