网站搜索

typescript - 类型 'Session' 不能赋值给类型 'Record'


我正在尝试实现 https://next-auth.js.org/configuration/callbacks#session-callback 的回调。session 回调的定义如下:

export interface CallbacksOptions {
    session?:
        | ((session: Session) => WithAdditionalParams<Session>)
        | ((session: Session, userOrToken: User | JWT) => Promise<WithAdditionalParams<Session>>);
}

session 回调的实现:

   async session(session: Session, token: JWT) {

        if (token?.accessToken) {
            session.user.accessToken = token.accessToken;
            session.accessToken = token.accessToken as string;
        }

        if (token?.provider) {
            session.user.provider = token.provider;
        }

        if (token?.accountId) {
            session.user.accountId = token.accountId;
        }

        return Promise.resolve<WithAdditionalParams<Session>>(session);
    }

编译器报错:

TS2345: Argument of type 'Session' is not assignable to parameter of type 'WithAdditionalParams<Session> | PromiseLike<WithAdditionalParams<Session>>'.
  Type 'Session' is not assignable to type 'WithAdditionalParams<Session>'.
    Type 'Session' is not assignable to type 'Record<string, unknown>'.
      Index signature is missing in type 'Session'.

“session” 定义如下:

export interface Session {
    user: WithAdditionalParams<User>;
    accessToken?: string;
    expires: string;
}

export type WithAdditionalParams<T extends Record<string, any>> = T & Record<string, unknown>;

export interface User {
    name?: string | null;
    email?: string | null;
    image?: string | null;
}

我做错了什么?next-auth 的仓库托管在 https://github.com/nextauthjs/next-auth,类型定义在 https://github.com/DefinitelyTyped/DefinitelyTyped/blob/7c785c26527720bf726f8b8bcbab2f96c600d1a4/types/next-auth/index.d.ts

答案#1

当你写作时

Promise.resolve<WithAdditionalParams<Session>(session); // error!

你正在尝试在期望类型为 WithAdditionalParams<Session> 的位置使用类型为 Session 的值。因为编译器不认为 Session 可以赋值给 WithAdditionalParams<Session>,所以会出现错误。如果你尝试写

Promise.resolve<number>("someString"); // error! string is not a number

你会遇到类似的错误。大概你不会想着在需要number的地方使用string。所以问题是:为什么Session不能赋值给WithAdditionalParams<Session>?


好,类型 WithAdditionalParams<Session> 是 Session 的一个 子类型,它包含一个 string 索引签名,其属性类型为 unknown。(这就是 Record<string, unknown> 的含义。) 由于 Session 没有索引签名,编译器不会认为 WithAdditionalParams<Session> 可以赋值给 Session。

但是你可能会想到,session 的任何随机额外属性都可以赋值给 unknown 类型……所有东西都可以赋值给 unknown。那么,为什么编译器不把 Session 当作拥有字符串索引签名来处理呢?事实上,这种情况有时确实会发生,通过 隐式索引签名。请看:

type SessionType = {
  user: WithAdditionalParams<User>;
  accessToken?: string;
  expires: string;
}
declare const session: SessionType;
Promise.resolve<WithAdditionalParams<Session>>(session); // no error

这里的值 session 是 SessionType 类型,这是一个对象类型的类型别名,其结构与 Session 相同。而编译器在 WithAdditionalParams<Session> 期望的地方使用 SessionType 时完全没有问题……它给 SessionType 一个隐式索引签名,一切都成功了。

(所以,你处理这个问题的一种可能方法是使用 SessionType 而不是 Session。)


那么现在的问题是,为什么作为interface的Session不会获得隐式索引签名,而结构完全相同的type别名SessionType却会?你可能会再次认为,编译器不会只是简单地拒绝给interface类型添加隐式索引签名吧?令人惊讶的是,这正是发生的情况。

参见 microsoft/TypeScript#15300,特别是这条评论:

只是为了让大家了解,目前这种行为是设计使然。因为接口可以通过额外的声明进行扩展,而类型别名不能,因此为类型别名推断一个隐式索引签名比为接口推断更“安全”(这里的安全打上重引号)。但如果这样做对接口也有意义,我们会考虑对接口也这样做。

就这样。你不能用 Session 替代 WithAdditionalParams<Session>,因为将来有人可能会合并与索引签名冲突的属性。至于这是否是一个令人信服的理由,则存在相当激烈的辩论,如果你阅读 microsoft/TypeScript#15300 就会看到。


那么,我们该如何继续呢?如果你不想将 Session 从 interface 改为 type 别名,你总是可以使用一种工具告诉编译器“我不在乎你认为值 x 不是 Y 类型,我告诉你它是!”也就是说,使用类型断言:

Promise.resolve(session as WithAdditionalParams<Session>); // no error

这是可行的,因为编译器将 session 的类型视为与 WithAdditionalParams<Session>“相关”,所以当你断言它实际上 是 那种类型时,编译器就会相信你的说法并继续执行。注意,一旦你给 session 指定了 WithAdditionalParams<Session> 类型,你就不必在调用 Promise.resolve() 时手动指定类型参数;编译器会自动推断出来。

请记住,当你使用类型断言时,你需要自己承担验证其准确性的责任……因为编译器无法为你验证,它也无法确认你是否正确地进行了断言。如果你的断言结果不正确,那么你就是对编译器撒了谎,运行时因此发生的任何不愉快情况都是你的责任,而不是编译器的责任。所以请谨慎使用断言,仅在你相对确定这样做是安全的时候使用。

代码的 Playground 链接