typescript 生成的区分联合出现打字错误

zu0ti5jz  于 2023-03-13  发布在  TypeScript
关注(0)|答案(2)|浏览(110)

我试着写一段代码,为一个类型的每个成员生成一个有区别的联合体,以及一个函数,该函数将接受该类型的对象和一个联合体成员的示例。
这是我目前拥有的代码

interface RickAndMortyCharacter {
  species: string;
  origin: {
    name: string;
    url: string;
  };

}

type RickAndMortyCharacterChange = {
  [Prop in keyof RickAndMortyCharacter]: {
    updatedProps: Prop;
    newValue: RickAndMortyCharacter[Prop];
  };
}[keyof RickAndMortyCharacter];

function applyPropertyChange(
  state: RickAndMortyCharacter,
  payload: RickAndMortyCharacterChange
) {
  // Error on this line
  state[payload.updatedProps] = payload.newValue;

  return state;
}

也可在这里与 typescript 操场
typescript 给我以下错误
键入'string|{名称:字符串;url:字符串;}“不能分配给类型”字符串& { name:字符串; URL:string;}“。类型”string“不能分配给类型”string & { name:字符串; URL:string;}“。类型”string“不能分配给类型”{ name:字符串; URL:字符串;}'。(2322)
有趣的是,当我将鼠标悬停在RickAndMortyCharacterChange类型上时,我看到声明了以下类型:

type RickAndMortyCharacterChange = {
    updatedProps: "species";
    newValue: string;
} | {
    updatedProps: "origin";
    newValue: {
        name: string;
        url: string;
    };
}

这对我来说似乎是正确的,因为Typescript阻止我写这篇文章

const s: RickAndMortyCharacterChange = {
  updatedProps: "origin",
  newValue: "bonjour"
};

const s2: RickAndMortyCharacterChange = {
  updatedProps: "species",
  newValue: {
    name: "Hello",
    url: "Hoi"
  }
}

我最近在一个Angular 项目中使用了一个非常非常相似的方法,它工作得很完美,我不明白为什么 typescript 在这里抱怨

uplii1fm

uplii1fm1#

这实际上是TypeScript类型检查算法的一个局限性,microsoft/TypeScript#30581中描述了根本问题。当您有一个联合类型的表达式,并且编写了该表达式多次出现的代码时,编译器通过将该表达式的每个语句视为独立于原始语句来分析它。

declare const state: RickAndMortyCharacter;
declare const payload: {
    updatedProps: "species";
    newValue: string;
} | {
    updatedProps: "origin";
    newValue: {
        name: string;
        url: string;
    };
}

你会得到这个

const up = payload.updatedProps;
// const up: "species" | "origin"
const nv = payload.newValue;
// const nv: string | { name: string; url: string; }

注意,upnv都是联合类型,但是这丢失了你所关心的信息,如果你所知道的up"species" | "origin"类型,而你所知道的nvstring | { name: string; url: string}类型,那么你就不能再写这样的代码而不出错:

state[up] = nv; // error!

因为如果up"origin",而nvstring呢?你只知道这是不可能的,因为upnv都来自同一个payload。但是TypeScript并不分析payloadidentity,它只看 type。在TypeScript中没有“相关联合”这样的东西。
所以赋值失败了,尽管它是100%类型安全的,而且他们不能通过让编译器多次分析state[up] = nv来修复它,而不严重破坏编译器的性能或使编译器不可预测。
幸运的是,有一种方法可以重构代码,使编译器遵循逻辑。microsoft/TypeScript#47109对此进行了描述。该方法是用generics替换覆盖类似键类型的联合体,然后将通用的indexes执行为适当的mapped types。这些index-to-mapped类型可以发生在单个 * 分布式对象类型 * 中(如GitHub问题中所创造的)。
您的RickAndMortyCharacterChange本质上就是这样一种类型,但是您可以将其更改为泛型(并且仍然默认为完全联合):

type RickAndMortyCharacterChange<K extends keyof RickAndMortyCharacter
  = keyof RickAndMortyCharacter> = {
    [P in K]: {
      updatedProps: P;
      newValue: RickAndMortyCharacter[P];
    };
  }[K];

现在,applyPropertyChange()可以在K中通用,并限制为keyof RickAndMortyCharacter,你将传入一个RickAndMortyCharacterChange<K>类型的值,突然间一切都正常了:

function applyPropertyChange<K extends keyof RickAndMortyCharacter>(
  state: RickAndMortyCharacter,
  payload: RickAndMortyCharacterChange<K>
) {
  state[payload.updatedProps] = payload.newValue; // okay
  return state;
}

如果将其分解为upnv,您将看到原因:

function applyPropertyChange<K extends keyof RickAndMortyCharacter>(
  state: RickAndMortyCharacter,
  payload: RickAndMortyCharacterChange<K>
) {
  const up = payload.updatedProps;
  // const up: K
  const nv = payload.newValue;
  // RickAndMortyCharacter[K]
  state[up] = nv; // okay
  return state;
}

现在up的类型是Knv的类型是RickAndMortyCharacter[K],它们都是泛型,赋值可以看作是将右边的RickAndMortyCharacter[K]赋值给左边的RickAndMortyCharacter[K],因为这些类型是相同的泛型,所以一切都按预期进行。
Playground代码链接

t40tm48m

t40tm48m2#

为了补充jcalz awesome answer的不足,可以创建一个实用程序类型,这样就可以轻松地将此模式应用于任何对象类型:

type PropertyChange<T extends Object, K extends keyof T = keyof T> = {
  [P in K]: {
    updatedProps: P;
    newValue: T[P];
  };
}[K];

function applyPropertyChange<T extends Object, K extends keyof T = keyof T>(
  state: T,
  payload: PropertyChange<T, K>
) {
  state[payload.updatedProps] = payload.newValue;
  return state;
}

用途

const character: RickAndMortyCharacter = {
  species: 'initial species',
  origin: { name: '', url: '' },
};

const change: PropertyChange<RickAndMortyCharacter> = {
  updatedProps: 'species',
  newValue: 'new species',
}; // ok

const change2: PropertyChange<RickAndMortyCharacter> = {
  updatedProps: 'origin',
  newValue: '',
}; // error

applyPropertyChange(character, change); // ok

applyPropertyChange(
  character, 
  { updatedProps: 'origin', newValue: '' }
); // error

applyPropertyChange(
  character, 
  { updatedProps: 'species', newValue: '' }
); // ok

applyPropertyChange(
  { prop1: 'string' },
  { updatedProps: 'prop1', newValue: 'new string' }
); // ok

applyPropertyChange(
  { prop1: 1 },
  { updatedProps: 'prop1', newValue: 'new string' }
); // error

applyPropertyChange<{ prop1: number }>(
  { prop1: 'string' },
  { updatedProps: 'prop1', newValue: 'new string' }
); // error

相关问题