web-dev-qa-db-fra.com

Dactylographier, fusionner des types d'objets?

Est-il possible de fusionner les accessoires de deux types d'objets génériques? J'ai une fonction similaire à celle-ci:

function foo<A extends object, B extends object>(a: A, b: B) {
    return Object.assign({}, a, b);
}

Je voudrais que le type soit toutes les propriétés de A qui n'existent pas en B et toutes les propriétés de B.

merge({a: 42}, {b: "foo", a: "bar"});

donne un type assez étrange de {a: number} & {b: string, a: string}, a est cependant une chaîne. Le retour réel donne le type correct, mais je ne peux pas comprendre comment je l'écrirais explicitement.

10
Jomik

Le type d'intersection produit par la définition de bibliothèque standard TypeScript de Object.assign() est un approximation qui ne représente pas correctement ce qui se passe si un argument ultérieur a une propriété avec le même nom qu'un argument antérieur. Jusqu'à très récemment, cependant, c'était le mieux que vous puissiez faire dans le système de type de TypeScript.

À partir de l'introduction de types conditionnels dans TypeScript 2.8, cependant, il existe des approximations plus proches à votre disposition. Une telle amélioration consiste à utiliser la fonction de type Spread<L,R> Définie ici , comme ceci:

// Names of properties in T with types that include undefined
type OptionalPropertyNames<T> =
  { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T];

// Common properties from L and R with undefined in R[K] replaced by type in L[K]
type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = {[K in keyof T]: T[K]} // see note at bottom*

// Type of { ...L, ...R }
type Spread<L, R> = Id<
  // Properties in L that don't exist in R
  & Pick<L, Exclude<keyof L, keyof R>>
  // Properties in R with types that exclude undefined
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  // Properties in R, with types that include undefined, that don't exist in L
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  // Properties in R, with types that include undefined, that exist in L
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
  >;

(J'ai légèrement modifié les définitions liées; en utilisant Exclude de la bibliothèque standard au lieu de Diff, et en encapsulant le type Spread avec le no-op Id type pour rendre le type inspecté plus maniable qu'un tas d'intersections).

Essayons-le:

function merge<A extends object, B extends object>(a: A, b: B) {
  return Object.assign({}, a, b) as Spread<A, B>;
}

const merged = merge({ a: 42 }, { b: "foo", a: "bar" });
// {a: string; b: string;} as desired

Vous pouvez voir que a dans la sortie est désormais correctement reconnu comme string au lieu de string & number. Yay!


Mais notez qu'il s'agit toujours d'une approximation:

  • Object.assign() copie uniquement énumérable, propres propriétés , et le système de type ne vous donne aucun moyen de représenter l'énumération et la propriété d'une propriété sur laquelle filtrer. Cela signifie que merge({},new Date()) ressemblera au type Date à TypeScript, même si au moment de l'exécution aucune des méthodes Date ne sera copiée et la sortie est essentiellement {}. C'est une limite stricte pour l'instant.

  • De plus, la définition de Spread ne fait pas vraiment distinction entre les propriétés manquantes et une propriété qui est présent avec une valeur non définie . Ainsi, merge({ a: 42}, {a: undefined}) est tapé à tort comme {a: number} Alors qu'il devrait être {a: undefined}. Cela peut probablement être corrigé en redéfinissant Spread, mais je ne suis pas sûr à 100%. Et cela pourrait ne pas être nécessaire pour la plupart des utilisateurs. (Modifier: cela peut être corrigé en redéfinissant type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T])

  • Le système de type ne peut rien faire avec des propriétés qu'il ne connaît pas. declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows); aura un type de sortie de {a: number} au moment de la compilation, mais si whoKnows se trouve être {a: "bar"} (qui est assignable à {}), alors notGreat.a est une chaîne au moment de l'exécution mais un nombre au moment de la compilation. Oups.

Soyez donc prévenu; la saisie de Object.assign() comme intersection ou Spread<> est une sorte de "meilleur effort" et peut vous induire en erreur dans les cas Edge.


Quoi qu'il en soit, j'espère que cela vous aidera. Bonne chance!


* Remarque: Quelqu'un a modifié la définition de Id<T> À partir d'un type mappé d'identité pour être simplement T. Un tel changement n'est pas incorrect, exactement, mais il va à l'encontre du but ... qui est de parcourir les touches pour éliminer les intersections. Comparer:

type Id<T> = { [K in keyof T]: T[K] }

type Foo = { a: string } & { b: number };
type IdFoo = Id<Foo>; // {a: string, b: number }

Si vous inspectez IdFoo, vous verrez que l'intersection a été supprimée et les deux constituants ont été fusionnés en un seul type. Encore une fois, il n'y a pas de réelle différence entre Foo et IdFoo en termes d'affectation; c'est juste que ce dernier est plus facile à lire dans certaines circonstances. Certes, parfois, la représentation sous forme de chaîne du compilateur d'un type sera juste l'opaque-ish Id<Foo>, Donc ce n'est pas parfait. Mais cela avait un but. Si vous souhaitez remplacer Id<T> Par T dans votre propre code, soyez mon invité.

17
jcalz

Je pense que vous cherchez plus d'une union (|) au lieu d'une intersection (&) type. C'est plus proche de ce que tu veux ...

function merge<A, B>(a: A, b: B): A | B {
  return Object.assign({}, a, b)
}

merge({ a: "string" }, { a: 1 }).a // string | number
merge({ a: "string" }, { a: "1" }).a // string

apprendre TS J'ai passé beaucoup de temps à revenir sur cette page ... c'est une bonne lecture (si vous êtes dans ce genre de chose) et donne beaucoup d'informations utiles

3
Tyler Sebastian

Si vous souhaitez conserver l'ordre des propriétés, utilisez la solution suivante.

Voyez-le en action ici .

export type Spread<L extends object, R extends object> = Id<
  // Merge the properties of L and R into a partial (preserving order).
  Partial<{ [P in keyof (L & R)]: SpreadProp<L, R, P> }> &
    // Restore any required L-exclusive properties.
    Pick<L, Exclude<keyof L, keyof R>> &
    // Restore any required R properties.
    Pick<R, RequiredProps<R>>
>

/** Merge a property from `R` to `L` like the spread operator. */
type SpreadProp<
  L extends object,
  R extends object,
  P extends keyof (L & R)
> = P extends keyof R
  ? (undefined extends R[P] ? L[Extract<P, keyof L>] | R[P] : R[P])
  : L[Extract<P, keyof L>]

/** Property names that are always defined */
type RequiredProps<T extends object> = {
  [P in keyof T]-?: undefined extends T[P] ? never : P
}[keyof T]

/** Eliminate intersections */
type Id<T> = { [P in keyof T]: T[P] }
2
aleclarson