【Typescript专题】之类型进阶
🚀【TypeScript入门手册】记录了出场率较高的Ts概念,旨在帮助大家了解并熟悉Ts
🎉 本系列会持续更新并更正,重点关照大家感兴趣的点,欢迎同学留言交流,在进阶之路上,共勉!
👍 star本项目给作者一点鼓励吧
📚 系列文章,收藏 不走丢哦
一、类型别名(type)
类型别名用来给一个类型起个新名字。例如:
typeisNumber=number;const num: isNumber =1;
上面的例子没有任何问题,当然也是一句“废话”,那么类型别名又是为什么创造的呢?来看下面的例子:
typeName=string;// 字符串typeNameResolver=()=>string;// 函数typeNameOrResolver= Name | NameResolver;// 联合类型functiongetName(n: NameOrResolver): Name {if(typeof n ==="string"){return n;}else{returnn();}}
别名常用于联合类型,如:
typeID=number|string;const id1:ID=123;const id2:ID="wpsd";
二、属性修饰符(Property Modifiers)
对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。
2.1 可选属性(Optional Properties)
我们可以在属性名后面加一个
?
标记表示这个属性是可选的:
typeShape="circle"|"square";interfacePaintOptions{
shape: Shape;
xPos?:number;
yPos?:number;}functionpaintShape(opts: PaintOptions){let xPos = opts.xPos;// (property) PaintOptions.xPos?: numberlet yPos = opts.yPos;// (property) PaintOptions.yPos?: number}const shape ='circle';paintShape({ shape });paintShape({ shape, xPos:100});paintShape({ shape, yPos:100});paintShape({ shape, xPos:100, yPos:100});
在这个例子中,xPos 和 yPos 就是可选属性。因为他们是可选的,所以上面所有的调用方式都是合法的。
在 JavaScript 中,如果一个属性值没有被设置,我们获取会得到 undefined 。所以我们可以针对 undefined 特殊处理一下——解构
functionpaintShape({ shape, xPos =0, yPos =0}: PaintOptions){console.log("x coordinate at", xPos);// (parameter) xPos: numberconsole.log("y coordinate at", yPos);// (parameter) yPos: number// ...}
这里我们使用了解构语法以及为 xPos 和 yPos 提供了默认值。现在 xPos 和 yPos 的值在 paintShape 函数内部一定存在,但对于 paintShape 的调用者来说,却是可选的。
注意:现在并没有在解构语法里放置类型注解的方式。这是因为在 JavaScript 中,下面的语法代表的意思完全不同。
functiondraw({ shape: Shape, xPos:number=100/*...*/}){render(shape);// 找不到名称“shape”。你是否指的是“Shape”?render(xPos);// 找不到名称“xPos”}
在对象解构语法中,
shape: Shape
表示的是把shape的值赋值给局部变量
Shape
。
xPos: number
也是一样,会基于xPos创建一个名为
number
的变量。
2.2 readonly 属性(readonly Properties)
在TypeScript中,属性可以被标记为·readonly·,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为
readonly
的属性是不能被写入的。
interfaceSomeType{readonly prop:string;}functiondoSomething(obj: SomeType){console.log(`prop has the value '${obj.prop}'.`);
obj.prop ="hello";// 无法分配到 "prop" ,因为它是只读属性}
不过使用
readonly
并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。
readonly
仅仅表明属性本身是不能被重新写入的。大家应该猜到了,如果是引用类型,则可以避开这个问题。
interfaceDeveloper{readonly fe:{ name:string; age:number};}functiongetDeveloper(developer: Developer){console.log(developer.fe.name);// (property) name: string
developer.fe.name ='余光';
developer.fe.age++;}functiongetDeveloper1(developer: Developer){console.log(developer.fe.name);// (property) name: string
developer.fe ={
name:"余光",
age:18,};// 无法分配到 "fe" ,因为它是只读属性。ts(2540)}
TypeScript 在检查两个类型是否兼容的时候,并不会考虑两个类型里的属性是否是
readonly
,这就意味着,
readonly
的值是可以通过别名修改的。
interfacePerson{
name:string;
age:number;}interfaceReadonlyPerson{readonly name:string;readonly age:number;}let writablePerson: Person ={
name:"Person McPersonface",
age:42,};// workslet readonlyPerson: ReadonlyPerson = writablePerson;console.log(readonlyPerson.age);// prints '42'
writablePerson.age++;console.log(readonlyPerson.age);// prints '43'
2.3 索引签名(Index Signatures)
有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征。
这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型,举个例子:
interfaceStringArray{[index:number]:string;}const myArray: StringArray =[1,2,3];// ❌ 不能将类型“number”分配给类型“string”const secondItem = myArray[1];// const secondItem: string
这样,我们就有了一个具有索引签名的接口
StringArray
,一个索引签名的属性类型必须是 string 或者是 number。
三、属性继承(Extending Types)
有时我们需要一个比其他类型更具体的类型。举个例子,假设我们有一个
BasicGoods
类型用来描述一个商品的基本信息
interfaceBasicGoods{
color:string;
size:string;
brand:string;
address:string;}
这在一些情况下已经满足了,但同一个品牌的商品可能在,不同的分类中,例如:
interfaceBasicGoodsWithCategory{
color:string;
size:string;
brand:string;
address:string;
category:string}
这样写固然可以,但为了加一个字段,就是要完全的拷贝一遍。我们可以改成继承
BasicGoods
的方式来实现:
interfaceBasicGoodsWithCategoryextendsBasicGoods{
category:string}
对接口使用
extends
关键字允许我们有效的从其他声明过的类型中拷贝成员,并且随意添加新成员。
接口也可以继承多个类型:
interfaceColorful{
color:string;}interfaceCircle{
radius:number;}interfaceColorfulCircleextendsColorful, Circle {}const cc: ColorfulCircle ={
color:"red",
radius:42,};
四、交叉类型(Intersection Types)
TypeScrip 也提供了名为交叉类型的方法,用于合并已经存在的对象类型。交叉类型的定义需要用到
&
操作符:
interfaceColorful{
color:string;}interfaceCircle{
radius:number;}typegroup= Colorful & Circle;functiondraw(circle: group){console.log(`Color was ${circle.color}`);// (property) Circle.radius: numberconsole.log(`Radius was ${circle.radius}`);// (property) Circle.radius: number}
这里,我们连结
Colorful
和
Circle
产生了一个新的类型,新类型拥有
Colorful
和
Circle
的所有成员。
组合和继承,在上面的两个例子中解决的问题是一样的,那么怎么区分呢?
五、接口继承与交叉类型(Interfalces vs Intersections)
这两种方式在合并类型上看起来很相似,但实际上还是有很大的不同。最原则性的不同就是在于冲突怎么处理,这也是你决定选择那种方式的主要原因。
interfaceColorful{
color:string;}interfaceColorfulSubextendsColorful{
color:number;}// ❌// 接口“ColorfulSub”错误扩展接口“Colorful”。// 属性“color”的类型不兼容。// 不能将类型“number”分配给类型“string”。
使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会:
interfaceColorful{
color:string;}typeColorfulSub= Colorful &{
color:number;};
虽然不会报错,那 color 属性的类型是什么呢,答案是
never
,取得是
string
和
number
的交集。
六、泛型对象类型(Generic Object Types)
让我们写这样一个Box类型,可以包含任何值,此时需要做一些预防检查,或者用一个容易错误的类型断言。
interfaceBox{
contents:unknown;}let x: Box ={
contents:"hello world",};// we could check 'x.contents'if(typeof x.contents ==="string"){console.log(x.contents.toLowerCase());}// or we could use a type assertionconsole.log((x.contents asstring).toLowerCase());
一个更加安全的做法是将 Box 根据 contents 的类型拆分的更具体一些:
interfaceNumberBox{
contents:number;}interfaceStringBox{
contents:string;}interfaceBooleanBox{
contents:boolean;}
但是这也意味着我们不得不创建不同的函数或者函数重载处理不同的类型:
functionsetContents(box: StringBox, newContents:string):void;functionsetContents(box: NumberBox, newContents:number):void;functionsetContents(box: BooleanBox, newContents:boolean):void;functionsetContents(box:{ contents:any}, newContents:any){
box.contents = newContents;}
这样写就太繁琐了。此时引入一个概念——泛型,反省
Box
,它声明了一个类型参数 (type parameter):
interfaceBox<Type>{
contents: Type;}
你可以这样理解:Box的
Type
就是
contents
拥有的类型
Type
。当我们引用
Box
的时候,我们需要给予一个类型实参替换掉
Type
:
let aaa: Box<string>={
contents:1};// ❌ 不能将类型“number”分配给类型“string”。
把 Box 想象成一个实际类型的模板,Type 就是一个占位符,可以被替代为具体的类型。当 TypeScript 看到
Box<string>
,它就会替换为
Box<Type>
的
Type
为
string
,最后的结果就会变成
{ contents: string }
。换句话说,
Box<string>
和
StringBox
是一样的。
interfaceBox<Type>{
contents: Type;}interfaceStringBox{
contents:string;}
不过现在的 Box 是可重复使用的,如果我们需要一个新的类型,我们完全不需要再重新声明一个类型。
interfaceBox<Type>{
contents: Type;}interfaceApple{// ....}// Same as '{ contents: Apple }'.typeAppleBox= Box<Apple>;// 这也意味着我们可以利用泛型函数避免使用函数重载。functionsetContents<Type>(box: Box<Type>, newContents: Type){
box.contents = newContents;}
6.1 类型别名与泛型
interfaceBox<Type>{
contents: Type;}
使用别名对应就是:
typeBox<Type>={
contents: Type;};
类型别名不同于接口,可以描述的不止是对象类型,所以我们也可以用类型别名写一些其他种类的的泛型帮助类型。
typeOrNull<Type>= Type |null;typeOneOrMany<Type>= Type | Type[];typeOneOrManyOrNull<Type>= OrNull<OneOrMany<Type>>;typeOneOrManyOrNull<Type>= OneOrMany<Type>|nulltypeOneOrManyOrNullStrings= OneOrManyOrNull<string>;typeOneOrManyOrNullStrings= OneOrMany<string>|null
现代 JavaScript 也提供其他是泛型的数据结构,比如 Map<K, V> , Set 和 Promise。因为 Map 、Set 、Promise的行为表现,它们可以跟任何类型搭配使用。
七、字符串字面量类型
字符串字面量类型用来约束取值只能是某几个字符串中的一个。
typeEventNames="click"|"scroll"|"mousemove";functionhandleEvent(ele: Element, event: EventNames){// do something}handleEvent(document.getElementById("hello"),"scroll");// 没问题handleEvent(document.getElementById("world"),"onmouseout");// 报错,event 不能为 'onmouseout'
上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。
写在最后
本篇文章是《Typescript基础入门》第四篇,本篇文章主要聊一聊基本类型相关的扩展,毕竟随着文章的加深,我在阅读官方文档的时候经常会见到陌生的声明和“单词”,感觉有必要跟大家分享一下,下一篇文章,我们主要聊一聊Ts的泛型。
参考:
- TypeScript文档
- TypeScript4中文文档
关于我:
- 花名:余光
- 邮箱:webbj97@163.com
- csdn:传送门
其他沉淀:
- Github: Js版LeetCode题解
- 余光的前端成长笔记
- 高频手撕代码系列
版权归原作者 余光、 所有, 如有侵权,请联系我们删除。