我有一个库,它允许用户定义架构,并根据他们的定义返回自定义响应类型。这是一个最小的例子:
interface Item {
id: string;
}
interface CardinalityDef {
[objectType: string]: 'one' | 'many'
}
class Schema<Cardinality extends CardinalityDef> {
constructor(public cardinality: Cardinality) {}
}
type ResponseOf<
S extends Schema<any>,
K extends keyof S['cardinality']
> = S['cardinality'][K] extends 'one'
? Item
: Item[];
class DB<S extends Schema<any>>{
query<ObjectType extends keyof S['cardinality']>(objectType: ObjectType): ResponseOf<S, ObjectType> {
return 1 as any;
}
}
function init<S extends Schema<any>>(schema: S): DB<S> {
return 1 as any;
}
现在我可以定义一个模式,并按照我的预期获取响应类型:
const schema = new Schema({posts: 'many', author: 'one'})
const db = init(schema);
// This is correctly typed at Item[]
const postsResponse = db.query('posts');
// This is correctly typed as Item
const authorResponse = db.query('author');
schema
可选我想让用户不有提供模式。
const db = init(); // no schema provided
如果未提供架构,所有查询响应应返回
Item[]
:
const postsResponse: Item[] = db.query('posts');
const authorResponse: Item[] = db.query('author');
我可以将
schema
参数设置为可选:
function init<S extends Schema<any>>(schema?: S): DB<S> {
return 1 as any;
}
然后更新
ResponseOf
类型,如下所示:
type IsAny<T> = 0 extends (1 & T) ? true : never;
type ResponseOf<
S extends Schema<any>,
K extends keyof S['cardinality']
> = IsAny<S['cardinality']> extends true
? Item[]
: S['cardinality'][K] extends 'one'
? Item
: Item[];
现在它可以工作了:
const dbWithoutSchema = init();
const postsResponseWithoutSchema = dbWithoutSchema.query('posts'); // types as Item[]
const authorResponseWithoutSchema = dbWithoutSchema.query('author'); // types as Item[]
但是,我不确定这是否惯用。主要:
db
被输入为Schema<any>
。 它确实根本没有没有模式。any
。这就像我在与打字稿战斗。我也可以明确说明未定义的情况,如下所示:
function init<S extends Schema<any> | undefined>(schema?: S): DB<S> {
return 1 as any;
}
但是,这感觉也不对劲。我看到的缺点:
| undefined
S
细化为严格undefined
哪种方法在打字稿中更惯用?您会建议采用不同的方法来允许可选模式吗?
我倾向于将类型默认为 Schema,并明确执行 IsAny 检查。我的主要理由是,这将 a) 消除线程通过 | 的需要。到处都是未定义的,b) 在没有提供模式时使类型细化。
欢迎所有想法!
如果您希望类型参数在无法推断的情况下回退到某些内容,您可以给它一个默认类型参数:
function init<
S extends Schema<any> = Schema<{ [k: string]: 'many' }>
>(schema?: S): DB<S> {
return 1 as any;
}
在这里,如果您调用
init()
并且 TypeScript 无法从中推断出 S
,因为您没有传入 schema
参数,TypeScript 将回退到默认值 Schema<{ [k: string]: 'many' }>
,其中 Schema
的类型参数有一个 string
索引签名,其类型为 "many"
。 这意味着如果您不带参数调用 init()
,TypeScript 会认为您调用了 init(schema)
,其中 schema
的 cardinality
属性将所有可能的属性视为 "many"
类型,从而映射每个可能的键至Item[]
:
const dbWithoutSchema = init();
const postsResponseWithoutSchema = dbWithoutSchema.query('posts');
// ^? const postsResponseWithoutSchema: Item[]
const authorResponseWithoutSchema = dbWithoutSchema.query('author');
// ^? const authorResponseWithoutSchema: Item[]
const randomResponseWithoutSchema = dbWithoutSchema.query('randomCrud');
// ^? const randomResponseWithoutSchema: Item[]
它允许您使用您想要的任何属性密钥调用
query()
。当您传入参数时,它的行为与以前一样,其中类型参数被推断为该参数的类型,并根据键返回 Item
或 Item[]
,并且它只允许您传入已知键:
const schema = new Schema({ posts: 'many', author: 'one' })
const db = init(schema);
const postsResponse = db.query('posts');
// ^? const postsResponse: Item[]
const authorResponse = db.query('author');
// ^? const authorResponse: Item
db.query('randomCrud') // error,
// ~~~~~~~~~~~~~
// '"randomCrud"' is not assignable to '"posts" | "author"'
使用默认的泛型类型参数是处理此类事情的惯用方法。你的其他方法可以工作或者可以工作,尽管你真的不想尝试检测
any
,虽然 undefined
可能是添加到类型参数域中的合理选择,但 TypeScript 实际上不会如果省略参数,则为 infer undefined
,因为 init(undefined)
和 init()
的等价性不会反映在类型推断中。所以你仍然需要将其设置为默认值,产生类似的结果
function init<
S extends Schema<any> | undefined = undefined
>(schema?: S): S extends undefined ? DB<Schema<{ [k: string]: 'many' }>> : DB<S> {
return 1 as any;
}
但这更复杂,我只建议如果您希望人们明确地调用
init(undefined)
(实际上几乎没有人这样做)。