我有一个带有以下签名的函数,这是一个辅助函数,它将值附加到
上的列表属性export function append<
T extends Entity,
S extends Collection<T>
>(state: S, entityId: number, path: string | string[], value): S {}
它基于以下两个简单的接口
export type EntityId = number;
/**
* Interface for entities
*/
export interface Entity extends Object {
readonly id: EntityId;
}
/**
* Interface for collection-based state
*/
export interface Collection<T extends Entity> extends Object {
readonly entities: { [key: string]: T };
readonly ids: EntityId[];
}
示例用法看起来像
interface Comment extends Entity {
text: string
likedByIds: number[]
}
interface CommentState extends Collection<Comment> {}
const comment: Comment = { id: 1, text: 'hello', likedByIds: [] }
const commentState: CommentState = {
entities: {
1: { id: 1, text: 'hello', likedByIds: [] }
},
ids: [1]
}
const commentWithLike = append(commentState, 1, 'likedByIds', 555)
commentWithLike // { id: 1, text: 'hello', likedByIds: [555] }
目标是上述,强制传入的likesById的类型符合接口,即。只允许数字,如果我试图将ID作为字符串"555"
这可能吗?非常感谢
答案 0 :(得分:2)
您无法输入整个路径(path: string | string[]
),但可以确保path
属性为T
且value
与path
属性相同export function append<
T extends Entity,
TKey extends keyof T,
>(state: Collection<T>, entityId: EntityId, path: TKey, value: T[TKey]): Collection<T> {
return state;
}
const commentWithLike = append(commentState, 1, 'likedByIds', [555]) // OK
const commentWithLike2 = append(commentState, 1, 'likedByIds', '555') // error
const commentWithLike3 = append(commentState, 1, 'likedByIds', 555) // error
财产:
S extends Collection<T>
请注意,我没有使用S
。这是因为typescript推断泛型参数的方式存在一些限制,因此在函数签名中使用T
会导致Entity
始终被推断为Comment
而不是{{} 1}}。
虽然对于某些用例来说这可能已经足够好了但是如果你想拥有一个特定类型的集合,你可以做以下两件事之一:
在打字稿2.8(在撰写本文时未发布,计划于2018年3月,但您可以使用npm install -g typescript@next
获取)中,您可以使用条件类型来提取实体类型:
export function append<
S extends Collection<any>,
T = S extends Collection<infer U> ? U : never,
TKey extends keyof T = keyof T,
>(state: S, entityId: EntityId, path: TKey, value: T[TKey]): S {
return state;
}
或者在ts 2.8之前,您可以在append
上声明Collection
方法,无需推断S
export interface Collection<T extends Entity> extends Object {
readonly entities: { [key: string]: T };
readonly ids: EntityId[];
append<TKey extends keyof T>(entityId: EntityId, path: TKey, value: T[TKey]): this
}
const commentWithLike = commentState.append(1, 'likedByIds', [555])
修改 - 支持路径
花了一些时间让它正常工作,但这是一个可行的解决方案:
type PathHelper<T> = { <TKey extends keyof T>(path: TKey): PathHelper<T[TKey]>; Value?: T; };
type Path<TSource, TResult> = { (source: TSource): TResult, fullPath: string[] };
function path<TSource, TResult>(v: (p: PathHelper<TSource>) => PathHelper<TResult>): Path<TSource, TResult> {
let result: string[] = [];
function helper(path: string) {
result.push(path);
return helper;
}
v(helper);
return Object.assign(function (s: TSource) { throw new Error("Do not call directly, use path property") }, {
fullPath: result
});
}
type CollectionType<S> = S extends Collection<infer U> ? U : never;
export function append<S extends Collection<any>,
TValue>(state: S,
entityId: EntityId,
path: Path<CollectionType<S>, TValue>,
value: TValue): S {
console.log(path.fullPath);
return state;
}
//Usage:
interface Comment extends Entity {
text: string;
comment?: Comment; // added for nested object example
likedByIds: number[]
}
interface CommentState extends Collection<Comment> { }
const comment: Comment = { id: 1, text: 'hello', likedByIds: [] }
const commentState: CommentState = {
entities: {
1: { id: 1, text: 'hello', likedByIds: [] }
},
ids: [1]
}
const commentWithLike2 = append(commentState, 1, path(v => v("comment")("likedByIds")("length")), 5)
备注强>
path
函数接受一个函数,您必须通过调用返回函数的函数返回路径。每次调用它时都会导航到对象。
path
函数返回一个Path
对象,该对象具有调用签名和fullPath
属性。应使用fullPath
属性,应忽略函数签名。当函数返回一个函数时,Typescript做了一种有趣的反向推理形式,Path
具有的这个函数签名使我们不必指定path
的类型参数,并允许编译器推断出根据{{1}}
path
的起始类型
你可以保留一个简单的签名来追加简单路径append
。