Reputation: 457
Is there a way to create an interface or a narrow type from an object?
For example I would like something like:
const obj : someType = {
a: "a",
b: "b",
c: {
c: "c"
}
};
type myType = narrowTypeOf obj
Whereas myType
should then result into:
{
a: string,
b: string,
c: {
c: string
}
};
Sadly (or better fortunately) the build in type mytype = typeof obj
would result into someType
, since I did already declare it to be of that type.
If there is a way to use an object as a parameter to a type definition - that would solve my problem aswell.
Upvotes: 0
Views: 402
Reputation: 327679
Unless SomeType
is specifically a union in which one of the members is the narrow type you want, this isn't going to work. Let's first look at the happy case where SomeType
is such a union, even though apparently that's not your use case:
type SomeType = {
a: string,
b: string,
c: {
c: string
}
} | { foo: string } | { bar: string };
Here SomeType
is explicitly the union of the type you want and two unrelated types. Then when you assign a value to obj
annotated as SomeType
,
const obj: SomeType = {
a: "a",
b: "b",
c: {
c: "c"
}
};
the type typeof obj
captured at this point is the type you want:
type MyType = typeof obj;
/* type MyType = {
a: string;
b: string;
c: {
c: string;
};
} */
The type MyType
does not contain {foo: string}
or {bar: string}
. That's because the compiler uses control flow based type analysis to narrow union types upon assignment (see microsoft/TypeScript#8010 for the PR notes implementing this).
But there is no control flow based narrowing of non-union typed variables upon assignment. At one point the TS team seemed to be considering it, and there's an open suggestion (see microsoft/TypeScript#16976) for control flow narrowing of non-union types, but for now it's just not part of the language.
That means if your SomeType
is just wider than you'd like but not specifically a union containing the element, you're stuck. Once you annotate a variable to be that wide type, all assignments to that variable will forget anything more specific about the current value of that type.
The right thing to do here is not to annotate the variable's type. The only reason why you should annotate a variable as a wider type than that of the value you assign is if you want to mutate the variable's contents later to make it some other inhabitant of that wider type. For example, if SomeType
is given as this:
interface SomeType {
a: string | number;
b: unknown;
c: object;
}
Then the only good reason why you would annotate the following declaration as SomeType
:
const obj: SomeType = {
a: "a",
b: "b",
c: {
c: "c"
}
};
Would be if you planned to do this later:
obj.a = 123;
obj.b = () => 456;
obj.c = [789];
Assuming you are not planning to need that wider type, then you should let the compiler infer the type of obj
. If you are concerned that this will let you assign something incompatible with SomeType
, then you can use a helper function which acts sort of like a narrowing annotation:
const annotateAs = <T>() => <U extends T>(u: U) => u;
This helper function can be used like this:
const asSomeType = annotateAs<SomeType>();
Now asSomeType()
is a function which will return its input without widening, but will give an error if that input cannot be assigned to SomeType
:
const oops = asSomeType({
a: "a",
b: "b",
c: "c" // error! string is not an object
})
Now you can write the obj
assignment like this:
const obj = asSomeType({
a: "a",
b: "b",
c: {
c: "c"
}
});
That succeeded, so obj
is definitely a SomeType
, but now the type of obj
is:
type MyType = typeof obj;
/* type MyType = {
a: string;
b: string;
c: {
c: string;
};
} */
as you wanted.
Okay, hope that helps; good luck!
Upvotes: 3