Home > database >  Select only one key type pair from string key - object type pair list in typescript
Select only one key type pair from string key - object type pair list in typescript

Time:01-30

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 };

Playground

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.

  •  Tags:  
  • Related