在函数参数和嵌套对象属性上强制使用相同的类型

时间:2018-03-19 08:59:55

标签: typescript

我有一个带有以下签名的函数,这是一个辅助函数,它将值附加到

上的列表属性
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"

传递,则会失败

这可能吗?非常感谢

1 个答案:

答案 0 :(得分:2)

您无法输入整个路径(path: string | string[]),但可以确保path属性为Tvaluepath属性相同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