Home > other >  Getting the specific type of a variable while ensuring it is compatible with its type
Getting the specific type of a variable while ensuring it is compatible with its type

Time:10-03

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.

Playground link to code

  • Related