Skip to content

泛型对象类型

让我们想象一下,一个可以包含任何数值的盒子类型:字符串、数字、长颈鹿,等等。

typescript
interface Box { 
  contents: any; 
}

现在,内容属性的类型是任意,这很有效,但会导致下一步的意外。

我们可以使用 unknown ,但这意味着在我们已经知道内容类型的情况下,我们需要做预防性检查,或者使用容易出错的类型断言。

typescript
interface Box { 
  contents: unknown; 
}

let x: Box = { 
  contents: "hello world", 
};

// 我们需要检查 'x.contents' 
if (typeof x.contents === "string") { 
  console.log(x.contents.toLowerCase()); 
}

// 或者用类型断言 
console.log((x.contents as string).toLowerCase());

一种安全的方法是为每一种类型的内容搭建不同的盒子类型:

typescript
interface NumberBox { 
  contents: number; 
}

interface StringBox { 
  contents: string; 
}

interface BooleanBox { 
  contents: boolean; 
}

但这意味着我们必须创建不同的函数,或函数的重载,以对这些类型进行操作:

typescript
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 类型,声明一个类型参数:

typescript
interface Box<Type> { 
  contents: Type; 
}

你可以把这句话理解为:"一个类型的盒子,是它的内容具有类型的东西"。以后,当我们引用 Box 时,我们必须给一个类型参数来代替 Type 。

typescript
let box: Box<string>;

把 Box 想象成一个真实类型的模板,其中 Type 是一个占位符,会被替换成其他类型。当 TypeScript 看到 Box<string> 时,它将用字符串替换 Box<Type> 中的每个 Type 实例,并最终以 { contents: string } 这样的方式工作。换句话说, Box<string> 和我们之前的 StringBox 工作起来是一样的。

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

let boxA: Box<string> = { contents: "hello" }; 
boxA.contents; 

let boxB: StringBox = { contents: "world" }; 
boxB.contents;

盒子是可重用的,因为Type可以用任何东西来代替。这意味着当我们需要一个新类型的盒子时,我们根本不需要声明一个新的盒子类型(尽管如果我们想的话,我们当然可以)。

typescript
interface Box<Type> { 
  contents: Type; 
}

interface Apple { 
  // .... 
}

// 等价于 '{ contents: Apple }'. 
type AppleBox = Box<Apple>;

这也意味着我们可以完全避免重载,而是使用通用函数。

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

值得注意的是,类型别名也可以是通用的。我们可以定义我们新的 Box<Type> 接口:

typescript
interface Box<Type> {
  contents: Type; 
}

通过使用一个类型别名来代替:

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

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

typescript
type OrNull<Type> = Type | null; 

type OneOrMany<Type> = Type | Type[]; 

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>; 

type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

我们将在稍后回到类型别名。 通用对象类型通常是某种容器类型,它的工作与它们所包含的元素类型无关。数据结构以这种方式工作是很理想的,这样它们就可以在不同的数据类型中重复使用。