# 四、对象类型

# 1.类型别名(type)

类型别名用来给一个类型起个新名字。例如:

type isNumber = number;
const num: isNumber = 1;

上面的例子没有任何问题,当然也是一句“废话”,那么类型别名又是为什么创造的呢?来看下面的例子:

type Name = string; // 字符串
type NameResolver = () => string; // 函数
type NameOrResolver = Name | NameResolver; // 联合类型

function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}

别名常用于联合类型,如:

type ID = number | string;
const id1: ID = 123;
const id2: ID = "wpsd";

# 2.属性修饰符(Property Modifiers)

对象类型中的每个属性可以说明它的类型、属性是否可选、属性是否只读等信息。

# 2.1 可选属性(Optional Properties)

我们可以在属性名后面加一个?标记表示这个属性是可选的:

type Shape = "circle" | "square";
interface PaintOptions {
    shape: Shape;
    xPos?: number;
    yPos?: number;
}

function paintShape(opts: PaintOptions) {
    let xPos = opts.xPos; // (property) PaintOptions.xPos?: number
    let 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 特殊处理一下——解构

function paintShape({ shape, xPos = 0, yPos = 0 }: PaintOptions) {
    console.log("x coordinate at", xPos); // (parameter) xPos: number
    console.log("y coordinate at", yPos); // (parameter) yPos: number
    // ...
}

这里我们使用了解构语法以及为 xPos 和 yPos 提供了默认值。现在 xPos 和 yPos 的值在 paintShape 函数内部一定存在,但对于 paintShape 的调用者来说,却是可选的。

**注意:**现在并没有在解构语法里放置类型注解的方式。这是因为在 JavaScript 中,下面的语法代表的意思完全不同。

function draw({ shape: Shape, xPos: number = 100 /*...*/ }) {
    render(shape);
    // 找不到名称“shape”。你是否指的是“Shape”?
    render(xPos);
    // 找不到名称“xPos”
}

在对象解构语法中,shape: Shape 表示的是把shape的值赋值给局部变量 ShapexPos: number也是一样,会基于xPos创建一个名为number的变量。

# 2.2 readonly 属性(readonly Properties)

在TypeScript中,属性可以被标记为·readonly·,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为readonly的属性是不能被写入的。

interface SomeType {
    readonly prop: string;
}
function doSomething(obj: SomeType) {
    console.log(`prop has the value '${obj.prop}'.`);

    obj.prop = "hello";
    // 无法分配到 "prop" ,因为它是只读属性
}

不过使用readonly并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。readonly仅仅表明属性本身是不能被重新写入的。大家应该猜到了,如果是引用类型,则可以避开这个问题。

interface Developer {
    readonly fe: { name: string; age: number };
}

function getDeveloper(developer: Developer) {
    console.log(developer.fe.name);
    // (property) name: string
    developer.fe.name = '余光';
    developer.fe.age++;
}
 
function getDeveloper1(developer: Developer) {
    console.log(developer.fe.name);
    // (property) name: string
    developer.fe = {
        name: "余光",
        age: 18,
    };
    // 无法分配到 "fe" ,因为它是只读属性。ts(2540)
}

TypeScript 在检查两个类型是否兼容的时候,并不会考虑两个类型里的属性是否是readonly,这就意味着,readonly 的值是可以通过别名修改的。

interface Person {
    name: string;
    age: number;
}

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

let writablePerson: Person = {
    name: "Person McPersonface",
    age: 42,
};

// works
let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

# 2.3 索引签名(Index Signatures)

有的时候,你不能提前知道一个类型里的所有属性的名字,但是你知道这些值的特征。

这种情况,你就可以用一个索引签名 (index signature) 来描述可能的值的类型,举个例子:

interface StringArray {
    [index: number]: string;
}

const myArray: StringArray = [1, 2, 3];// ❌ 不能将类型“number”分配给类型“string”
const secondItem = myArray[1]; // const secondItem: string

这样,我们就有了一个具有索引签名的接口StringArray,一个索引签名的属性类型必须是 string 或者是 number。

# 3.属性继承(Extending Types)

有时我们需要一个比其他类型更具体的类型。举个例子,假设我们有一个BasicGoods类型用来描述一个商品的基本信息

interface BasicGoods {
    color: string;
    size: string;
    brand: string;
    address: string;
}

这在一些情况下已经满足了,但同一个品牌的商品可能在,不同的分类中,例如:

interface BasicGoodsWithCategory {
    color: string;
    size: string;
    brand: string;
    address: string;
    category: string
}

这样写固然可以,但为了加一个字段,就是要完全的拷贝一遍。我们可以改成继承BasicGoods的方式来实现:

interface BasicGoodsWithCategory extends BasicGoods{
    category: string
}

对接口使用extends关键字允许我们有效的从其他声明过的类型中拷贝成员,并且随意添加新成员。

接口也可以继承多个类型:

interface Colorful {
    color: string;
}

interface Circle {
    radius: number;
}

interface ColorfulCircle extends Colorful, Circle {}

const cc: ColorfulCircle = {
    color: "red",
    radius: 42,
};

# 4.交叉类型(Intersection Types)

TypeScript 也提供了名为交叉类型的方法,用于合并已经存在的对象类型。交叉类型的定义需要用到&操作符:

interface Colorful {
    color: string;
}
interface Circle {
    radius: number;
}

type group = Colorful & Circle;

function draw(circle: group) {
    console.log(`Color was ${circle.color}`); // (property) Circle.radius: number
    console.log(`Radius was ${circle.radius}`); // (property) Circle.radius: number
}

这里,我们连结ColorfulCircle产生了一个新的类型,新类型拥有ColorfulCircle的所有成员。

# 5.接口继承与交叉类型(Interfalces vs Intersections)

这两种方式在合并类型上看起来很相似,但实际上还是有很大的不同。最原则性的不同就是在于冲突怎么处理,这也是你决定选择那种方式的主要原因。

interface Colorful {
    color: string;
}
interface ColorfulSub extends Colorful {
    color: number;
}
// ❌
// 接口“ColorfulSub”错误扩展接口“Colorful”。
// 属性“color”的类型不兼容。
// 不能将类型“number”分配给类型“string”。

使用继承的方式,如果重写类型会导致编译错误,但交叉类型不会:

interface Colorful {
    color: string;
}

type ColorfulSub = Colorful & {
    color: number;
};

虽然不会报错,那 color 属性的类型是什么呢,答案是never,取得是stringnumber交集

# 6.泛型对象类型(Generic Object Types)

让我们写这样一个Box类型,可以包含任何值,此时需要做一些预防检查,或者用一个容易错误的类型断言。

interface Box {
    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 assertion
console.log((x.contents as string).toLowerCase());

一个更加安全的做法是将 Box 根据 contents 的类型拆分的更具体一些:

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;
}

这样写就太繁琐了。此时引入一个概念——泛型,反省Box ,它声明了一个类型参数 (type parameter):

interface Box<Type> {
    contents: Type;
}

你可以这样理解:Box的Type就是contents拥有的类型Type。当我们引用Box的时候,我们需要给予一个类型实参替换掉Type

let aaa: Box<string> = {
    contents: 1
};
// ❌ 不能将类型“number”分配给类型“string”。

把 Box 想象成一个实际类型的模板,Type 就是一个占位符,可以被替代为具体的类型。当 TypeScript 看到 Box<string>,它就会替换为 Box<Type>Typestring ,最后的结果就会变成 { contents: string }。换句话说,Box<string>StringBox 是一样的。

interface Box<Type> {
  contents: Type;
}
interface StringBox {
  contents: string;
}

不过现在的 Box 是可重复使用的,如果我们需要一个新的类型,我们完全不需要再重新声明一个类型。

interface Box<Type> {
  contents: Type;
}
 
interface Apple {
  // ....
}
 
// Same as '{ contents: Apple }'.
type AppleBox = Box<Apple>;
// 这也意味着我们可以利用泛型函数避免使用函数重载。

function setContents<Type>(box: Box<Type>, newContents: Type) {
  box.contents = newContents;
}

# 6.1 类型别名与泛型

interface Box<Type> {
    contents: Type;
}

使用别名对应就是:

type Box<Type> = {
    contents: Type;
};

类型别名不同于接口,可以描述的不止是对象类型,所以我们也可以用类型别名写一些其他种类的的泛型帮助类型。

type OrNull<Type> = Type | null;
 
type OneOrMany<Type> = Type | Type[];
 
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
           
type OneOrManyOrNull<Type> = OneOrMany<Type> | null
 
type OneOrManyOrNullStrings = OneOrManyOrNull<string>;
               
type OneOrManyOrNullStrings = OneOrMany<string> | null

现代 JavaScript 也提供其他是泛型的数据结构,比如 Map<K, V>Set<T>Promise<T>。因为 Map 、Set 、Promise的行为表现,它们可以跟任何类型搭配使用。

# 7.字符串字面量类型

字符串字面量类型用来约束取值只能是某几个字符串中的一个。

type EventNames = "click" | "scroll" | "mousemove";
function handleEvent(ele: Element, event: EventNames) {
  // do something
}

handleEvent(document.getElementById("hello"), "scroll"); // 没问题
handleEvent(document.getElementById("world"), "onmouseout"); // 报错,event 不能为 'onmouseout'

上例中,我们使用 type 定了一个字符串字面量类型 EventNames,它只能取三种字符串中的一种。

# 写在最后

参考:

欢迎Star⭐️

Last Updated: 12/5/2022, 9:45:07 PM