给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库
基本类型介绍
1.Boolean,Number,String
声明:类型 = 类型对应变量
let flag:boolean = true let age: number = 21; let name: string = "shixin"; 复制代码
类型收敛——字面量类型
const flag: true = true; const age: 21 = 21; const name: 'shixin' = 'shixin'; 复制代码
2.undefined,null
在 TypeScript 中,null 与 undefined 类型都是有具体意义的类型。所以在默认情况下会被视作其他类型的子类型。
const data1: null = null; const data2: undefined = undefined; const data3: number = null; // That's OK const data4: string = null; // That's OK 复制代码
tsconfig strictNullChecks配置项,会严格校验undefined和null。在该配置开启的环境下。undefined和null将不为其他类型的子类型
{ "extends": "./tsconfig", "compilerOptions": { "strictNullChecks": true } } 复制代码
const data5: string = null; // error 复制代码
3.联合类型
联合类型就是一个数组的可用的集合
let numOrStr :string|number = 1; numOrStr = 'a' // ok 复制代码
4.Array
在 TypeScript 中有两种方式来声明一个数组类型:
根据项目规范二选一(如果没有这个规范可以随便写)
const arr1: string[] = []; // 可以向数组内添加string类型的值 const arr2: Array<string> = []; 复制代码
类型收敛——元组
场景1 基本类型元组定义
或许我们想定义一个数组来描述三家公司的名称(只有那三家,没有第四家,也不会只有两家),公司名是string,长度是3。但是我们声明string[]就是有问题的,因为他不够收敛。这个时候我们就可以使用元组了。
形式 [typeofmember1, typeofmember2,typeofmember3]
元组声明:[string,string,string]
const companyArr: [string,string,string] = ['公司n1', '公司n2', '公司n3']; 复制代码
场景2 字面量类型元组定义
或许我们想定义一个数组来描述三家优秀公司的名称(NIO,小鹏,理想) 元组声明:['NIO','小鹏','理想']
const companyArr1: ['NIO','小鹏','理想'] = ['NIO','小鹏','理想']; 复制代码
在这种情况下,对数组合法边界内的索引访问(即companyArr1[0],companyArr1[1],companyArr1[2])将能够安全的返回我们理想中的哪三颗韭菜。
场景3 元组类型的 Rest 使用
或许我们想定义一个数组来定义一个人前三项分别为姓名,年龄,性别,后面可以添加若干项描述。这时候我们就可以引入Rest,语法如同js一样,三个点。 元组声明:[string,number,string,...string[]]
const Person: [string, number, string, ...string[]] = ['shixin', 22, '男', 'JV', '前端']; 复制代码
但是问题来了, [string, number, string, ...string[]]未免太难理解了,我怎么知道哪一项代表了哪个信息。在TS4.0添加了具名元组。操作如下
const Person: [name:string, age:number,sex:string,...desc:string[]] = ['shixin', 22, '男', 'JV', '前端']; 复制代码
5.常用对象类型描述
对于上面的元组场景3(定义一个数组来定义一个人前三项分别为姓名,年龄,性别)毕竟不是我们的常规操作,更加常见的是我们通过定义一个对象来完成对于一个人的定义。那么如何描述一个对象呢?
内联注解
我们可以如同基本类型一样直接在声明后面添加对象的类型描述
const shixin: { name: string; age: number; sex: string; desc: string[] } = { name: 'shixin', age: 22, sex: '男', desc: ['JV', '前端'] }; 复制代码
接口 interface
或通过interface来声明一个类型变量,在变量内进行对象的类型描述
interface IPerion { name: string; age: number; sex: string; desc: string[]; } const shixin: IPerion = { name: 'shixin', age: 22, sex: '男', desc: ['JV', '前端'] }; 复制代码
这样声明的好处,最直接的就是可以复用,这对于我们维护和统一类型起源非常有帮助。
import { type IPerion } from 'xxx'; const zhang3: IPerion = { name: 'zhang3', age: 20, sex: '男', desc: ['xx', 'xx'] }; 复制代码
object、Object 以及 { }
Object用于描述一个对象,就是万物皆为对象的那个对象。在js中他包含了原始的功能,比如谁都有的toString。也正因为如此,这个属性其实并不好用。甚至有点像any。
let data1: Object = 'a'; // That's OK let data2: Object = 1;// That's OK let data3: Object = () => {};// That's OK 复制代码
为了解决上面这个不好用的Object,在(typescript2.2)[www.typescriptlang.org/docs/handbo…] 引入了object type以表示不含原始类型(number,string,symbol等等)的object类型
const data1:object = 42; // Error const data2:object = 'string'; // Error const data3:object = false; // Error const data4:object = undefined; // Error const data5:object = { prop: 0 }; // OK 复制代码
{},一个空对象。你可以在该类型初始化的时候赋予各种各样的值,如同上面的第一个Object。但是在赋值的时候却比较麻烦。
const data1: {} = { foo: 1 }; // OK data1.baz = 2;// Error 复制代码
或许这样看起来并不常见,那么这个呢。
const obj = {}; 复制代码
事实上,这种定义的方式会讲{}推导为变量obj的类型。在javascript时期,这行代码随处可见。以至于在ts的代码中,依然会有非常多的这种写法,从而出现了下面这些代码.
const obj = {}; // 添加一个属性 obj.foo = 1; //类型“{}”上不存在属性“foo”。 // ⬇ /** * 给个初始值 */ const obj1 = { foo: undefined }; obj1.foo = 1; //不能将类型“1”分配给类型“undefined”。 // ⬇ /** * 分支1:给个初始类型undefined */ const obj: { foo: undefined | number; } = { foo: undefined }; obj.foo = 1; /** * 分支2:把foo初始为-1 */ const obj = { foo: -1 }; obj.foo = 1; 复制代码
除了object之外,其他两种方式其实在初始化变量的时候非常的开放,对于类型基本没有收敛。所以我们更建议不要使用。而object实际上对变量的描述并不够细致,通常对于对象的创建,我们都能够预想到对象的内容,所以我们更倾向于使用interface接口来定义对象。
6.枚举
枚举的作用和定义形式像一个简单的键值一一对应的map
const CaseMap = { case1: 'Im case1', case2: 'Im case2' }; enum CaseMap { case1 = 'Im case1', case2 = 'Im case2' } 复制代码
如果你没有声明枚举的值,它会默认使用数字枚举,并且从 0 开始。
const Direction = { Up: 0, Down: 1, Left: 2, Right: 3 }; enum Direction { Up, Down, Left, Right } 复制代码
如果你希望数字枚举从 1 开始,只需要在第一项枚举值声明起始值,那么接下来的项都会递增。
enum Direction { Up = 1, Down, Left, Right } 复制代码
对于枚举和对象的区别,也很简单,枚举是双向的,对象是单向映射的。原理很简单,我们将上面Direction编译为js,编译产物如下。
"use strict"; var Direction; (function (Direction) { Direction[Direction["Up"] = 1] = "Up"; Direction[Direction["Down"] = 2] = "Down"; Direction[Direction["Left"] = 3] = "Left"; Direction[Direction["Right"] = 4] = "Right"; })(Direction || (Direction = {})); 复制代码
给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库
7.any,unknow,never
any,unknow
any,unknow基本可以用于声明所有的类型。在某些时候我们是需要这种能力的。例如系统日志对于error的catch。我们无法知道用户会在里面报什么类型的错误,也不知道谁会在代码里面throw 一个number,string,boolean类型的Error出来。对于这种完全无法预知的变量,我们就可以定义为any或者unknow。
那么都是可以用于声明任意类型,any和unknow的区别是什么呢。
unknow对应中文就是不知道的意思。既然是不知道,即使定义的时候,我们无法确定类型,所以使用的时候,我们也不能随意使用。
try{ // xxxxx } catch (e) { const num: number = e; //不能将类型“unknown”分配给类型“number”。 if (isNumber(e)) { const num1: number = e; // e is number } } 复制代码
如上例子,我们需要通过一些判断手段,才能够对e进行使用。这样带来的好处就是,就算我不知道e是什么类型,但是只要通过一些必要的判断,我就能够安全的去操作他。坏处也显而易见,无论我做什么操作,我都需要进行判断,对类型进行过滤,才能够正常使用。
而any在这个demo的表现就相对的肆无忌惮。
try{ // xxxxx } catch (e:any) { const num: number = e; //ok const str: string = e; // ok if (isNumber(e)) { const num1: number = e; // e is number } } 复制代码
并且当我们定义了一个变量为any之后,他是具有传染性质的。any下面的所有属性都将是any。也就是在使用声明为any类型的变量时,该变量下面的所有属性都是危险的any。类型推断将会失效。
1. const data: any = 1; 2. 复制代码
any用的很爽,但是背后的代价就是把ts变成了anyScript,抛弃所有的类型推断,以让你的代码编辑看起来没有任何报错,表面风平浪静,背后暗流涌动。所以在我们想声明一个未知的catch error以及其他不知道的变量的时候,或许使用unknow更好,而现在unknow也成为了catch error的默认类型。
never
never代表着走不到头,无法执行下去的类型。抛开在类型体操的使用,在日常声明的使用场景多为 throw error。
function thouchError(): never { throw new Error(); console.log("已经error咯,不能正常执行下面的内容了") } 复制代码
在实际应用中,我们通过if ... else ...在最后的else添加never的声明。这时候类型报错就可以帮助你处理干净所有的类型情况。例子如下:
const fun = (data: string | number) => { if (isString(data)) { data.charAt(1); //OK } else if (isNumber(data)) { data.toFixed(); //OK } else { const check: never = data; throw new Error(check); } }; 复制代码
当data的类型情况多了一个boolean,ts就会推断报错。
const fun = (data: string | number | boolean) => { if (isString(data)) { data.charAt(1); //OK } else if (isNumber(data)) { data.toFixed(); //OK } else { const check: never = data; //不能将类型“boolean”分配给类型“never”。 throw new Error(check); } }; 复制代码
TS基本心智
类型上下文收敛和推断
类型推断就是TypeScript会根据上下文代码自动帮我们推算出变量或方法的类型。
当我们又一个函数名为 padLeft。如果参数 padding 是一个数字,我们就在 input 前面添加同等数量的空格,而如果 padding 是一个字符串,我们就直接添加到 input 前面。
function padLeft(padding: number | string, input: string) { return new Array(padding + 1).join(' ') + input; //运算符“+”不能应用于类型“string | number”和“number”。 } 复制代码
报错的原因很简单,不能用string + 1(尽管在js中是可以的),而变量padding是个number或者string,这样运算最后可能不是我们想要的结果。那么我们可以这样做
function padLeft(padding: number | string, input: string) { if (typeof padding === "number") { return new Array(padding + 1).join(" ") + input; } return padding + input; } 复制代码
而在这个过程中,我们其实对变量进行了几次收敛和推断。
- 通过typeof number 讲padding收敛为number
- 推断new Array(padding + 1)推断为数组
- 通过join推断new Array(padding + 1).join(" ")为字符串
- 顺利的让new Array(padding + 1).join(" ")可以和input相加
- 如果能走到最后的return padding + input 则说明typeof number为false。那么padding就是string
- string的padding可以顺利的和input相加
我们的类型系统就是如此保障我们的代码运行安全的。而对于类型上下文的收敛 ,这里借助(官方文档)[www.typescriptlang.org/docs/handbo…] 的demo举两个例子。
- typeof
function printAll(strs: string | string[] | null) { if (typeof strs === "object") { for (const s of strs) { // Object is possibly 'null'. console.log(s); } } else if (typeof strs === "string") { console.log(strs); } else { // do nothing } } 复制代码
- in操作符
type Fish = { swim: () => void }; type Bird = { fly: () => void }; function move(animal: Fish | Bird) { if ("swim" in animal) { return animal.swim(); // (parameter) animal: Fish } return animal.fly(); // (parameter) animal: Bird } 复制代码
- 穷举检查(参考上面的never相关demo)
对于这个类型的收敛,我们通常会用控制流分析来描述。通过上面的收敛方法,在if中进行对类型的控制,让一个变量可以被观察到不同的类型,作出不同的操作。除了上面的操作外,我们常常会自己去封装控制流的方法,也就是类型守卫。
类型守卫
为了拿到一个变量的string类型分支,我们常常会去做控制流分析。对于typeof string多次的使用,我们就会去封装一个isString的方法。以优雅我们的代码。但是当我们如下去完成一个isString。
const isString = (val: unknown): boolean => typeof val === 'string' function padLeft(padding: number | string, input: string) { if (isString(padding)) { const str: string = padding; //不能将类型“string | number”分配给类型“string”。 } // TODO xxx } 复制代码
我们会发现竟然报错了,但从我们主观类型推断的角度这padding确实已经是个string了。其实在typescript中,控制流分析是无法跨越上下文的。也就是我们padLeft函数中,我们无法从isString这个方法中捕获到类型的形象,我们只能知道他返回的是个boolean。为了解决这类问题,TS引入了 is 关键字。
语法: is 关键字 + 预期类型 我们只需要把 val is string替换掉boolean即可
const isString = (val: unknown): val is string => typeof val === 'string' function padLeft(padding: number | string, input: string) { if (isString(padding)) { const str: string = padding; //不能将类型“string | number”分配给类型“string”。 } // TODO xxx } 复制代码
同样的,vue3向外抛出的 (isRef)[vuejs.org/api/reactiv…] 方法的原理实现也添加了类型守卫
///core/packages/reactivity/src/ref.ts export function isRef<T>(r: Ref<T> | unknown): r is Ref<T> export function isRef(r: any): r is Ref { return !!(r && r.__v_isRef === true) } 复制代码
类型先行
当Typescript成为开发的附属品,javascripter会先将代码撸完,在开发过程中只写一些简单的类型声明。如果javascripter在逻辑编程上不关注类型编程。我们总能看到最后的代码成果会嵌套着大量的any , as等等不规范的操作。并不是any,as不能用。只是大多数人的使用场景,仅仅是因为改不动之前的数据结构了,或者同一个变量在许多的地方都有进行操作,而在操作的过程中,数据早就变了味(包括但是不限于可以往一个对象里来回赋值,string与number直接用+运算)。在这样的操作之后,类型系统将会变得更加的难以维护,甚至由于粗暴的as。让下一位开发者进入了一个对类型的错误判断,在这基础上进行了后续的操作。
为避免这种现象,我们可以提前进行基础类型的完成。
例如我们进行业务开发的时候的业务逻辑依赖于接口,我们就可以定义好接口的入参和出参数的interface。而这一份interface将成为我们对于这一块业务的开发基础类型。
interface IUserReq{ userId:number } interface IUserRsp{ userId:number, userName:string, QRCode:string, age:number } 复制代码
当我们将有如上的类型基准,那么接下来我们就可以从这一份类型基准,去进行开发,接下来的与user相关的代码类型,都可以是IUserRsp的衍生类型。
例如我们进行业务开发的时候的业务逻辑依赖于数据库,我们就可以定义好数据库的类型。那么我们在node开发的过程中,相关数据库数据操作数据的代码类型,都可以是数据库的衍生类型。
例如我们进行组件开发的时候,我们就可以定义好组件入参props的类型。那么我们在组件开发的过程中,相关props的组件变量类型,都可以是props的衍生类型。
类型约束和维护
不少开发者都称typescript是带着脚镣跳舞,脚镣就意味着约束。这句话的背后意思就是,我们在用TS的时候,其实是在一个约束的条件下,去完成我们的代码开发。而对于类型的约束,约束的程度就会直接反应到我们的项目代码上。
上面我们讲到组件开发以props作为类型的源头,那么以vue3组件开发为例子,我们会在setup语法糖下使用defineProps宏进行props声明。
const props = defineProps({ title: { type: String, }, UserList:{ type:Array } }); 复制代码
但是在这个props宏中,我们的声明收敛的其实非常烂,对于UserList,如果数据源来自我们上面的接口,那么这个Array是完全不满足的,我们从UserList上拿到项后直接进行取值,是毫无类型提示的,因为UserList此时是一个Array。
为了解决这个问题,vue3提供了PropType这个类型工具,以生成我们的props类型 。然后我们可以通过将type 通过as强行声明到我们通过PropType生成的props类型。也就是这样。
const props = defineProps({ title: { type: String, }, UserList:{ type:Array as PropType<User[]> } }); 复制代码
这样我们组件类型源头props才会被收敛,我们后面在基于props进行完成开发的时候,才会有类型保障。这就是类型收敛的意义。
tips:defineProps在vue3+TS可以 仅声明类型,配合withDefaults宏就可以达到上面的效果
export interface Props { title?: string; UserList?: User[]; } const props = withDefaults(defineProps<Props>(), { title: '', UserList: () => [] }); 复制代码
给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库
【面试题】 TypeScript 前端面试题 由浅到深(二):https://developer.aliyun.com/article/1414034