Typescript取决于参数的不同类属性类型

问题描述 投票:0回答:1

我有一个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'子句中产生一堆错误,例如else。我的问题与this one有关,建议答复为this.authenticated = true as any。实际上,这确实消除了一个错误,但是这个头痛的要点是能够写

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

不适用于此解决方案。

typescript class generics types
1个回答
0
投票

似乎您希望User类型像discriminated union一样是{authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...}。不幸的是,classinterface类型本身不能成为并集,因此没有简单的方法来表示它。

您对条件类型的尝试只能从类实现的外部进行,只有当您的user的类型为User<IUser> | User<null>时,该类型才成为有区别的并集。但是类型User<IUser | null>不等效于此。而且,它在类实现中不起作用,其中类型参数T未指定,因为the compiler doesn't do control-flow type analysis on generic types。因此,我们应该找到一种不同的表示方式。


到目前为止,最直接的方法是使User

have

成为可能的IUser,而不是使[[be
成为可能的IUser。这种“拥有”而不是“存在”的原理被称为composition over inheritance。而且,您不必检查其他名为unauthenticated的属性,而只需检查可能的IUser类型的属性。这是:class User { constructor(public user: IUser | null) {} } const u = new User(Math.random() < 0.5 ? null : { id: "alice", email: "[email protected]", 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: "[email protected]", age: 30 }); if (u.user.authenticated) { console.log(u.user.id.toUpperCase()); }

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

如果您确实需要User类以使[[be

可能成为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() { } }

然后创建两个子类,AuthenticatedUserUnauthenticatedUser,它们专门针对不同类型的行为: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: "[email protected]", age: 30 }); if (u.authenticated) { console.log(u.id.toUpperCase()); }

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

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: "[email protected]", age: 30 }); if (u.authenticated) { console.log(u.id.toUpperCase()); }

好,完成。由您决定哪种方式去这里。我个人将推荐最简单的解决方案,该解决方案需要一些重构,但这是对TypeScript最友好的方法。希望能有所帮助;祝你好运!

Playground Link to code
© www.soinside.com 2019 - 2024. All rights reserved.