首页 > 解决方案 > 打字稿不同的类属性类型取决于参数

问题描述

我有一个User类,我用一个对象实例化它,这可能是null如果用户没有经过身份验证。我想根据其状态制作不同的属性类型。这是我尝试过的:

// `IUser` is an interface with user properties like `id`, `email`, etc.
class User<T extends IUser | null> implements IUser {
  id: T extends null ? null : string
  email: T extends null ? null : string
  age: T extends null ? null : number
  // ...

  authenticated: T extends null ? false : true

  constructor(user: T) {
    if(user === null) {
      this.authenticated = false
      this.id = this.email = this.age = null
    } else {
      this.authenticated = true
      ;({
        id: this.id,
        email: this.email,
        age: this.age,
      } = user)
    }
  }
}

Type 'true' is not assignable to type 'T extends null ? false : true'但是这个解决方案在设置字段时会导致错误,以及子句中authenticated的一堆错误。我的问题与这个问题有关,其中的响应建议做。事实上,这确实消除了一个错误,但这个令人头疼的重点是能够编写property 'email' doesn't exist on type 'IUser | null'elsethis.authenticated = true as any

if(user.authenticated) {
  // email is certainly not null
  const { email } = user 
}

这不适用于此解决方案。

标签: typescriptclassgenericstypes

解决方案


看来您希望User类型是有区别的联合,例如{authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...}. 不幸的是,class类型interface本身不能是联合,所以没有简单的方法来表示它。

您对条件类型的尝试只能在类实现的外部起作用,并且只有当您user的类型是 type时才可能起作用User<IUser> | User<null>,这将成为一个可区分的联合。但类型User<IUser | null>不等同于那个。此外,它在未指定类型参数的类实现中不起作用T,因为编译器不对泛型类型进行控制流类型分析;请参阅microsoft/TypeScript#13995microsoft/TypeScript#24085。所以我们应该找到一种不同的方式来表示这一点。


到目前为止,最直接的方法是让User 一个可能的IUser,而不是一个可能IUser。这种“拥有”而不是“存在”的原则被称为组合优于继承。而不是检查其他一些名为 的属性unauthenticated,您只需要检查可能IUser类型的属性。这里是:

class User {
  constructor(public user: IUser | null) {}
}

const u = new User(Math.random() < 0.5 ? null : 
  { id: "alice", email: "alice@example.com", age: 30 });
if (u.user) {
  console.log(u.user.id.toUpperCase());
}

如果您真的想检查一个authenticated属性以区分 的存在和不存在IUser,您仍然可以通过使该user属性成为有区别的联合来做到这一点,如下所示:

// the discriminated type from above, written out programmatically
type PossibleUser = (IUser & { authenticated: true }) |
  ({ [K in keyof IUser]: null } & { authenticated: false });

// a default unauthenticated user
const nobody: { [K in keyof IUser]: null } = { age: null, email: null, id: null };

class User {
  user: PossibleUser;
  constructor(user: IUser | null) {
    this.user = user ? { authenticated: true, ...user } : { authenticated: false, ...nobody };
  }
}

const u = new User(Math.random() < 0.5 ? null :
  { id: "alice", email: "alice@example.com", age: 30 });
if (u.user.authenticated) {
  console.log(u.user.id.toUpperCase());
}

但是这里的额外复杂性可能不值得。


如果您真的需要该User课程成为可能IUser- ,那么您还有其他选择。使用类获取联合类型的正常方法是使用继承;有一个抽象BaseUser类,它捕获经过身份验证和未经身份验证的用户共有的行为,如下所示:

type WideUser = { [K in keyof IUser]: IUser[K] | null };
abstract class BaseUser implements WideUser {
  id: string | null = null;
  email: string | null = null;
  age: number | null = null;
  abstract readonly authenticated: boolean;
  commonMethod() {

  }
}

然后创建两个子类,AuthenticatedUser它们UnauthenticatedUser专门针对不同类型的行为:

class AuthenticatedUser extends BaseUser implements IUser {
  id: string;
  email: string;
  age: number;
  readonly authenticated = true;
  constructor(user: IUser) {
    super();
    ({
      id: this.id,
      email: this.email,
      age: this.age,
    } = user)
  }
}

type IUnauthenticatedUser = { [K in keyof IUser]: null };
class UnauthenticatedUser extends BaseUser implements IUnauthenticatedUser {
  id = null;
  email = null;
  age = null;
  readonly authenticated = false;
  constructor() {
    super();
  }
}

现在你有两个,而不是一个单一的类构造函数,所以你的User类型将是一个联合,并且要获得一个新的,你可以使用一个函数而不是类构造函数:

type User = AuthenticatedUser | UnauthenticatedUser;
function newUser(possibleUser: IUser | null): User {
  return (possibleUser ? new AuthenticatedUser(possibleUser) : new UnauthenticatedUser());
}

它具有预期的行为:

const u = newUser(Math.random() < 0.5 ? null : 
  { id: "alice", email: "alice@example.com", age: 30 });
if (u.authenticated) {
  console.log(u.id.toUpperCase());
}

最后,如果您绝对必须有一个与您的User联合相对应的类,您可以通过使一个类实现更广泛的非联合类型来做到这一点,然后断言该类的构造函数创建联合实例。它在类实现内部不是很安全,但在外部应该也能正常工作。这是实现:

class _User implements WideUser {
  id: string | null
  email: string | null
  age: number | null
  authenticated: boolean;
  constructor(user: IUser | null) {
    if (user === null) {
      this.authenticated = false
      this.id = this.email = this.age = null
    } else {
      this.authenticated = true;
      ({
        id: this.id,
        email: this.email,
        age: this.age,
      } = user)
    }
  }
}

请注意,我对其进行了命名,_User以便该名称User可用于以下类型和构造函数定义:

// discriminated union type, named PossibleUser before
type User = (IUser & { authenticated: true }) | 
  ({ [K in keyof IUser]: null } & { authenticated: false });
const User = _User as new (...args: ConstructorParameters<typeof _User>) => User;

现在,这可以按您的预期工作,使用new User()

const u = new User(Math.random() < 0.5 ? null : 
  { id: "alice", email: "alice@example.com", age: 30 });
if (u.authenticated) {
  console.log(u.id.toUpperCase());
}

好的,完成。走哪条路取决于你。就我个人而言,我会推荐最简单的解决方案,它需要进行一些重构,但却是最适合 TypeScript 的前进方式。

Playground 代码链接


推荐阅读