typescript - 打字稿不同的类属性类型取决于参数
问题描述
我有一个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.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, ...}
. 不幸的是,class
类型interface
本身不能是联合,所以没有简单的方法来表示它。
您对条件类型的尝试只能在类实现的外部起作用,并且只有当您user
的类型是 type时才可能起作用User<IUser> | User<null>
,这将成为一个可区分的联合。但类型User<IUser | null>
不等同于那个。此外,它在未指定类型参数的类实现中不起作用T
,因为编译器不对泛型类型进行控制流类型分析;请参阅microsoft/TypeScript#13995和microsoft/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 的前进方式。
推荐阅读
- java - gRPC client not working when called from within gRPC service
- javascript - 在 React Native 中获取选中的值 CheckBox
- svg - 在 Phaser3 中向 svg 元素添加事件
- android-studio - 在flutter中调试时如何定位特定的小部件
- angular - 代码流身份验证 Angular 8 无法获取访问令牌
- python - Python将具有相同id但不同值的术语附加到列表中?
- python - Django 'WSGIRequest' 对象没有属性 'completed_todo'
- security - Go 的任何内置功能(更有可能是 go 编译器)可以解决使您的二进制文件更防篡改的问题吗?
- java - 如何将具有对象作为键的 Map 列表转换为 Map?
- visual-studio-code - 有没有办法使用 VS Code 将自动化测试与 Azure DevOps 中的测试用例相关联?