网站搜索

TypeScript 接口实现中带有额外参数对象成员不会产生预期的编译错误


考虑以下类型:

type User = {
  id: string;
  name: string;
};

interface UsersRepository {
  getUser(query: { id: string }): User;
}

class Repo implements UsersRepository {
  getUser({ id, name }: { id: string; name: string }) {
    return { id, name: name.toUpperCase() };
  }
}

const illustrateProblem = () => {
  const repo: UsersRepository = new Repo();

  console.log(repo.getUser({ id: "foo" }));
};

illustrateProblem();

请注意,实施方法要求输入对象具有两个属性:id和name,而接口仅要求id。

这不会产生任何编译错误!

然而,在运行时,这段代码会失败,因为对 repo.getUser 的调用没有在其参数中提供 name,因此它将是 undefined,name.toUpperCase() 会抛出错误。换句话说,在运行时 name 是 undefined,尽管 TypeScript 编译器认为它永远不会是这样。

为什么 TypeScript 允许这样?

澄清:我知道这个实现使得 LSP 失效,而这正是重点:我本以为 TypeScript 会因为我这么做而报错,在 getUser 实现的声明上给出错误,指出该方法无法实现接口,因为它在参数上需要的字段在接口的签名中没有定义。

答案#1

您所遇到的问题是由于 TypeScript 中方法参数的双变性造成的。这是类型系统不安全的地方之一,允许您做一些不安全的操作。显然在很多实际代码中,子类期望方法接受更具体的参数而不是更通用的参数。TypeScript 通过有意接受更宽(安全)或更窄(不安全)参数来支持这一点,而您不幸恰好遇到了这个问题。

幸运的是,自 TypeScript 2.6 起,已经有一个 --strictFunctionTypes 编译器标志可以禁用函数参数双变性(并且只允许逆变性)。不幸的是,即使启用了此标志,它也仅适用于独立函数,而不适用于类方法。幸运的是,通过接口实际上有一种(文档不多的)方法可以区分方法签名和函数值属性签名:

interface Foo<A, R> {
   methodSignature(arg: A): R;
   functionSignature: (arg: A) => R;
}

无论是 methodSignature 还是 functionSignature 都指的是挂在 Foo 上的类似函数的东西,在实践中,你可以使用基于原型的方法或具有实例值的函数属性来实现任意一种。而且,幸运的是,函数属性签名会遵守 --strictFunctionTypes。因此,我会将 UsersRepository 的定义改为如下:

interface UsersRepository {
  getUser: (a: { id: string }) => User;
}

然后你会突然看到你所预期的错误:

class Repo implements UsersRepository {
  getUser({ id, name }: { id: string; name: string }) {
    // error!  Property 'name' is missing in type '{ id: string; }'
    // but required in type '{ id: string; name: string; }'.
    return { id, name: name.toUpperCase() };
  }
}

因此你可以修复它:

class FixedRepo implements UsersRepository {
  getUser({ id, name }: { id: string; name?: unknown }) {
    return {
      id,
      name: (typeof name === "string" ? name : "nobody").toUpperCase()
    };
  }
}

const illustrateProblem = () => {
  const repo: UsersRepository = new FixedRepo();
  console.log(repo.getUser({ id: "foo" }));
};

illustrateProblem(); // { id: "foo", name: "NOBODY" }

好的,希望有帮助;祝你好运!

代码链接