Is it possible to enforce a specific type at initialization of a variable, but then extract the literal value of it to a literal type, at a later point? See Below:
type Person = { name: string; }
//this needs to be typed. to enforce structure
const john: Person = {
name: 'John'
}; //'as const' doesn't seem to work here because it's already typed
//I'm wanting the literal value of john { name: 'John' } instead of { name: string }
type John = typeof john; //as const here throws an error
CodePudding user response:
No; when you annotate a variable with a non-union type like Person
, the compiler does not narrow to something more specific upon assignment. So in your code, the type of john
is Person
, and any narrower type it might have inferred from the initializing value has been thrown away forever. It is too late to retrieve it.
So if you want to capture the narrow type of the initial value, you will need to do so before assigning it to the annotated variable. For example:
type Person = { name: string; }
const johnInitializer = {
name: 'John'
} as const;
type John = typeof johnInitializer;
/* type John = {
readonly name: "John";
} */
const john: Person = johnInitializer;
The assignment at the end enforces that the structure of johnInitializer
is compatible with Person
. After this you can use john
anywhere you would want to use a Person
.
Unless you have a reason why you want john
to be as wide as Person
(e.g., you want to reassign the name
property or something), you can skip the assignment and just use the initial value:
const john = { name: "John" } as const;
type John = typeof john;
/* type John = {
readonly name: "John";
} */
Presumably you will be using john
somewhere that expects a Person
.
// later...
acceptPerson(john); // okay
The fact that this is accepted means that john
conforms to the Person
structure. If you made a mistake, then you'd get an error at the usage site:
const jhon = { naem: "Jhon" } as const; // oops
// later ...
acceptPerson(jhon); // error!
You can think of that assignment as just a fail-fast check so that you catch the error closer to the declaration. There are other ways to do this; for example, a generic identity helper function:
const checkType = <T,>() => <U extends T>(u: U) => u;
const personCheck = checkType<Person>();
The function personCheck
will accept a value of a type assignable to Person
and return it, without widening the value to Person
:
const john = personCheck({ name: "John" } as const); // okay
type John = typeof john;
/* type John = {
readonly name: "John";
} */
const jhon = personCheck({ naem: "Jhon" } as const); // error!
Finally, you mentioned that using a direct assignment would perform excess property checking whereas these other indirect methods would not. And that's true, you lose these excess property checks:
const johnnyAppleseed = personCheck(
{ name: "Johnny", appleSeeds: true }); // okay
Personally I try to avoid code that cares about excess properties, since they are a fact of structural typing. Still, if you care, you can change the helper function to be stricter:
const checkExactType = <T,>() => <U extends T>(
u: { [K in keyof U]: K extends keyof T ? U[K] : never }
) => u;
const personCheck = checkExactType<Person>();
Now personCheck
will only accept values of types U
which are both assignable to T
and which have no extra properties (technically it requires that any such extra properties be of type never
):
const john = personCheck({ name: "John" } as const); // okay
const jhon = personCheck({ naem: "Jhon" } as const); // error!
const johnnyAppleseed = personCheck(
{ name: "Johnny", appleSeeds: true }); // error!
So now you have narrowed types with excess property checking as well.