学习编程的几个阶段
1.先熟悉基础知识,记不住没关系,做到有个印象,知道大概什么知识在哪一章;
2.由浅入深看示例代码,遇到有看不懂的函数,代码写法先去查基础知识,还不明白去查度娘看其他人的理解分享;
3.一直看别人的代码,就算记住了。也很难真正成为你的知识,做不到举一反三,只是代码的搬运工。还得多写代码,在别人代码的基础上多做改动,既能动脑思考,加深记忆,又能扩展一些新的知识点;
4.多讨论代码和多总结代码,这也是知识和技术升华的过程。
与传统动态弱类型语言 javascript 不同,静态类型的 typescript 在执行前会先编译成 javascript ,因为它强大的 type 类型系统加持,能让我们在编写代码时增加更多严谨的限制。注意,它并不是一门全新的语言,所以并没有增加额外的学习成本,你甚至可以将它理解为新增了类型封装的 javascript ,因此原先代码逻辑怎么写现在还是一样的写,至于底层编译的事我们无需关心。
原始数据类型
typescript 与 js 一样也有原始数据类型与对象数据类型,我们先来介绍常用的原始数据类型。
1 string
let str: string = '听风是风'; // 支持模板字符串 const echo = '听风是风'; let s: string = `name is ${echo}`;
2 number
let num: number = 1; let notANumber: number = NaN;
3 boolean
let bool: boolean = true;
4 任意类型any
any 用于表示任意类型,如果一个变量的类型为 any ,那么它可以被赋予任意类型的值,你可能会觉得 any 应该没啥使用场景,但恰恰相反的是,在日常赶项目赶交付期间,研发同学可能没有太多时间去做细致化类型定义,于是 any 走天下,这也是 typescript 又被戏称为 anyscript 的原因。
let a: any = 4; a = "4"; a = false;
这一点也能证明 typescript 所有类型都属于 any 的子类型。
5 undefined与null
let a: undefined = undefiend; let b: null = null;
默认情况下 undefined 与 null 这两个类型属于所有类型的子类型,也就是说你能将一个 undefined 复制给一个 number 的变量,但是开发中一般不推荐这么做,既然我们希望 typescript 为我们添加严格的类型判断,那么严格尊重总是有好处,你肯定也希望某种情况下 undefiened 调用了某个 number 的 API 。
我们可以在 tsconfig 配置中添加 strictNullChecks:true 的开关,来启用严格的控制判断,这样 undefined 或者 null 就只能赋值给它们自身类型的变量,虽然这两个类型本身用的也不多。
6 空 void
void 与 any 相反,它表示没有任意类型,比如一个函数需要返回一个字符串,你可以定义函数返回值的类型:
const fn = (): string => { return '1'; }
但假设这个函数没有返回值,那么就可以用 void :
const fn = (): void => { console.log(1); }
但事实上开发中如果一个函数无返回,我们也不会写 void ,因为本身也没什么意义,所以 void 使用并不多。
另外即使打开了 strictNullChecks 开关,我们还是能将 undefined 与 null 赋予给 void 类型的变量,即使这也没啥意义= =。
数组类型
array 定义支持两种写法,一种是 类型[] ,另一种是 Array<类型> ,比如:
// 元素全是number类型的数组 let arr1: number[] = [1, 2, 3]; // 元素全是string类型的数组 let arr2: Array<string> = ['1', '2', '3'];
number[] 就表示这个数组的所有元素都必须是数字,包括之后你也不能往数组中添加非数字的元素,比如:
let arr: number[] = [1, 2, 3]; arr.push('听风是风');// error 听风是风不是number类型
那假设数组元素种类比较多,我们可以用 any 来表示数组中可以出现任意类型,比如:
let arr: any[] = [1, 2, '3']; arr.push(undefined);
接口类型(对象类型)
我们一般用接口 interface 来描述对象类型(这里的对象指 {} ),比如人都有名字,性别年龄等属性,接口即可用于对人这个类的形状进行抽象描述,直接看个例子:
// 我们定义了一个叫Person的接口,推荐首字母大写 interface Person { name: string; age: number; } let echo: Person = { name: '听风是风', age: 28, }
我们定义了一个接口 Person ,然后变量 echo 使用了接口 Person ,可以看到 echo 和 Person 的属性形状需要保持一致,缺属性或者多属性都不行:
// 少属性 // error Property 'age' is missing in type '{ name: string; }'. let echo: Person = { name: '听风是风', } // 多属性 let echo: Person = { name: '听风是风', age: 28, gender: 'male'// Person上未指定gender:string }
1 可选属性
接口支持使用 ? 让某个属性变为可选,女性的年龄都是秘密,某种场景下我们并不能拿到 age 属性,那么我们可以让 Person 的 age 为可选,比如:
interface Person { name: string; age?: number; } // 缺少了age属性,但并不会报错 let echo: Person = { name: '西西', }
2 只读属性
除了限定属性可选,我们还可以通过 readonly 字段来限制某个属性只读,比如:
interface Person { readonly name: string; age: number; } let echo: Person = { name: '听风是风', age: 28, } echo.name = '时间跳跃';// error 无法修改只读属性
可以看到 name 添加了只读,因此它在初始化之后就无法修改。
3 限制接口属性范围
比如上述代码我们限制了名称是字符串,也就是说任意字符串都可以,假设我们设计了一个组件,它的名称属性将决定组件如何展示,而且我们预定了只支持两种模式,那么此时我们就可以更精确的来限制属性范围,比如:
interface P { name: 'wide'| 'narrow'; } let props:P = { name: 'wide' }
此时 props 的 name 字段只能是 wide 或者 narrow 其一,输入其它就会报错,这样能很好的让组件属性输入符合预期。
4 额外的任意属性
某些情况我们希望接口能添加任意属性,那么可以通过如下方式:
interface Person { name: string; age?: number; [propName: string]: any; } let echo: Person = { name: '听风是风', age: 28, hobby: '吃西瓜', gender: 'male' }
[propName: string]: any 表示可以添加变量名为 string 且值为 any 类型, propName 的类型只会限制额外的属性,假设我们将其改为 [propName: number] ,那么 hobby 与 gender 就会报错,而我们将其变量名改为数字则不会有问题:
interface Person { name: string; age?: number; [propName: number]: any; } let echo: Person = { name: '听风是风', age: 28, 1: '吃西瓜', 2: 'male' }
但重点需要注意的是,假设我们定了额外的属性,那么确定属性以及可选属性的类型一定得是额外属性的子类型,比如上面 string 和 number 都是 any 的子类型,假设我们将 any 改为 string ,那么 age 属性就会报错:
interface Person { name: string; // 类型“number”的属性“age”不能赋给字符串索引类型“string” age?: number; [propName: string]: string; } let echo: Person = { name: '听风是风', age: 28, hobby: '吃西瓜', gender: 'male' }
假设我们不想定义 any 来包含 string 与 number ,这里也可以使用联合类型 string | number 来取代 any :
interface Person { name: string; age?: number; [propName: string]: string | number; } let echo: Person = { name: '听风是风', age: 28, hobby: '吃西瓜', gender: 'male' }
函数类型
javascript 中创建函数常用有函数声明与函数表达式两种形式,由于函数有输入和输出,所以我们需要对参数以及返回结果都做限制,我们来分别介绍两种写法。
1 函数声明
function sum(x: number, y: number): number { return x + y; };
比如上述的 sum 函数就接受2个类型为数字的变量 x,y ,并会返回它们的和,和的类型也是数字。
在限制下我们调用函数时少传多传,或者传递的参数类型不对都会有错误提示:
sum(1,'2');// error '2'的不是数字类型 sum(1);// error 需要2个参数但只传递了1个 sum(1,2,3)// error 需要2个参数但传递了3个
2 函数表达式
我们将上面的代码改为函数表达式可以是这样:
let sum = function (x: number, y: number): number { return x + y; };
但其实这种写法是省略了 sum 类型的写法,让 typescript 自己进行了类型推断,啥意思呢?我们将鼠标放到 sum 上你就能看到 sum 自身的类型限制:
let sum: (x: number, y: number) => number let sum = function (x: number, y: number): number { return x + y; };
意思就是,我们将一个匿名函数赋值给了 sum ,匿名函数虽然做了参数以及返回值的类型限定,但是我们没对变量 sum 做类型限定, sum 是什么类型?很显然是函数类型,所以完整的写法应该是这样:
let sum: (x: number, y: number) => number = function (x: number, y: number): number { return x + y; };
注意 (x: number, y: number) => number 这一段是对于 sum 这个变量类型的描述,表示它是一个函数类型,接受了哪些参数,分别是什么类型,以及返回什么类型。正常我们在接口中描述对象某个属性是一个函数也是相同的写法,比如:
interface Person { name: string; age: number; canFly: () => boolean }; let echo: Person = { name: '听风是风', age: 28, canFly: function () { return false } }
在接口中我们对 canFly 进行了描述,它是一个函数类型,没有入参且返回一个布尔值,于是在变量 echo 中我们具体实现了这个方法,同样没有入参,直接返回一个布尔值。这就相当于函数类型说明都被提到接口中统一描述了,而到具体实现时你不用重复再限制一次。而在上面的函数表达式中,我们要么省略变量的限制让 typescript 自行推断,要么我们手动补全变量的函数类型限制,其实就是这个意思。
当然,如果你觉得自己补全看着函数太长了,我们也能将函数变量这一块的描述交给接口,这样看着就相对简洁一点:
// 将函数变量的约束抽离出来给接口来做 interface MySum { (x: number, y: number): number } let sum: MySum = function (x: number, y: number): number { return x + y; };
3 可选参数
函数同样支持使用 ? 来表示某个参数可选:
function sum(x: number, y?: number): number { return x + y; }; sum(1);
4 参数默认值
有默认值的参数在 typescript 中会被默认识别为可选参数,毕竟有默认值传不传递都可以:
function sum(x: number, y: number = 1): number { return x + y; }; sum(1);
5 ...rest参数
在 es6 中我们可以用 ...rest 来表示函数剩余参数,这也巧妙解决了 arguments 是类数组无法使用数组 api 的问题,因为在函数内 rest 就是一个数组,因此我们可以用数组类型来描述 rest ,比如:
function fn(a: number, ...rest: any[]) { rest.forEach((item) => console.log(item)) } fn(1, 2, 3, 4, 5);
联合类型
很多时候,我们可能需要让一个变量支持数字以及字符串等多种类型,这时候就需要使用联合类型,它使用符号 | 表示,比如:
// echo的值可以是数字或者字符串 let echo: number | string = 'echo'; echo = 1;
以上代码中的 echo 可以被赋值为任意的字符串或数字类型的值。但需要注意的是,当我们未给 echo 赋予准确的值,但需要访问某个属性或 api ,此时只能访问联合类型共有的属性或者方法,比如:
let echo: number | string; // 数字和字符串都支持toString方法 echo.toString(); // 报错,Property 'toFixed' does not exist on type 'string'. echo.toFixed(1);
但假设我们给 echo 赋予具体的值,此时联合类型同样会走类型推断,从而让我们能正确使用对应类型的 api ,比如:
let echo: number | string; // 此时被推断成字符串,因此能调用字符串的api echo = '听风是风'; echo.length;// 4 // 此时被推断成数字,因此能调用数字的api echo = 1.021; echo.toFixed(2); // '1.02'
类型推断
首先 类型推断 属于 typescript 的一个概念,相当于 typescript 底层自动会帮我们做的一件事,大家作为了解就好。
正常来说我们定义字符串是这样,我们明确标明了 str 的类型,以及符合预期的修改 str 的值:
let str: string = 'echo'; str = '听风是风';
但假设我们不去定义一个变量的类型,但赋予了这个变量一个明确的值,比如一个字符串,再修改值为数字时,你会发现报错了:
let str = 'echo'; str = 1; //Type '1' is not assignable to type 'string'.
这是因为 typescript 会根据我们最初赋予的值,尝试去推断这个变量的类型,所以即便我们没指定具体类型,后续也不能随意修改值的类型,这就是所谓的 类型推断 了。
上述代码等价于:
let str: string = 'echo'; str = 1; //Type '1' is not assignable to type 'string'.
也就是说,只要你定义的文件是 .ts ,就别想着在 ts 文件不定义类型然后随意赋值,这对于 ts 而言肯定是不允许的。除了上文提到的几个不常用的类型,日常开发中我们还是希望能明确标明变量类型。
还有一种比较特殊,我指定以了一个变量但没赋值,这时候因为没具体的值,所以 typescript 会将这个变量推断成 any 类型,因此我们可以随意修改这个变量的值,比如:
let echo; echo = '时间跳跃'; echo = 1; // 等同于 let echo: any; echo = '时间跳跃'; echo = 1;
类型断言
如果说类型推断是 typescript 自动做的类型判断,那么类型断言就是我们人为手动的来指定一个值的类型,它支持两种写法:
// <类型>变量名 <string>echo // 变量名 as 类型 echo as string
需要注意的是,在 tsx 文件中只支持 as 这种写法,保险起见统一使用 as 更稳。
为什么会有类型断言的使用场景?我们来看几个例子你就明白了。
1 联合类型断言场景
我们前面说了,当一个变量没具体赋值,且有联合类型时,它只能使用联合类型共有的属性或方法,那假设我们现在封装了一个方法:
function fn(s: string | number): number { // 报错 Property 'length' does not exist on type 'number'. return s.length; };
我们假定参数 s 可能是字符串或者数字两种类型,但是你很清楚这个方法只会接受到字符串,那我们就可以手动指定 s 的类型,比如:
function fn(s: string | number): number { return (s as string).length; };
类型断言的目的就是我们开发者主动的告诉 typescipt ,我现在很清楚这个变量此时的类型是什么,从而让 typescript 不报错,但上述编码编译成 js 后其实也只是一个普通的返回 s.length 的函数,假设参数依旧传递了一个数字进来,那么还是无法避免报错的尴尬,所以使用类型断言时一定得谨慎,它只是绕过 typescirpt 报错,并没有从根源上解决代码兼容问题。
上述代码通过 if 来限制执行,你会发现这样实现其实更稳:
function fn(s: string | number): number { let length = 0; // 我们添加了判断,也相当于了人为对类型做了判断,因此也不会报错 if (typeof s === 'string') { length = s.length; }; return length; };
2 父子类断言场景
ES6 支持类的定义与继承,而当类具有继承关系时,类型断言也会起到作用,比如:
class P { } class P1 extends P { a: number = 1; } class P2 extends P { b: number = 2; } const fn = (s: P): boolean => { // 报错 Property 'a' does not exist on type 'P'. if (typeof s.a === 'number') { return true; } return false; }
这里我们定义了一个父类 P ,基于 P 继承得到了 P1 P2 两个类,且两个类都有属于自己的实例属性,现在我们定义了一个比较通用的检测 P 类属性的方法,考虑到公用型,所以类型定义我们使用 P ,但在内部实现中, typescript 会告诉你 P 上并没有属性 a 。这时候我们同样可以利用断言将类型精确到子类 P1 ,如下:
const fn = (s: P): boolean => { if (typeof (s as P1).a === 'number') { return true; } return false; }
当然这也只是绕过了 typescript 的检测,假设我们传递了一个 P2 实例进来,你会发现代码会报错,这并没有解决根本问题。所以更好的做法还是从逻辑层间提升代码稳定性,比如:
const fn = (s: P): boolean => { if (s instanceof P1) { return true; } return false; }
3 将任意类型断言为any
javascript 中存在很多原生对象,这些对象一开始就没被添加类型,比如我们希望在全局对象 window 上添加属性就会报错:
window.echo = 1;// error window上不存在echo的类型
这时候我们可以手动将 window 断言为 any 以解决修改属性以及添加属性的问题:
(window as any).echo = 1;
我们知道 window 上默认自带一些属性,比如 name 字段默认是一个空字符,因此在 typescript 中 name 也默认被推断成了 string 类型,比如我们想将 window.name 修改为数字默认会报错:
window.name = 1;// error 不能将1赋予给字符串类型的name
而断言成 any 可以让我们任意修改 window 上的属性类型,这很方便但也有一定风险,在实际开发中请谨慎对待。
4 将any断言成精确的类型
在旧有代码迁移 ts 或者维护不规范的 ts 代码时,我们可能会遇到因为赶项目赶时间而定义比较随意的 any 类型,而你了解了这段代码其实知道类型定义可以更为精确,重写重构代码写出完全规范的代码之外,你能通过断言对于模糊类型进行补救,比如:
interface UserInfo { name: string; age: number; } function getUserInfo(): any { return { name: '听风是风', age:28 } }; const user = getUserInfo() as UserInfo;
这样 user 后续的代码就能清楚知道这个对象是什么类型,对应让代码编写更严谨。
5 类型断言的限制
断言虽然在某些场景很好用,但它也得满足一些断言场景,毕竟我们总不能将猫的类型断言成鱼的类型,这就不符合规范了。那么满足什么条件才能断言呢?先说结论,当 类型A兼容了类型B,或者B兼容了A,那么A可以断言成B,B也能断言成A 。
什么意思?我们在上文提到,所有的类型都是 any 的子类型,也就是说 any 兼容了其它所有类型,比如 string 类型,因此我们可以将 any as string ,也能将 string as any ,这都是可以的。
我们再来看个例子:
interface Person { name: string; } interface User { name: string; age: number; } let echo: User = { name: 'Tom', age: 28 }; // 注意,echo包含age,但Person类并没有age let xixi: Person = echo;
我们定义了 Person 与 User 两个接口,比较有趣的事,我们将已定义了 User 类的变量 echo 赋值给 xixi ,而 xixi 的类 Person 并没有 age 属性,但并不会报错,而假设我们将代码改为如下这样,就会报错:
interface Person { name: string; } interface User { name: string; age: number; } let echo: User = { name: 'Tom', age: 28 }; let animal: Person = { name: 'Tom', age: 28 //error age在Person中未定义 };
为啥上面正常,下面这段代码就报错了,区别在哪?区别就在于上面的代码的 echo 提前定义好了 User 类型,而下面的赋值只是一个单纯的对象,是一个数据,它无类型。
那为什么上面不报错呢?其实说到底还是底层类型断言帮我们做了处理,上面的 Person 与 User 类型的关系你可以理解为:
interface Person { name: string; } // User继承了Person接口,并额外添加了age interface User extends Person{ age: number; }
因此接口 Person 兼容了 User (有相同属性,属性少的兼容属性多的)。还记得上文父子类断言场景中,我们将父类断言成子类的操作吗?你没发现参数处我们用了属性更少的父类 P ,但事实上传递进来的参数可能是接口 P1 或者 P2 的对象,它们的属性都比 P 接口定义的属性要多,但是参数这里并不会报错,而且在函数内我们还能将 P 断言为 P1 ,本质原因也是 P 兼容了 P1 P2 。
其实总结上面聊到的几个使用场景:
联合类型 string | number 断言为 string ,两者存在兼容关系。 父类断言成子类场景,两者同样存在兼容关系。 any 断言成任意,任意类型断言成 any ,本质上也是兼容关系。因此,只要两个类型存在兼容关系,不管谁兼容谁,都能相互断言成对方的类型。
另外,我们在前面说,猫类型不能断言成鱼类型,但现在你会发现一个很有趣的事情,猫类型和 any 是包含关系,而 any 和鱼类型同样是包含关系,那我能不能 cat as any as fish 双重断言直接实现跨物种进化呢?很明显通过这种做法,我们能实现类的任意断言,但 typescript 中一般不推荐这么做,因为大概率会导致类型错误....
5 类型断言与类型声明
我们在将 any 断言成其它类型讲解中,为了完善类型定义模糊的代码,我们给了一个例子,其实它还有其它的修复方法,比如不全函数的返回值的类型:
interface UserInfo { name: string; age: number; } function getUserInfo(): UserInfo { return { name: '听风是风', age:28 } }; const user = getUserInfo();
除此之外,我们还能补全 user 的类型声明,达到相同的效果,比如:
interface UserInfo { name: string; age: number; } function getUserInfo(): any { return { name: '听风是风', age: 28 } }; const user: UserInfo = getUserInfo();
单看类型断言和类型声明的补全的,你会发现后续使用 user 完全一致,那这两者有啥区别吗?我们再来看个例子:
interface Person { name: string; } interface User { name: string; age: number; } let echo: Person = { name: '听风是风' } // echo类型是Person,没有age属性 let xixi = echo as User; // xixi此时能使用age属性了 xixi.age = 27
上述代码很明显 Person 兼容了 User ,因此变量 echo 我们能使用断言将其类型由 Person 转为 User 后再赋值给 xixi ,之后 xixi 就能赋予 age 属性了。
但假设上述代码我们通过类型声明来做,如下,你会发现报错了:
interface Person { name: string; } interface User { name: string; age: number; } let echo: Person = { name: '听风是风' } // error Person缺少age属性 let xixi: User = echo;
为什么报错?很明显 echo 的 Person 类没有 age 属性,而 User 需要 age 属性,回到断言限制的第一个例子,对比下你会发现,我们将一个属性更多的类赋值给一个属性更少的类可以(后者兼容前者),反过来则会报错。
结合类型断言,你会发现类型声明的限制比类型断言要严格的多,大致我们可以总结为:
若 A 需要断言成 B ,只需要 A 兼容 B 或者 B 兼容 A 都可以 若将 A 赋值给 B ,那么一定是 B 兼容 A 才行(被赋值的类型属性比作为值的类型属性要少才行)。以上就是两者的区别。
内置对象
前文我们提到,在 js 中其实存在很多内置对象,比如 window、Date、RegExp 等等,你有没有想过,假设我现在声明了一个正则表达式,那我应该添加什么类型?
其实在 typescript 底层已经帮我们封装好了这些类型,我们可以直接使用,看部分例子:
let a: Boolean = new Boolean(1); let b: Error = new Error('报错啦'); let c: Date = new Date(); let d: RegExp = /'听风是风'/;
可以看到通过 new 一个构造器得到的实例,它们的类型都是首字母大写,正则比较特殊,不管是对象声明还是 new 它本身就只支持 RegExp 。
再比如 js 中内置了一些 DOM 以及 BOM 对象,比如类数组 NodeList ,事件对象 Event 等,这些也能直接用于类型定义,比如:
let div:NodeList = document.querySelectorAll('.div');
更多内置对象类型查阅 MDN ,这里就不一一细说了。
总结
那么到这里,我们已经快速过完了 typescript 基础内容,准备来说这些知识已经足以支撑看懂项目中大部分 ts 代码了,毕竟 ts 也没有太多的额外知识,更多是对于 js 类型的限制,后续会再利用进阶篇补全剩下概念,那么到这里全文结束。
原文地址:https://www.cnblogs.com/echolun/p/15956736.html
查看更多关于typescript快速上手的基础知识篇的详细内容...