By combining two other answers, I got the result I'm looking for. But the latter answer has a big DON'T DO THIS warning in front of it, so take this with the same precautions:
type ExtractKeyUnionByType<Obj, Type> = {
[Key in keyof Obj]: Obj[Key] extends Type ? Key : never
}[keyof Obj]
This is close to what I was looking for, but returns a union type instead of a tuple type.
ExtractKeyUnionByType<MyType, string> // => 'bar' | 'baz'
And if you want to include optional types, add undefined
to the union
type MyType = {
foo?: number;
bar: string;
baz?: string;
obj: MyOtherType
}
ExtractKeyUnionByType<MyType, string | undefined> // => 'bar' | 'baz' | undefined
// 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']
And you can now strongly type a tuple, such that it will throw an error if you add any extra properties, or remove any properties.
const requiredFields: BarAndBazTuple = ['bar', 'baz'] // ✅
const missing: BarAndBazTuple = ['bar'] // ❌ error
const extra: BarAndBazTuple = ['bar', 'baz', 'other'] // ❌ error