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