I've been faced a problem how can pick only one item from key-type list in generic.
And I solve it with dirty way but I hope to write with more good ways.
example) I would like to make dto for users and one user type is selected but others is not.
// here 3 classes
class User { .. }
class IntegrationUser { .. }
class Admin {..}
// and they will be matched with keys
export type UserKeyEntityMap = {
user: User;
admin: Admin;
integration: IntegrationUser;
}
// so user type keys is
type UserKey = keyof UserKeyEntityMap;
export type UserResponseDto {
[ K in keyof UserKeyEntityMap]: UserKeyEntityMap[K];
}
expected working is below:
// error
const testEmpty = {} as UserResponseDto;
// Success
const testOnlyHasUser = { user: {} as User } as UserResponseDto;
// Success
const testOnlyHasAdmin = { admin: {} as Admin } as UserResponseDto;
// error
const testHaveUserAndAdmin = { user: {} as User; admin: {} as Admin } as UserResponseDto;
// error
const testHaveUserAndAdminAndIntegration = { user: {} as User; admin: {} as Admin; integration: IntegrationUser } as UserResponseDto;
Here is my dirty solution
// this cannot be passed test case: testHaveUserAndAdmin
export type UserResponseDto =
Pick<UserKeyEntityMap, 'user'> |
Pick<UserKeyEntityMap, 'admin'> |
Pick<UserKeyEntityMap, 'integration'>;
Whatever have a beautiful solution ?
Help me !
CodePudding user response:
Take a look at distributive-conditional-types, I think this is exactly what you are looking for. From my experience, this feature is very important when it comes to TS conditional types.
Consider this example:
class User { tag = 'User' }
class IntegrationUser { tag = 'IntegrationUser' }
class Admin { tag = 'Admin' }
// and they will be matched with keys
export type UserKeyEntityMap = {
user: User;
admin: Admin;
integration: IntegrationUser;
}
type Distribute<Dict, Keys extends keyof Dict = keyof Dict> = Keys extends any ? Record<Keys, Dict[Keys]> : never
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
type StrictUnion<T> = StrictUnionHelper<T, T>
// Record<"user", User> | Record<"admin", Admin> | Record<"integration", IntegrationUser>
type UserResponseDto = StrictUnion<Distribute<UserKeyEntityMap>>
// error
const testEmpty: UserResponseDto = {};
// Success
const testOnlyHasUser: UserResponseDto = { user: {} as User };
// Success
const testOnlyHasAdmin: UserResponseDto = { admin: {} as Admin };
// error
const testHaveUserAndAdmin: UserResponseDto = { user: {} as User, admin: {} as Admin };
// error
const testHaveUserAndAdminAndIntegration: UserResponseDto = { user: {} as User, admin: {} as Admin, integration: {} as IntegrationUser };
Usualy, if you want to distribute something - just use T extends any.
See here similar question/answer
CodePudding user response:
Oh I found advanced way that satisfy my question partially.
// this type passes testEmpty, testUser, testAdmin but not on testUserAndAdmin or testUserAndAdminAndIntegration
export type UserResponseDto<K> =
K extends 'user' ?
Pick<UserKeyEntityMap, 'user'> :
K extends 'admin' ?
Pick<UserKeyEntityMap, 'admin'> :
K extends 'integration' ?
Pick<UserKeyEntityMap, 'integration'> :
never;
if you have a more good solution, Please teach me.
