TypeScript 中的别名问题

问题描述 投票:0回答:1

我目前正在对 CQL 进行包装,并在重命名属性键(即别名)方面遇到麻烦。这是包含所有所需代码和 3 个案例的 Playground:Playground

如果可能,仅更改部分代码的类型。 “1”是数据库连接客户端,因此代码中无论如何都会出现“asknown as ***”。 这是代码:

export type SelectFields<T> = {
  field: Select<T>;
  alias?: string;
};

export type SelectResult<
  T,
  S extends SelectFields<T>[],
> = S[number]['alias'] extends string
  ? { [K in S[number]['field'] as S[number]['alias']]: T[K] }
  : { [K in S[number]['field']]: T[K] };

@Injectable()
export class BaseRepository<T> {
  public readonly mapper: ModelMapper<T>;

  private readonly queryFactory: QueryFactory<T>;

  public async findByField<const S extends SelectFields<T>[]>(params?: {
    select?: SelectFields<T>[];
    where?: WhereFields<T>[];
    limit?: number;
  }) {
    const { select, where } = params || {};
    const query = this.queryFactory.createQuery(select, where);
    const result = await this.client.execute(query);
    return result.rows as unknown as SelectResult<T, S>[];
  }

  constructor(
    private readonly client: Client,
    private readonly entity: new (...args) => any,
  ) {
    const tableName = getTableName(entity);
    const primaryKeys = getPrimaryKeyFields(entity);
    if (!tableName) throw new Error('No entity name provided!');
    const newMapper = new Mapper(client, {
      models: { [tableName + 'Model']: { tables: [tableName] } },
    });
    this.mapper = newMapper.forModel(tableName);
    this.queryFactory = new QueryFactory<T>(tableName, primaryKeys);
  }
}

//Usage: 
@Injectable()
export class MyService {
  constructor(
    @Inject('SCYLLA_CLIENT') private readonly scylla: Client,
    @Inject('test_MAPPER') private readonly repository: BaseRepository<Test>,
  ) {}

  async fetchData() {
    const result = await this.repository.findByField({
      select: [
        { field: 'numero', alias: 'number_alias' },
        { field: 'test_id', alias: 'id_alias' },
      ],
    });
    return result;
  }
}

但被结果困住了。

javascript typescript
1个回答
0
投票

代码最重要的问题是编译器不希望您关心 field

literal types
alias
参数的
select
子属性。它往往会推断出
string
,这会阻止一切工作。您可以使用 a
const
类型参数
:

来更改它
declare const findByField:
  <const S extends SelectFields<Test>[]>(params?: {
    select?: S;
    where?: any[];
    limit?: number;
  }) => Promise<SelectResult<Test, S>>;

现在 TypeScript 将尝试将

S
推断为它所能实现的最窄类型,包括为
field
alias
传入的确切值,甚至数组元素的顺序(尽管我认为你不关心这一点) ).

在此之后,您只需要实现

SelectResult
,以便它实际上将键与值正确匹配。您已经在映射类型中使用了键重新映射,但仍在迭代键类型,事实证明,只要as之后的内容是键类型,您就可以迭代任意
联合类型
。 (所以在
{[A in B as C]: D}
中,你不需要
B
成为按键,只要
C
成为按键即可。)所以你可以这样做:

type SelectResult<
  T,
  S extends SelectFields<T>[],
> = { [U in S[number]
  as U['alias'] extends string ? U['alias'] : U['field']
  ]: T[U['field']] }

请注意,由于这现在只是一个映射类型,因此 IntelliSense 在显示结果时可能会保留

SelectResult
名称。为了避免这种情况,您可以对空对象类型执行无操作intersection,以提示类型显示进一步简化:

type SelectResult<
  T,
  S extends SelectFields<T>[],
> = { [U in S[number]
  as U['alias'] extends string ? U['alias'] : U['field']
  ]: T[U['field']] } & {};

好的,我们来测试一下:

const result1 = findByField({
  select: [
    { field: 'numero', alias: 'number_alias' },
    { field: 'test_id', alias: 'id_alias' }],
})
// const result1: Promise<{ number_alias: number; id_alias: string; }>

const result2 = findByField({
  select: [
    { field: 'numero' },
    { field: 'test_id', alias: 'id_alias' }
  ]
})
// const result2: Promise<{ numero: number; id_alias: string; }>

const result3 = findByField({
  select: [
    { field: 'numero' },
    { field: 'test_id' }],
})
// const result3: Promise<{ numero: number; test_id: string; }>

看起来不错!

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.