Vyhledávání na webu

typescript - Jak vyvolat chybu typu, když jsou do objektu přidány další klíče konkrétního typu


Vzhledem k tomu, že mám typ MyType

type MyType = {
  foo: number;
  bar: string;
  baz: string;
}

Chtěl bych definovat pole requiredFields, které obsahuje všechny klíče MyType, jejichž hodnota je určeného typu T. Takže pokud bude později přidán další klíč typu T do MyType, requiredFields by měl vyvolat chybu typu, protože nový klíč nyní chybí.

To by mohlo vypadat například takto:

const numberFields: ExtractKeysByType<MyType, number> = ['foo'] // ✅
const stringFields: ExtractKeysByType<MyType, string> = ['bar'] // ❌ error - missing "baz"

Odpověď č. 1

Kombinací dvou dalších odpovědí jsem dostal výsledek, který hledám. Ale u té druhé odpovědi je velké NEDĚLEJ TO varování, takže to berte se stejnými opatrnostmi:

Krok 1: Jak získat klíče určitého typu z objektu

type ExtractKeyUnionByType<Obj, Type> = {
  [Key in keyof Obj]: Obj[Key] extends Type ? Key : never
}[keyof Obj]

Tohle je blízko tomu, co jsem hledal, ale vrací to unijní typ místo n-tice.

ExtractKeyUnionByType<MyType, string> // => 'bar' | 'baz'

A pokud chcete zahrnout volitelné typy, přidejte undefined do unie

type MyType = {
  foo?: number;
  bar: string;
  baz?: string;
  obj: MyOtherType
}

ExtractKeyUnionByType<MyType, string | undefined> // => 'bar' | 'baz' | undefined

Krok 2: Jak převést typ unie na typ n-tice (připomenutí, autor opravdu nechce, abyste tento kód použili)

// oh boy don't do this
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
  ? I
  : never
type LastOf<T> = UnionToIntersection<T extends any ? () => T : never> extends () => infer R
  ? R
  : never

// TS4.0+
type Push<T extends any[], V> = [...T, V]

// TS4.1+
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> = true extends N
  ? []
  : Push<TuplifyUnion<Exclude<T, L>>, L>
type BarAndBazTuple = TuplifyUnion<
  NonNullable<ExtractKeyUnionByType<MyType, string | undefined>>
> // => ['bar', 'baz']

A nyní můžete silně typovat tuple, takže se zobrazí chyba, pokud přidáte jakékoliv další vlastnosti nebo odstraníte nějaké vlastnosti.

const requiredFields: BarAndBazTuple = ['bar', 'baz'] // ✅

const missing: BarAndBazTuple = ['bar']               // ❌ error
const extra: BarAndBazTuple = ['bar', 'baz', 'other'] // ❌ error

Odpověď č. 2

Cíle vyvolání typu chyby, když klíč MyType typu T chybí v poli, lze dosáhnout snadněji nejprve definováním objektu intermediateObj s příslušnými klíči a poté použitím Object.keys() k přeměně tohoto objektu na pole requiredFields.

Použitím unijního typu a Record může být vyvolána chyba, pokud klíč chybí; na rozdíl od Array<UnionType>, který by chybu vyvolal pouze v případě, že byla přidána nesprávná vlastnost (nikoli pokud některá chybí).

Nejprve dva užitečné typy. První je převzat z tohoto skvělého jiného odpovědi. Druhý upravuje první tak, aby také zahrnoval volitelné vlastnosti.

type ExtractRequiredKeysByType<Obj, Type> = {
  [Key in keyof Obj]: Obj[Key] extends Type ? Key : never
}[keyof Obj]

type ExtractKeysByType<Obj, Type> = NonNullable<
  ExtractRequiredKeysByType<Obj, Type | undefined>
>

ExtractKeysByType lze poté použít k definování objektu, který musí obsahovat všechny string klíče z MyType

type StringKeys = ExtractKeysByType<MyType, string>

const intermediateObj: Record<StringKeys, true> = {
  bar: true,
  baz: true,
}

A nakonec může být tento mezilehlý objekt převeden na požadované pole pomocí Object.keys()

const requiredFields = Object.keys(offerTermDateFieldsObj) as StringKeys[]

Teď, pokud někdo přijde a přidá další hodnotu řetězce anotherString do MyType, dojde k chybě při definici intermediateObj

Vlastnost 'anotherString' chybí v typu { bar: true; baz: true }, ale je vyžadována v typu Record<StringKeys, true>.