何为ts
ts作为js的超集,意味着js的语法在ts中是能够跑通的。【ts主要提供了类型系统和ES6的支持】
ts一方面拓展了js的能力,另一方面是对js加上了限制
客观看待ts的使用
- 优点:可读可维护
- ts和js最大的差别其实还是ts提供了类型系统,类型系统本身就是文档,通过类型系统可以快速了解函数的作用、参数的类型或者接口相关的数据等
- 使用ts能够将很多bug问题在开发中暴露。举个例子,某个函数接收的参数为interface为PARAMS的params,如果传入的数据不匹配,编译器会给出警告提示
- 优点:包容性
- ts具有一定的包容性,即使ts编译报错,同样可以生成对应的js
- 没有显式定义类型的时候,能够实现类型推论
- 可以定义几乎一切数据类型
- 缺点
- 有一定的学习成本(学习成本不大)
- 相对于使用js的话,会增加一些开发上的成本,但是对于大型项目或者需要长期维护的项目来说,使用ts一定程度上能够减少维护上的成本
- 和第三方框架或库的结合不够完善。比如vue2.X
实践心得
- 减少代码容错判断分支
使用js在封装函数的时候需要在代码里面增加一个兼容处理来做一个容错操作。举个例子:封装一个阿拉伯数字转成中文汉字的函数,参数接收的应该是一个number类型的数字,但是传空串其实在一些中间代码可能存在类型转换或者其它原因导致这个空串变成0最终输出零,这不是我们要的结果,所以需要使用一些分支代码去做对应的处理。但是使用ts的时候我们可以直接限制参数的输入类型,来减少为了容错的编码。【在实际开发中其实这个是很实用的,在协同开发中团队的开发会重视复用率,使用ts进行编码可以在传参数据类型不对或者其它过程中可能出现的问题在开发过程中被发现,上诉例子就是一个又减少代码量又减少维护工作的例子,当然代码的健壮性另做考虑】 - interface接口数据
使用interface定义后台返回的数据,减少维护过程对接口文档的查阅。【当然这只是表面功能,很多时候后台的接口文档并不会做更新。举个例子,现在发现某个api接口中少了某个字段,需要后台做补充,后台做了相应的补充后很可能不会去修改接口文档对应的地方,或者说本来前端使用后台某个接口返回的某个字段去做一些业务判断处理,后台私自改了字段名等操作导致出现bug也能够快速复现。】 - 减少粗心造成的问题【表面】
let a = 1;
let b = 2;
const obj = {
fn1 () {},
fn2 () {},
fn4 () {},
}
obj.fn3() // obj中没有fn3
console.leg(aa) // leg,fooooo无定义
function b () { // b 已经被声明
alert(tast) // tast不存在
}
/*
上面的代码如果放在js文件中是没有报错提醒的,但是如果放在ts文件中编译器会飘红
*/
- 减少粗心造成的问题【非表面】
const oDiv = document.getElementById('id');
oDiv.className = 'have-bug';
/*
上面的代码如果放在js文件中在编码过程也是不会出现报错提醒的,但是ts文件下编译器会提示oDiv的类型为HTMLElement | null 来提醒开发者矫正
*/
学习文档
- 原始数据类型
JavaScript中没有空值概念,ts中可以用void来表示无任何返回值的函数(一般情况不用void作为变量类型,void类型只能赋值undefined和null)
null和undefined也是ts中的原始数据类型,与void的区别是:null和undefined是所有类型的子类型,意思就是说这两种类型声明的变量可以赋值给其他类型,其他类型的变量也可以赋值为null或undefined。 - 任意值any
普通类型在赋值过程是不允许修改变量类型的,而any类型的变量可以(允许访问任意值上的任何属性和调用任意值上的任何方法)
声明变量的时候如果未指定数据类型且未赋值则默认为any - 类型推论
没有明确指定类型的时候,ts会根据类型推论推断出一个数据类型
const params = 'caoyue';
// 等价于
const params: string = 'caoyue';
- 联合类型
联合类型表示取值可以为多个类型中的某一个(用 | 隔开)
当ts没有办法确认联合类型的变量属于哪个类型的时候只能访问联合类型的共有方法或属性
function getString(something: string | number): string {
return something.toString(); // number和string都有toString()方法
}
function getLength(something: string | number): number {
return something.length; // number没有length方法,error
}
- 接口类型interface
ts中的接口类型可用于对对象进行描述和限制
interface Person {
name: string;
age: number;
}
// 可选属性:允许该属性不存在
interface Person {
name: string;
age: number;
gender?: string;
}
// 任意属性: 注意!一旦定义了任意属性,那么确定属性和可选属性的类型必须是它的类型的子集
interface Person {
name: string;
age: number;
[propName: string]: any;
}
// 只读属性:注意!只读属性的约束是在第一次给对象赋值的时候而不是第一次给只读属性赋值的时候
interface Person {
readonly id: number;
name: string;
age: number;
[propName: string]: any;
}
- 数组的表示
let array = number[];
let array = Array<number>
- 函数类型
定义函数的时候要同时定义函数的输入输出类型
function (number: number):boolean {
return number > 1;
}
调用函数的时候输入参数必须等于函数接收的函数(rest和可选参数除外)
函数表达式的定义
let mySum = function (x: number, y: number): number {
return x + y;
};
// 上面其实是对匿名函数进行了类型定义,然后赋值给mySum,由于赋值操作触发类型推断出来的。
// 下面是手动给mySum进行定义
// => 箭头与ES6中的箭头函数不同,这里的箭头用来表示函数的定义,需要用括号括起来,括号左边为输入类型,右边为输出类型
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
使用interface定义函数
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
函数重载:允许一个函数在接收不同数量或类型的参数的时候做出不同的处理
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('');
}
}
// 上面的这个反转函数不能精确表达输入为数字的时候输出也为数字,输入为字符串的时候输出也为字符串
// 作出以下改变后,重载的reverse函数会在调用的时候那行正确的类型检查(前面几次其实都是函数定义,后面才是函数实现)
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('');
}
}
/*
官方文档:!!!为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
*/
- 类型断言:
语法: 值 as 类型 、 <类型>值。(个人喜欢用第一种,可以跟泛型区分开)
主要用途:
- 将变量定义为联合类型中的其中一个【ts不确定变量属于哪一个类型的时候只能访问联合类型的共有属性或方法(其实就是前面举例number和string的toString和length)】
- 将任何一个类型断言为any,有的时候能够保证代码是完全没有问题的,比如
window.foo = 1; // error,提示window上没有error属性
// 改写为
(window as any).foo = 1;
/*
应该注意对 as any的使用,有可能会掩盖掉真正的类型错误
*/
- 将any精确化
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
// 调用完getCacheData后立即将它断言为Cat类型,提高后续对tom相关代码的可维护性
tom.run();
- 将父类断言为具体的子类
interface ApiError extends Error { // interface 可换为class
code: number;
}
interface HttpError extends Error { // interface 可换为class
statusCode: number;
}
function isApiError(error: Error) {
// 如果是上面是class类型其实可以直接使用instanceof来判断
// 但是如果是interface就不行,因为interface是一个类型,不存在于编译结果中
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
- 类型断言 对比 类型转换
类型断言其实只是在编译告诉编译器某个变量的类型,类型断言相关的代码其实是不存在于编译结果的,但是类型转换是实际存在的。 - 类型断言 对比 类型声明
其实还是上面的道理,类型断言本身只是一个与编译器的交流行为。所以类型声明本身要更加严谨
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;
// 上面为类型断言
// 下面为类型声明 --error
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom: Cat = animal; // 不能将父类实例直接赋给子类变量
// animal 断言为 Cat,只需要满足 Animal 兼容 Cat 或 Cat 兼容 Animal 即可
// animal 赋值给 tom,需要满足 Cat 兼容 Animal 才行
类型断言 对比 泛型
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
// 使用泛型优雅解决前面any精确化问题
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
- 双重断言【不建议使用,后续有实践经验再补充】
- 类型别名:type关键字创建类型别名
type phone = string | number ;
type NameResolver = () => string;
- 字符串字面量类型
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// ...
}
handleEvent(document.getElementById('hello'), 'scroll');
handleEvent(document.getElementById('world'), 'dblclick'); // error
- 元组:数组合并的是相同类型的对象,元组可以合并不同类型的对象(无实践,后续补充)
- 枚举:用于取值限定在一定范围内的场景。【枚举值和枚举名双向映射】
- 泛型:在定义函数、接口或类的时候不预先指定具体类型,等到使用的时候再指定
实现一个函数直接返回传入的参数
function identity(arg: any): any { // 用any不友好,本质上其实也没有达到语义化效果
return arg;
}
function identity<T>(arg: T): T {
return arg;
}
泛型约束:函数内部使用泛型的时候,由于事先不知道泛型类型,所以会限制操作
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // error
return arg;
}
// 进行泛型约束
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 限制后传入的参数就必须包含length属性了
- 声明合并
函数合并:其实就是函数重载那个知识点
接口合并
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
// ==>
interface Alarm {
price: number;
weight: number;
}
// 属性类型冲突的时候编译器会有报错提示
// interface中方法的合并遵循函数重载