【问题标题】:Typescript generic index types打字稿通用索引类型
【发布时间】:2021-03-12 23:31:35
【问题描述】:

我正在尝试创建一个通用函数describeToCreate,它根据type 将输入的对象转换为不同的对象。提供的泛型类型T 确定data 的类型和返回对象的类型。提供type 是因为如果没有它,确定data 的类型并非易事。

我已经包含了我正在内联的 Typescript 错误。

功能:

export function dataToCreate<T extends RESOURCE_TYPE>(
  type: T,
  data: IDataResources[T]
): ICreateResources[T] {
  if (type === RESOURCE_TYPE.PHONE) {
    // Type Error
    // Type 'IPhoneCreate' is not assignable to type 'ICreateResources[T]'.
    //   Type 'IPhoneCreate' is not assignable to type 'IPhoneCreate & IClockCreate'.
    // Argument of type 'IDataResources[T]' is not assignable to parameter of type 'IPhoneData'.
    //   Type 'IClockData | IPhoneData' is not assignable to type 'IPhoneData'.
    return phoneDataToCreate(data);
  } else if (type === RESOURCE_TYPE.CLOCK) {
    // Type Error
    // Type 'IClockCreate' is not assignable to type 'ICreateResources[T]'.
    //   Type 'IClockCreate' is not assignable to type 'IPhoneCreate & IClockCreate'.
    // Argument of type 'IDataResources[T]' is not assignable to parameter of type 'IClockData'.
    //   Type 'IClockData | IPhoneData' is not assignable to type 'IClockData'.
    return clockDataToCreate(data);
  }
}
 
export const phoneDataToCreate = (data: IPhoneData): IPhoneCreate => {
  return {
    name: data.name,
    clockName: data.clockName,
    spec: somePhoneTransformationFunction(data),
  };
};

export const clockDataToCreate = (data: IClockData): IClockCreate => {
  return {
    name: data.name,
    unit: data.unit,
    spec: someClockTransformationFunction(data),
  };
};

调用如下:

const create = dataToCreate<RESOURCE_TYPE.CLOCK>(RESOURCE_TYPE.CLOCK, {
  name: 'name',
  unit: 1000,
  spec: { },
})

模型:

enum RESOURCE_TYPE {
  PHONE = 'phone',
  CLOCK = 'clock',
}

interface IClockData {
  name: string;
  unit: number;
  spec: PhoneDataSpec; // object
}

interface IPhoneData {
  name: string;
  clockName: string;
  spec: PhoneDataSpec; // object
}

interface IClockCreate {
  name: string;
  unit: number;
  spec: PhoneCreateSpec; // object
}

interface IPhoneCreate {
  name: string;
  clockName: string;
  spec: PhoneCreateSpec; // object
}
 
interface IDataResources {
  [RESOURCE_TYPE.PHONE]: IPhoneData;
  [RESOURCE_TYPE.CLOCK]: IClockData;
}
 
interface ICreateResources {
  [RESOURCE_TYPE.PHONE]: IPhoneCreate;
  [RESOURCE_TYPE.CLOCK]: IClockCreate;
}

当调用dataToCreate 时,它正确地推断出对象是IClockData 而create 是IClockCreate。

【问题讨论】:

    标签: typescript typescript-generics


    【解决方案1】:

    TypeScript 中的一个已知问题是,constrained 到联合类型(例如您的 RESOURCE_TYPE)的泛型类型参数(例如您的 T)不会被 control flow analysis 缩小范围,而非通用联合类型可以。请参阅microsoft/TypeScript#13995 和microsoft/TypeScript#24085 了解更多信息。

    所以您可以检查具体联合类型RESOURCE_TYPE 的值以缩小其类型:

    declare function acceptClock(x: RESOURCE_TYPE.CLOCK): void;
    declare function acceptPhone(x: RESOURCE_TYPE.PHONE): void;
    declare const rt: RESOURCE_TYPE; // union type
    if (rt === RESOURCE_TYPE.CLOCK) {
      acceptClock(rt); // okay
    } else {
      acceptPhone(rt); // okay
    }
    

    (// okay 行只是“正常”,因为控制流变窄),但是当值是扩展该联合类型的泛型类型时,您不能执行相同操作:

    function nope<T extends RESOURCE_TYPE>(rt: T) {
      if (rt === RESOURCE_TYPE.CLOCK) {
        acceptClock(rt); // error!
      } else {
        acceptPhone(rt); // error!
      }
    }
    

    这是一个问题。


    此外,当您有两个独立的联合类型变量(例如 type 和 data,其类型相关)时,编译器无法轻松跟踪这种相关性。即使type 和data 不是通用的,你也不能轻易地检查type 并让编译器看到data 的任何含义。请参阅microsoft/TypeScript#30581 了解更多信息。

    最接近这种行为的是使用discriminated union 类型。在这种情况下,我们可以这样:

    type TypeData = { [K in keyof IDataResources]: [K, IDataResources[K]] }[keyof IDataResources];
    // type TypeData = [RESOURCE_TYPE.PHONE, IPhoneData] | [RESOURCE_TYPE.CLOCK, IClockData]
    

    您可以看到TypeData 是两个tuple types 的并集:其中第一个元素是RESOURCE_TYPE.PHONE,第二个元素是IPhoneData;第一个元素是RESOURCE_TYPE.CLOCK,第二个元素是IClockData。如果你有一个td 类型为TypeData 的值,你可以检查td[0] 并缩小td[1] 的控制流。这和你做的一样,但是type 和data 都被打包成一个可区分的联合值:

    function dataToCreateConcrete(...typeData: TypeData) {
      if (typeData[0] === RESOURCE_TYPE.PHONE) {
        return phoneDataToCreate(typeData[1]);
      } else if (typeData[0] == RESOURCE_TYPE.CLOCK) {
        return clockDataToCreate(typeData[1]);
      };
      // exhaustiveness check
      ((x: never) => { throw new Error("oops, shouldn't get here") })(typeData[0]);
    }
    

    (在here 中提到了详尽性检查,并防止编译器认为存在不返回值的代码路径)

    所以这是另一个问题。


    对于 microsoft/TypeScript#13995 或 microsoft/TypeScript#30581,没有很好的答案。最简单的做法是对实施中的每个麻烦点使用type assertion,只要您确信您所做的事情是安全的:

    export function dataToCreateAsserted<T extends RESOURCE_TYPE>(
      type: T,
      data: IDataResources[T]
    ): ICreateResources[T] {
      if (type === RESOURCE_TYPE.PHONE) {
        return phoneDataToCreate(data as IPhoneData) as ICreateResources[T];
      } else if (type === RESOURCE_TYPE.CLOCK) {
        return clockDataToCreate(data as IClockData) as ICreateResources[T];
      }
      // exhaustiveness check
      ((x: never) => { throw new Error("oops, shouldn't get here") })(type as never);
    }
    

    这可行,但很多 as ...。您可以通过使用单一呼叫签名overload 来放松严格性:

    export function dataToCreateOverload<T extends RESOURCE_TYPE>(
      type: T,
      data: IDataResources[T]
    ): ICreateResources[T];
    export function dataToCreateOverload(...typeData: TypeData) {
      if (typeData[0] === RESOURCE_TYPE.PHONE) {
        return phoneDataToCreate(typeData[1]);
      } else if (typeData[0] == RESOURCE_TYPE.CLOCK) {
        return clockDataToCreate(typeData[1]);
      };
      // exhaustiveness check
      ((x: never) => { throw new Error("oops, shouldn't get here") })(typeData[0]);
    }
    

    我们所做的是在实现中使用具体的可区分联合代码,同时仍然公开通用调用签名。


    Playground link to code

    【讨论】:

    • 非常感谢您!我的临时解决方案是使用as 断言,但您创建元组和抛出的解决方案更为理想。为什么我无法找到基于彼此采用两个参数的泛型示例也更有意义。再次感谢!
    猜你喜欢
    • 1970-01-01
    • 2018-12-15
    • 2020-04-09
    • 2021-08-31
    • 2020-07-17
    • 1970-01-01
    • 2023-01-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多