Yes it is possible.
NAЇVE EXAMPLE
const isA = <A extends string>(value: 'A' extends A ? A : never) => value === "A"
declare var x: 'A' | 'B'
isA(x) // ok
declare var y: "B" | "C"
isA(y) // expected error
Playground
'A' extends A ? A : never
----> means that if literal type A
extends passed argument (if passed argument is a subtype of 'A') then return passed argument, otherwise - return never
. Since never
is unrepresentable, TS gives you an error.
MORE ADVANCED EXAMPLE
First of all, you should get rid of in
operator, because it is not always narrows the type. Please see here, here and here.
It is better to use hasProperty
instead:
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
This is generic and type safe function to check whether property exists or not in the object.
Since, you want to check whether any part of union is assignable to desired type we need to know distinguish arguments with union type with single typed argument.
// credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// credits goes to https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
COnsider this validation utility:
type Validation<Obj> =
(IsUnion<Obj> extends true
? (NumberRecord extends Obj
? Obj : never)
: (Obj extends NumberRecord
? Obj : never)
)
If Obj
is a union, check whether NumberRecord
extends Obj
. If yes, it means that some part of the union is assignable to NumberRecord
and we can return Obj
(allow the argument). If Obj
is not a union of types, we are checking whether Obj
is a subtype of NumberArgument
. If yes, Obj
is allowed, otherwise - return never
. Hence, if argument has type undefined
- it is disallowed, because it is neither a union with subtype of NumberRecord
not subtype of NumberRecord
.
Let's see how it works:
const hasProperty = <Obj, Prop extends string>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, unknown> =>
Object.prototype.hasOwnProperty.call(obj, prop);
type NumberRecord = {
one: number;
two: number;
}
// credits goes to https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type/50375286#50375286
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// credits goes to https://stackoverflow.com/questions/53953814/typescript-check-if-a-type-is-a-union
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true
type Validation<Obj> =
(IsUnion<Obj> extends true
? (NumberRecord extends Obj
? Obj : never)
: (Obj extends NumberRecord
? Obj : never)
)
const isA = <
Obj,
>(value: Validation<Obj>) =>
typeof value == "object" &&
hasProperty(value, 'one') &&
hasProperty(value, 'two') &&
typeof value.one === "number" &&
typeof value.two == "number";
/**
* Ok
*/
isA({}) // should not work
isA({ one: 1, two: 2 }) // should work
isA(foo) // should work
isA({one:1,two:2,three:3}) // ok
/**
* Errors
*/
isA(undefined) // should not work
declare var foo: undefined | NumberRecord
isA(42) // expected error
isA({one:1, two:'string'}) // expected error
Playground
In other words, we just negated all invalid types.
You can see my article about type negation and type validation