第三节:TypeScript对象类型

对象类型

在 JavaScript 中,我们分组和传递数据的基本方式是通过对象。在 TypeScript 中,我们通过对象类型来表示它们。

而TypeScript的核心原则之一是对值所具有的的结构进行类型检查,


1. 对象类型限制

1.1 数据类型限制

之前的数据类型中已经了解,如何限定变量的类型

例如:

// 限定变量类型
let userName: string = "张三"
console.log('userName', userName)   // 张三


如果变量的值不是基本数据类型的值,而是一个对象,可以使用object类型

let student: object = {
    name: '张三',
    age: 18
}
console.log(student)   // {name: "张三", age: 18}



这种写法只能限定变量是object 类型, 但是没法明确表明或限定对象内部属性以及值类型,

也就是说示例中name可是是任意数据类型;属性也不限定于nameage

如果要限定对象属性值的类型,就需要使用字面量的方式进行类型注释

// 变量值为对象的属性限制
let user: {name:string,age:number} = {name:'张三', age: 8}
console.log('user', user)   // user  {name: "张三", age: 8}

这样的写法虽然达到了限定对象内部结构, 但同时也带来了另外的问题,如属性过多,或多次复用相同类型注释.

因此可以定义接口或 类型别名来定义统一的对象属性限制


1.2 interface 接口

通过接口命名对象类型

// 定义接口
interface User{
  name:string,
  age:number
}

// 使用接口限定变量
let user: User = {name:'张三', age: 8}
console.log('user', user)


1.3 类型别名

或者使用类型别名来命名对象类型

// 定义类型别名
type User = {
  name:string,
  age:number
}

// 使用类型别名
let user: User = {name:'张三', age: 8}
console.log('user', user)
注意事项:

但是也需要注意到,在给使用了接口和类型别名时, 变量值接受对象中的属性必须和接口或类型别名中定义的属性一致.,多了少了编译时都会报错



// 接口
interface User {
  name:string
  age: number
}

// 1. 接口不符
let user:User = {name:'张三',age:18,sex:1}
/*
  报错:  
    不能将类型“{ name: string; age: number; sex: number; }”分配给类型“User”。
    对象文字可以只指定已知属性,并且“sex”不在类型“User”中。
*/ 

let user2:User = {name:'张三'}
// 报错:  类型 "{ name: string; }" 中缺少属性 "age",但类型 "User" 中需要该属性


// 类型别名
type Person = {
  name:string
  age: number
}
// 2. 类型别名不符
let user3:Person = {name:'张三',age:18,sex:1}
/*
  不能将类型“{ name: string; age: number; sex: number; }”分配给类型“Person”。
  对象文字可以只指定已知属性,并且“sex”不在类型“User”中。
*/ 

let user4:Person = {name:'张三'}
// 类型 "{ name: string; }" 中缺少属性 "age",但类型 "Person" 中需要该属性


2. 对象属性修饰符

为了让对象类型更具灵活性, 对象类型中每个属性都可以指定几件事:

  1. 属性类型
  2. 属性是否是可选的
  3. 属性是否是可以写入的(修改属性值)


2.1 可选属性

很多时候,在处理对象类型的时候, 某些属性可能并不一定存在. 这个时候就需要用到可选属性.

可选属性通过在属性名称末尾添加?来将这些属性标记为可选

例如:

// 1. 接口
interface User{
  age:number,
  name:string,
  sex?: string
}

 // 使用接口
let user: User = {name:'张三', age: 18,sex:"男"}
console.log('user', user)  // user {name: "张三", age: 18, sex: "男"}

let user2: User = {name:'张三', age: 18}
console.log('user2', user2)  // user {name: "张三", age: 18}


// 2. 类型别名定义可选属性
type Person = {
  age:number,
  name:string,
  sex?: string
}

 // 使用类型别名
let user3: User = {name:'张三', age: 18,sex:"男"}
console.log('user3', user3)  // user3 {name: "张三", age: 18, sex: "男"}

可选属性在进行检测时,可选属性在实现上可有可无,这样就提升了对象类型使用的灵活性.

可选属性的优点
  1. 对可能存在的属性进行预定义
  2. 可以捕获引用中不存在的属性时的错误,

例如:

// 接口
interface User{
  age:number,
  name:string,
  sex?: number
}

let user: User;

/*
  访问对象类型中不存在的属性
  虽然我们知道在JavaScript中会返回undefined
  但在TypeScript类型检查报错
*/ 
console.log(user.read)
// 报错: 类型“User”上不存在属性“read”。

// 访问可选属性,不会报错
console.log(user.sex)


2.2 对象的只读属性

在TypeScript中对象属性也可以标记为readonly只读属性

只读属性虽然不会在运行时更改任何行为.但在类型检查期间无法写入只读标记的属性

通过在属性名前添加readonly来标记只读属性

例如:

// 接口
interface User{
  readonly name:string,  // 只读属性name
  age:number,
}

// 接口类型注释
let user: User = {name:'张三', age: 18}
console.log('user', user)   // user {name: '张三', age: 18}

// 访问只读属性
console.log('name', user.name) //name 张三 

// 修改只读属性
user.name = '李四'
// 报错: 无法分配到 "name" ,因为它是只读属性。



// 类型别名定义只读属性
type Person = {
  readonly name:string,
  age:number,
}

此时name属性标记为只读属性, 当你尝试修改只读属性值时, TypeScript报错,提示你不可修改

使用readonly修饰符并不一定意味着一个值完全不可修改, 也就是说属性属性值是一个引用类型的值,比如对象

readonly只是表示当前属性本身不能被重写, 但属性值是引用类型, 引用类型内部的值是完全可变的

例如:

// 接口
interface User{
 name:string,  // 只读属性name
  age:number,
  readonly friend: Friend
}

interface Friend{
  name: string,
  sex: number
} 

// 接口类型注释
let user: User = {name:'张三', age: 18, friend: {name:'小明',sex:1}}

// 修改只读属性内部的值
user.friend.name = '李四'
console.log('user', user)
/*
  {
    name: "张三",
    age: 18,
    friend: {name: '李四', sex: 1}

  }
*/

示例中只读属性friend内部的属性name被修改了, 没有任何报错.

因为TypeScript在检查这些类型是否兼容时不会考虑两种类型内部的属性是否有readonly标记存在, 所以readonly属性也可以通过别名来更改, 也就是说将有只读属性的类型重新分配给没有只读属性的类型

interface Person{
    name:string
    age:number
}

interface ReadonlyPerson{
    readonly name:string
    readonly age:number
}

// person 类型定义的属性是可以修改的
const person: Person = {
    name:'张三',
    age:18
}

// readonlyPerson 的属性是只读属性不可修改
// 但是person对象可以修改,又由于TypeScript检查两个类型值能否赋值判断属性是否匹配,忽略readonly
// 因此ReadonlyPerson类型的值可以赋值给Person类型的变量
// 又由于JavaScript对象引用类型的关系
// 当person值被修改值, readonlyPerson 也会被修改
const readonlyPerson: ReadonlyPerson = person

console.log(readonlyPerson.age)  // 18
person.age = 30
console.log(readonlyPerson.age)  // 30


2.3 索引签名

有的时候你并不提前知道类型属性的所有属性名, 但你确实知道属性值的类型

在这样的情况下, 你可以使用索引签名来描述可能值的类型,

例如:

// 索引签名
interface StringArray{
    [index: number]: string
}

const arr:stringArray = ['hello','world']

const val = arr[1]  // 使用索引获取值
// const val:string

示例中所以签名的意思,表示使用number类型的索引获取值时,返回string类型

注意:索引签名的属性类型必须是字符串或数字

TypeScript索引签名可以同时支持两种类型的索引器.

虽然字符串索引签名是描述'字典'模式的强大方式, 但TypeScript 还是强制所有的属性与其返回的类型匹配

例如:

interface Person{
    [index: string]: string  // 索引签名为通用模式, 属性为字符串,值也为字符串
    // 除了通用的索引签名, 还可以具体罗列属性
    name: string
    
    // 具体罗列的属性的类型,必须是索引签名值的类型的子集
    age: number // 此时报错,number类型不能分配给字符串索引签名
    
}

示例中, age属性的类型是number类型, 将会出现错误,因为与索引签名冲突,

其实也很好理解, 因为在使用age属性时, 无论通过,obj.age, 还是obj["age"], 其都符合索引签名的模式, 返回的值类型应该是string类型, 可是你有明确的声明了age属性的返回值是明确的number类型

此时TypeScript不知道使用索引签名的规则来检查值类型,还是具体罗列age属性的类型来检查值类型


这种问题,可以通过给索引签名使用联合类型解决

interface Person{
    [index: string]: string | number  //  索引签名类型为联合类型
    name: string
    age: number // number类型是 索引签名类型的子集
}

// 此时使用age属性, 返回的值就是number类型
// 既符合索引签名的联合类型中的number, 也符合具体age属性返回的number类型
let user: Person;
let age = user.age;
// let age: number


最后你 还可以在索引签名上使用只读属性readonly, 表示不可以给索引分配值

例如:只读索引签名

interface Person{
    readonly [index: string]: string
}

const student:Person = {
    name:'张三',
}

const uname = student['name']
student.name = '李四'  // 报错:类型Person的 索引签名为只读属性(不可修改值)


3. 扩展类型

在实际使用时,一个类型有可能是其他类型的具体版本的类型很常见,

简单说就是, 一个类型只有另外一个类型中的部分信息

例如:

有一下两个类型:

人员基本信息,包含姓名,年纪信息

具体学生信息: 包含除了姓名,年纪外还有学号,班级等信息

// 基本信息接口
interface Person{
    name:string
    age: number
}

// 具体信息接口
interface StudentPerson{
    name:string
    age: number
    studentNum: number
    classNum: number
}

示例中,StudentPerson包含Person所有的属性信息, 也可以说是Person类型更详细的类型,

试想一下,如果每个定义包含name,age属性以及其他不同属性类型时,我们都像示例中把name,age属性重新定义一遍.

这样的使用方式会导致name,age属性大量重复


解决这样重复声明一个类型中所有的属性,我们就可以使用extends关键字扩展原有类型, 并添加新的属性

// 基本信息接口
interface Person{
    name:string
    age: number
}

// 具体信息接口
interface StudentPerson extends Person{
    studentNum: number
    classNum: number
}

关键字extends允许我们有效的从其他命名类型复制成员, 并添加我们想要的新成员


同样interface接口也允许从多个接口中扩展新的类型

// 人员信息接口
interface Person {
    name: string
    age: number
}

// 工作信息接口
interface Work{
    work: string 
}

// 工作人员信息接口
interface WorkPerson extends Person,Work{
    jobNum: number
}

这样我们就可以使用WorkPerson接口来注释一个具有姓名,年纪, 工作信息,工号属性对象的类型


4. 交叉类型

interface 允许我们通过extends扩展其他类型来构建新的类型

TypeScript中还为类型别名提供了另外一种称之为交叉类型的类型扩展方式. 主要用于组合现有类型

使用&运算符定义交叉类型

// 接口
interface Colorful{
    color:string
}
// 接口
interface Circle{
    radius: number
}

// 类型别名
type ColorCircle = Colorful & Circle;


// 使用类型别名
let obj:ColorCircle = {color:'red',radius: 50}
/*
  let obj: ColorCircle
  type ColorCircle = Colorful & Circle
*/ 

示例中,通过交叉类型组合ColorfulCircle生成一个新的类型别名,类型别名同时具有前两个类型的所有属性


也可以在类型注释的时候使用交叉类型

// 接口
interface Colorful{
    color:string
}
// 接口
interface Circle{
    radius: number
}


// 类型注释时使用交叉类型
let obj:Colorful & Circle = {color:'red',radius: 50}
/*
  let obj: ColorCircle
*/ 


扩展类型与交叉类型的不同
  1. extends 是扩展原有类型, 也就是继承原有类型的同时添加新的属性
  2. &只是将需要的多个类型进行并集然后合并形成一个新的类型, 不能添加新的属性

理解两者的主要区别,方便我们在使用时做出取舍


5. 通用对象类型

5.1 思考通用对象类型

通用对象类型:就是需要定义一个可以通用类型,

例如:定义一个Box类型,具有contents属性, 但是属性的值可能是string, number, 等各种类型

首先会想到的是属性值类型使用联合类型

interface Box{
    contents: string | number
}

但联合类型也仅仅是罗列我们已知的类型, 在使用场景下可能并不通用, 例如值也有可能是其他对象类型呢


此时也许会考虑any任何类型

interface Box{
    contents: any
}

any类型可以工作,但是可能会导致意外事故发生


也可以尝试定义unknown类型

使用unknown类型就意味着我们需要进行类型检查,或者使用类型断言

interface Box{
    contents: unknown
}

const student :Box = {
    contents: 'hello'
}

// 报错: unknow类型上不存在toUpperCase方法
// student.contents.toUpperCase()

// 使用类型判断
if(typeof student.contents === 'string'){
    student.contents.toUpperCase()
}

// 使用类型断言
(student.contents as string).toUpperCase()


不过unknow也不是特别安全, 比较安全的做法是为每一种类型添加一个接口

interface NumberBox {
    contents: number;
}

interface StringBox {
    contents: string;
}

interface BooleanBox {
    contents: boolean;
}

但这意味着如果是给函数参数使用, 我们需要创建不同函数或函数重载, 才能对这些进行操作

function setContents(box: StringBox, newContents: string): void;
function setContents(box: NumberBox, newContents: number): void;
function setContents(box: BooleanBox, newContents: boolean): void;
function setContents(box: { contents: any }, newContents: any) {
  box.contents = newContents;
}

这样的处理方式不仅繁琐, 而且之后需要需要新的类型还需要引入新的类型和重载, 因为我们contents类型和重载实际上都是相同的,


5.2 泛型的使用

最好的处理方式,就是我们创建一个声明类型参数的泛型,

其实就是将类型定义为像函数参数或变量一样, 类型参数就可以在多个地方使用, 通过传递具体类型,让使用类型参数的地方全部指代当前具体类型

// 泛型接口  Type为类型参数, 就像变量或函数参数
interface Box<Type>{
    contents: Type
}

此时当我们在使用Box类型注释时,必须给出一个类型参数来代替Type

let box:Box<string>

此时会将Box视为类型模板,其中Type为占位符将被其他类型替换,

// 也就是说类型中的Type被string替换
// 实际等价于
interface Box<string>{
    contents: string
}


Box类型可以重复使用, Type可以用任何类型代替, 这意味着当我们需要一个新类型Box是, 我们根本不要在 声明一个新的Box类型, 我们只需要传递不同的类型替换Type即可

// 泛型接口
interface Box<Type>{
    contents: Type
}

// 接口
interface Person{
    name: string
    age:number
}

// 也可以用已经定义接口类型作为类型参数
let box:Box<Person> = {
    contents: {
        name:'张三',
        age: 18
    }
}


这也意味着,如果将类型用在函数参数上,我们可以通过使用泛型函数来避免重载

interface Box<Type>{
    contents: Type
}
function setContent<Type>(box:Box<Type>,newContent:Type){
    box.contents =  newContent
}

const obj = {
    contents: 'hello'
}
setContent(obj,'world')

示例中, 我们并没有限定obj的类型, 但是在传递参数后, TypeScript根据入参推断出Type是一个string 类型, 因此函数的第二个参数也必须是一个字符串类,否则TypeScript将发出错误警告

例如:如下调用函数

setContent(box,10)
// 不能将number类型分配给string类型


类型的别名也是可以通用的

例如

interface Box<Type> {
  contents: Type;
}

Box接口也可以使用类型别名来替换

type Box<Type> = {
    contents:Type
}


由于类型别名与接口不同,类型别名不仅仅可以描述对象,还可以使用类型别名来编写其他类型的通用帮助类型

// 定义通用类型或Null类型的 类型别名
type OrNull<Type> = Type | null

// 定义通用类型或通用类型数组的 类型别名
type OneOrMany<Type> = Type | Type[]

// type  OneOrManyOrNull<Type> = OneOrMany<Type> | null
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>

// type OneOrManyOrNullStrings = OneOrMany<string> | null
type OneOrManyOrNullStrings =  OneOrManyOrNull<string> 
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容