source

스크립트 유형, 개체 유형 병합?

ittop 2023. 6. 26. 23:08
반응형

스크립트 유형, 개체 유형 병합?

두 개의 일반 객체 유형의 소품을 병합할 수 있습니까?다음과 유사한 기능이 있습니다.

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

저는 A에 있는 모든 속성 중 B에 없는 속성을 B에 있는 모든 속성을 타입으로 하고 싶습니다.

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

의 다소 이상한 유형을 제공합니다.{a: number} & {b: string, a: string},a그래도 문자열입니다.실제 반품은 정확한 유형을 제공하지만, 어떻게 명시적으로 작성해야 할지 모르겠습니다.

TS4.1+용 업데이트

원래 답변은 여전히 작동하지만(설명이 필요하면 읽어보셔야 합니다), 이제 재귀적 조건부 유형이 지원되므로, 우리는 쓸 수 있습니다.merge()변수가 될 경우:

type OptionalPropertyNames<T> =
  { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T];

type SpreadProperties<L, R, K extends keyof L & keyof R> =
  { [P in K]: L[P] | Exclude<R[P], undefined> };

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never

type SpreadTwo<L, R> = Id<
  & Pick<L, Exclude<keyof L, keyof R>>
  & Pick<R, Exclude<keyof R, OptionalPropertyNames<R>>>
  & Pick<R, Exclude<OptionalPropertyNames<R>, keyof L>>
  & SpreadProperties<L, R, OptionalPropertyNames<R> & keyof L>
>;

type Spread<A extends readonly [...any]> = A extends [infer L, ...infer R] ?
  SpreadTwo<L, Spread<R>> : unknown

type Foo = Spread<[{ a: string }, { a?: number }]>

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

테스트할 수 있습니다.

const merged = merge(
  { a: 42 },
  { b: "foo", a: "bar" },
  { c: true, b: 123 }
);
/* const merged: {
    a: string;
    b: number;
    c: boolean;
} */

코드에 대한 놀이터 링크

원답


TypeScript 표준 라이브러리 정의에 의해 생성된 교차 유형은 나중의 인수에 이전 인수와 동일한 이름의 속성이 있는 경우 발생하는 작업을 제대로 나타내지 않는 근사치입니다.하지만, 아주 최근까지, 이것은 당신이 타입스크립트의 타입 시스템에서 할 수 있는 최선이었습니다.

그러나 TypeScript 2.8에서 조건부 유형을 소개하는 것부터 시작하여 보다 가까운 근사치를 사용할 수 있습니다.이러한 개선 사항 중 하나는 유형 함수를 사용하는 것입니다.Spread<L,R>다음과 같이 정의됩니다.

// 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> = T extends infer U ? { [K in keyof U]: U[K] } : never // 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>
  >;

)를 사용Exclude 대신 표준 Diff요.Spread no-op으로 합니다.Id: .) type)의 경우에는 다음과 같이 합니다.

사용해 보겠습니다.

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

은 그것을 볼 수 .a에서 출에서이인식됩다니게르올바제로 올바르게 됩니다.stringstring & number 야호!


그러나 이는 여전히 근사치입니다.

  • Object.assign()열거 가능한 복사본, 소유 속성만 있고 형식 시스템은 필터링할 속성의 열거 가능성과 소유권을 나타내는 방법을 제공하지 않습니다.는 의미입니다.merge({},new Date())은 유처럼보것일입다니형▁like▁type처럼 보일 입니다.Date 시 TypeScript가 , " "는 TypeScript로 변환됩니다.Date이고 출력은 기본적으로 메드는복출기본로으적입니다.{}이것은 현재로서는 어려운 한계입니다.

  • 으로, 으로적는의 의정의가.Spread누락된 속성과 정의되지 않은 값을 가진 속성을 구분하지 않습니다.그렇게merge({ a: 42}, {a: undefined})를 잘못 했습니다.{a: number}로 할 때{a: undefined}이 문제는 아마도 재정의함으로써 해결될 수 있습니다.Spread100% 확신할 수는 없습니다그리고 대부분의 사용자에게 필요하지 않을 수 있습니다. (편집: 이것은 정의를 재정의하여 수정할 수 있습니다.type OptionalPropertyNames<T> = { [K in keyof T]-?: ({} extends { [P in K]: T[K] } ? K : never) }[keyof T])

  • 유형 시스템은 모르는 속성으로는 아무것도 할 수 없습니다. declare const whoKnows: {}; const notGreat = merge({a: 42}, whoKnows);은 "" " " " " " 입니다.{a: number}할 때, 만약 일시파만라면, 그나약러.whoKnows공교롭게도{a: "bar"}((으)로 {}), 그러면notGreat.a런타임에는 문자열이지만 컴파일 시간에는 숫자입니다.

하세요; 러니주세요하것타; 하는핑의이그▁of것▁typing▁so를 타이핑합니다.Object.assign() 교로또교서로차로는차로Spread<>일종의 "최선의" 것이고, 가장자리 사건에서 당신을 오해하게 만들 수 있습니다.

코드에 대한 놀이터 링크


*참고:Id<T>ID 유형이므로 원칙적으로 해당 유형에 대해 어떤 작업도 수행해서는 안 됩니다.누군가가 이 답변을 삭제하고 다음으로 바꾸기 위해 이 답변을 편집했습니다.T교차로를를 통해 .교차로를 제거하기 위해 키를 통해 반복하는 것입니다.비교:

type Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never 

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

IdFoo교차점이 제거되고 두 성분이 단일 유형으로 병합된 것을 볼 수 있습니다.다시 말하지만, 사이에 실질적인 차이는 없습니다.Foo그리고.IdFoo할당 가능성 측면에서; 단지 후자가 어떤 상황에서는 읽기 쉽다는 것입니다.

업데이트:

re-gor의 의견 덕분에 나는 이것을 다시 검토했고 병합에 대해 더 명확하게 하기 위해 구문을 업데이트했습니다.

type Merge<A, B> = {
  [K in keyof A | keyof B]: 
    K extends keyof A & keyof B
    ? A[K] | B[K]
    : K extends keyof B
    ? B[K]
    : K extends keyof A
    ? A[K]
    : never;
};

keyof에서 의로교원까지.A그리고.B개별적으로 모든 키가 노출됩니다.

유형을 가 중된차 3유사용키다가있먼다확니합인에 있는지 합니다.A그리고.B그리고 그것이 있을 때, 유형은.A[K] | B[K].

다음으로, 키가 다음에서만 올 때B 유형은 그다면종류는렇종▁is는▁then류입니다.B[K].

다음으로, 키가 다음에서만 올 때A 유형은 그다면종류는렇종▁is는▁then류입니다.A[K].

마지막으로 키는 어느 쪽에도 존재하지 않습니다.A도 아니다B 유형은 고종류는그리종는류▁is▁and.never.

type X = Merge<{ foo: string; bar: string }, { bar: number }>;

>>> type X = { foo: string; bar: string | number; }

이전:

두 개체의 모든 속성을 병합하는 형식을 선언하는 구문을 찾았습니다.

type Merge<A, B> = { [K in keyof (A | B)]: K extends keyof B ? B[K] : A[K] };

이 유형을 사용하면 A와 B 두 개체를 지정할 수 있습니다.

이러한 개체에서 사용 가능한 키에서 파생된 키를 가진 매핑된 유형이 생성됩니다.키는 다음에서 가져옵니다.keyof (A | B).

그런 다음 소스에서 적절한 유형을 찾아 각 키를 해당 키 유형에 매핑합니다.가 키가제는경우되에서 오는 .B그런 다음 유형은 해당 키의 유형입니다.B이 작업은 다음과 같이 수행됩니다.K extends keyof B ?이 부분은 다음과 같은 질문을 합니다.K에의열쇠에서 온 B , ? 해당키가려면오져을유형의,면▁?려,K 조회를 합니다.B[K].

가 키가원 ▁from의 것이 B에서 온 것이 틀림없습니다A이렇게 해서 3항성이 완성됩니다.

K extends keyof B ? B[K] : A[K]

것은 으로 감싸여 있습니다.{ }키가 두 개체에서 파생되고 유형이 소스 유형에 매핑되는 매핑된 개체 유형으로 만듭니다.

속성 순서를 보존하려면 다음 솔루션을 사용하십시오.

여기에서 실제로 확인하십시오.

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] }

tl;dr

type Expand<T> = T extends object
  ? T extends infer O
    ? { [K in keyof O]: O[K] }
    : never
  : T;

type UnionToIntersection<U> = Expand<
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never
>;

const merge = <A extends object[]>(...a: [...A]) => {
  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};

또 다른 대답에 대한 동기 부여

jcalz 답변은 좋고 저를 위해 수년간 일했습니다.그러나 병합된 개체 수가 특정 숫자로 증가하면 유형 스크립트에서 다음 오류가 발생합니다.

Type instantiation is excessively deep and possibly infinite. [2589]

결과 개체 유형을 추론하지 못합니다. 문제는 다음 github 문제에서 과도하게 논의된 유형 스크립트 문제 때문에 발생합니다. https://github.com/microsoft/TypeScript/issues/34933

세부 사항

merge() 위의 A[number] typestype의 됩니다. UnionToIntersection메타함수는 유니언을 교차로로 변환합니다. Expand교차로를 평평하게 만들어 IntelliSense와 같은 도구로 보다 쉽게 읽을 수 있도록 합니다.

자한내다용참참오시십조하조에 대한 자세한 하십시오.UnionToIntersection그리고.Expand구현:
https://.com/a/50375286/5000057https ://stackoverflow.com/a/50375286/5000057
https://github.com/shian15810/://github.com/shian15810/type-expand

추가의

을 할 때merge()함수입니다. 병합된 개체의 키 중복이 오류일 수 있습니다.이러한 과 중복을 과 같은 함수를 할 수 .throw Error:

export const mergeAssertUniqueKeys = <A extends object[]>(...a: [...A]) => {
  const entries = a.reduce((prev, obj) => {
    return prev.concat(Object.entries(obj));
  }, [] as [string, unknown][]);

  const duplicates = new Set<string>();
  entries.forEach((pair, index) => {
    if (entries.findIndex((p) => p[0] === pair[0]) !== index) {
      duplicates.add(pair[0]);
    }
  });

  if (duplicates.size > 0) {
    throw Error(
      [
        'objects being merged contain following key duplicates:',
        `${[...duplicates].join(', ')}`,
      ].join(' '),
    );
  }

  return Object.assign({}, ...a) as UnionToIntersection<A[number]>;
};

생각에 당신은 더 많은 조합을 찾고 있는 것 같습니다.| (으)로 입력합니다.&에 더 원하는 것에 더 가깝습니다.

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

TS를 배우면서 페이지로 돌아오는 데 많은 시간을 보냈습니다.그것은 좋은 읽을거리이고 (만약 당신이 그런 종류의 것에 관심이 있다면) 많은 유용한 정보를 줍니다.

는 @Michael P의 이 대답이 좋습니다. 스콧

저도 찾고 있었기 때문에 조금 더 간단하게 했습니다.단계별로 공유하고 설명해 드리겠습니다.

  1. 을 합니다.A병합 유형의 기준으로 입력합니다.
  2. 키가오기의 .B에 것A(제외와 같은 유틸리티 유형이 도움이 됩니다.
  3. 마지막으로, 단계에서 유형을 교차시킵니다.#1그리고.#2와 함께&결합된 유형을 가져옵니다.
type Merge<A, B> = A & { [K in Exclude<keyof B, keyof A>]: B[K] };

이전 답변은 좋지만 실제로 정확하지는 않습니다.

이것이 조금 더 낫습니다.

type Merge<A extends {}, B extends {}> = { 
  [K in keyof (A & B)]: (
    K extends keyof B 
      ? B[K] 
      : (K extends keyof A ? A[K] : never) 
  )
};

운동장에서 차이를 확인할 수 있습니다.

아마 모든 답이 맞겠지만, 저는 더 짧은 것을 만들었습니다.Merge일반적으로 필요한 작업을 수행합니다.

type Merge<T, K> = Omit<T, keyof K> & K;

언급URL : https://stackoverflow.com/questions/49682569/typescript-merge-object-types

반응형