Suppose we have a type like:
type Requirement = {
name: string;
}
And an object conforming to this Requirement type:
let some_requirement: Requirement = {
name: 'SomeRequirement'
}
Now suppose we want to use typeof some_requirement
to enforce another type to match its string literal name
type RequirementAssociate<R extends Requirement> = {
name: R['name']
}
let some_requirement_associate: RequirementAssociate<typeof some_requirement> = {
name: 'SomeRequirement' // Enforce this to have the same name as some_requirement
}
In the above example, TypeScript will enforce that some_requirement_associate.name
is a string
, but will not enforce that it is the string literal
'SomeRequirement'
Generally, how can we ensure an object is compatible with a type (some_requirement: Requirement
), while working with its "specific" type (not sure the term for this), like in the case of enforcing some_requirement_associate.name
is the string literal SomeRequirement
above
CodePudding user response:
What you want is not just a
type Requirement = {
name: string;
}
What you want is
type NamedRequirement<Name extends string> = {
name: Name;
}
let some_requirement_n: NamedRequirement<'SomeRequirement'> = {
name: 'SomeRequirement'
} as const
// or
let some_requirement_c /* const type */ = {
name: 'SomeRequirement'
} as const
otherwise name type will be lost in string
then what you did will work in both cases
CodePudding user response:
If you want the compiler to keep track of the specific literal types of your object properties, then instead of annotating as a wider type and throwing away any such information, you should tell the compiler a hint that you want narrower types. The easiest way to do this is with a const
assertion on the initializing value:
let some_requirement = {
name: 'SomeRequirement'
} as const;
which produces the behavior you want:
let some_requirement_associate: RequirementAssociate<typeof some_requirement> = {
name: 'SomeRequiremant' // error!
//~~ <-- Type '"SomeRequiremant"' is not assignable to type '"SomeRequirement"'
}
You may be concerned that there's no check that the assigned value satisfies the Requirement
constraint anymore:
let some_bad_requirement = {
name: 123
} as const; // no error here!
There are ways around this, the traditional one is to create a generic identity function as a helper. But starting in TypeScript 4.9 you can use the satisfies
operator to check the compatibility without losing the specific type:
let some_requirement = {
name: 'SomeRequirement'
} as const satisfies Requirement; // okay
let some_bad_requirement = {
name: 123
} as const satisfies Requirement; // error!
// ----------------> ~~~~~~~~~~~
// Type 'number' is not assignable to type 'string'
So the combination of as const
and satisfies
will allow you to infer very narrow types while still checking against the wider type you care about.