英文:
Typescript different class property types depending on arguments
问题
以下是代码的翻译部分:
// `IUser`是一个具有用户属性如`id`、`email`等的接口。
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)
}
}
}
请注意,上述代码是 TypeScript 代码,用于创建一个 User
类,根据传入的用户对象的状态来定义不同的属性类型。如果有其他需要翻译的部分,请提供具体内容。
英文:
I have a User
class, which I instantiate with an object, which may be null
if the user is not authenticated. I would like to make different property types depending on its state. This is what I tried:
// `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)
}
}
}
But this solution causes an error Type 'true' is not assignable to type 'T extends null ? false : true'
when setting the authenticated
field, and a bunch of errors like property 'email' doesn't exist on type 'IUser | null'
in the else
clause. My issue is related to this one, where the response suggested to do this.authenticated = true as any
. In fact, this does remove one error, but the whole point of this headache is to be able to write
if(user.authenticated) {
// email is certainly not null
const { email } = user
}
which doesn't work with this solution.
答案1
得分: 2
It seems you would like the User
type to be a discriminated union like {authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...}
. Unfortunately, class
and interface
types cannot themselves be unions, so there's no simple way to represent this.
Your attempt with conditional types would only possibly work from the outside of the class implementation, and then only if your user
is of type User<IUser> | User<null>
, which becomes a discriminated union. But the type User<IUser | null>
is not equivalent to that. And furthermore, it doesn't work inside the class implementation, where the type parameter T
is unspecified because the compiler doesn't do control-flow type analysis on generic types; see microsoft/TypeScript#13995 and microsoft/TypeScript#24085. So we should find a different way to represent this.
By far the most straightforward way is to make User
have a possible-IUser
, and not be a possible-IUser
. This principle of "having" instead of "being" is known as composition over inheritance. And instead of checking some other property named unauthenticated
, you'd only have to check the property of a possible-IUser
type. Here it is:
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());
}
If you really want to check an authenticated
property to distinguish between the presence and absence of an IUser
, you can still do it by making the user
property a discriminated union like this:
// 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());
}
But the extra complexity here might not be worth the benefit.
If you really need the User
class to be a possible-IUser
, then you have other options. The normal way of getting union types with classes would be to use inheritance; have an abstract BaseUser
class that captures behavior common to both authenticated and unauthenticated users, like this:
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() {
}
}
And then make two subclasses, AuthenticatedUser
and UnauthenticatedUser
, which specialize the behavior for the different types:
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();
}
}
Now instead of having a single class constructor you have two, so your User
type would be a union, and to get a new one you might use a function instead of a class constructor:
type User = AuthenticatedUser | UnauthenticatedUser;
function newUser(possibleUser: IUser | null): User {
return (possibleUser ? new AuthenticatedUser(possibleUser) : new UnauthenticatedUser());
}
And it has the expected behavior:
const u = newUser(Math.random() < 0.5 ? null :
{ id: "alice", email: "alice@example.com", age: 30 });
if (u.authenticated) {
console.log(u.id.toUpperCase());
}
Finally, if you absolutely must have a single class corresponding to your User
union, you can do it by making a class implement a wider non-union type and then assert that the constructor for that class makes union instances. It's not very type-safe inside the class implementation, but it should work as well outside. Here's the implementation:
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)
}
}
}
Note that I named it _User
so that the name User
is available for the following type and constructor definitions:
// 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;
And now this works as you might have expected, using 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());
}
Okay, done. It's up to you which way to go here. Personally, I'd recommend the simplest solution, which requires some refactoring but is the most TypeScript-friendly way forward.
英文:
It seems you would like the User
type to be a discriminated union like {authenticated: true, age: number, email: string, ...} | {authenticated: false, age: null, email: null, ...}
. Unfortunately, class
and interface
types cannot themselves be unions, so there's no simple way to represent this.
Your attempt with conditional types would only possibly work from the outside of the class implementation, and then only if your user
is of type User<IUser> | User<null>
, which becomes a discriminated union. But the type User<IUser | null>
is not equivalent to that. And furthermore it doesn't work inside the class implementation, where the type parameter T
is unspecified, because the compiler doesn't do control-flow type analysis on generic types'; see microsoft/TypeScript#13995 and microsoft/TypeScript#24085. So we should find a different way to represent this.
By far the most straightforward way is to make User
have a possible-IUser
, and not be a possible-IUser
. This principle of "having" instead of "being" is known as composition over inheritance. And instead of checking some other property named unauthenticated
, you'd only have to check the property of possible-IUser
type. Here it is:
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());
}
If you really want to check an authenticated
property to distinguish between the presence and absence of an IUser
, you can still do it by making the user
property a discriminated union like this:
// 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());
}
but the extra complexity here might not be worth the benefit.
If you really need the User
class to be a possible-IUser
, then you have other options. The normal way of getting union types with classes would be to use inheritance; have an abstract BaseUser
class which captures behavior common to both authenticated and unauthenticated users, like this:
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() {
}
}
And then make two subclasses, AuthenticatedUser
and UnauthenticatedUser
which specialize the behavior for the different types:
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();
}
}
Now instead of having a single class constructor you have two, so your User
type would be a union, and to get a new one you might use a function instead of a class constructor:
type User = AuthenticatedUser | UnauthenticatedUser;
function newUser(possibleUser: IUser | null): User {
return (possibleUser ? new AuthenticatedUser(possibleUser) : new UnauthenticatedUser());
}
And it has the expected behavior:
const u = newUser(Math.random() < 0.5 ? null :
{ id: "alice", email: "alice@example.com", age: 30 });
if (u.authenticated) {
console.log(u.id.toUpperCase());
}
Finally, if you absolutely must have a single class corresponding to your User
union, you can do it by making a class implement a wider non-union type, and then assert that the constructor for that class makes union instances. It's not very type safe inside the class implementation, but it should work as well outside. Here's the implementation:
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)
}
}
}
Note that I named it _User
so that the name User
is available for the following type and constructor definitions:
// 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;
And now this works as you might have expected, using 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());
}
Okay, done. It's up to you which way to go here. Personally I'd recommend the simplest solution, which requires some refactoring but is the most TypeScript-friendly way forward.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论