繁体   English   中英

根据参数打字稿不同的类属性类型

[英]Typescript different class property types depending on arguments

我有一个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' Type 'true' is not assignable to type 'T extends null ? false : true'设置已authenticated字段时, property 'email' doesn't exist on type 'IUser | null'等一系列错误property 'email' doesn't exist on type 'IUser | null' property 'email' doesn't exist on type 'IUser | null'else子句中。 我的问题与相关,其中响应建议将this.authenticated = true as any 事实上,这确实消除了一个错误,但这个头疼的重点是能够写

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

这不适用于此解决方案。

您似乎希望User类型是一个可区分的联合,例如{authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...} {authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...} 不幸的是, classinterface类型本身不能是联合,因此没有简单的方法来表示这一点。

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


到目前为止,最简单的方法是让User一个possible- IUser ,而不是成为一个possible- 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() {

  }
}

然后创建两个子类, 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: "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 代码链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM