Let's explore this scenario:
const word = 'hello';
const phrase = 'foo';
const saying = 'example';
interface instance {
hi: 'hello';
bar: 'foo';
};
type FindMatch<Obj, Str extends string> = {
[Property in keyof Obj]: Str extends Obj[Property] ? Str : never
}[keyof Obj]
type Outcome1 = FindMatch<instance, 'hello'> // hello
type Outcome2 = FindMatch<instance, 'foo'> // foo
type Outcome3 = FindMatch<instance, 'example'> // never
FindMatch
:
Property
- denotes each key
Obj
- signifies an object, specifically the instance
interface.
This custom type goes through each key of the Obj
(instance
) and determines if Obj[Property]
matches with the second parameter Str
(string
or phrase
). If it does match, then use Str
as the value for that property, otherwise use never
.
The line [keyof Obj]
at the end of the type fetches a union of all values in the object. If any value matches Str
, we will get Str | never
. Since never
is a bottom type and can be assigned to any type, the union of Str | never
simplifies to just Str
.
If you only want to obtain a boolean output from FindMatch
, where true
indicates a match exists and false
implies no match. You can add a conditional type that checks whether the result extends never
or not:
const word = 'hello';
const phrase = 'foo';
const saying = 'example';
interface instance {
hi: 'hello';
bar: 'foo';
};
type IsNever<T> = [T] extends [never] ? true : false
type FindMatch<Obj, Str extends string> = IsNever<{
[Property in keyof Obj]: Str extends Obj[Property] ? Str : never
}[keyof Obj]> extends false ? true : false
type Result1 = FindMatch<instance, 'hello'> // true
type Result2 = FindMatch<instance, 'foo'> // true
type Result3 = FindMatch<instance, 'example'> // false
Playground
If you wish to trigger an error, follow this example:
const word = 'hello';
const phrase = 'foo';
const saying = 'example';
type instance = {
hi: 'hello';
bar: 'foo';
};
type Values<T> = T[keyof T]
type Validate<Obj extends Record<string, string>, Key extends Values<Obj>> = Obj
type Output1 = Validate<instance, 'hello'> // true
type Output2 = Validate<instance, 'foo'> // true
type Output3 = Validate<instance, 'example'> // false
https://www.typescriptlang.org/play?#code/FDDGHsDsGcBcAI4CcCWkDm8C88DkB3cJAGwBNcBuMKORWVDAJmzwCMBDJS6mBZNdAGYWuWAFM43YLACeABzHwxAD3YBbOcUU4A3sHgH4ACzHFi4AFx5CJclUPwAZuEttO3AL5Vp8xQDV2YgBXCQAeABUAPhZwgG0AazEZcEd4cIBdEFkFeABBaGgxJFhQgHlWACslZXFIUmh4ACUxCCRSUP4MABo6GvANuyCDpd87moJKZAAnKQB5bAjIVO4cwAOykAbqd60AE4ZGIUCIkOJnmvbBiEjAYoOSSlfjamPN7LynYeMNWfWHDRhoOiHB50CfluDHcvjcODigROxsBppPMNpgTvNOAs05kie7fDaiderNCafLoAlTIagAoQkFAvyhyQxnaalBAxPWoyOGDAQajNAvjykixdayhWDqCootbbuh9OCYZAkoRXYYYOIn3vDNhqcbmbMCJflTYcTnp0ZWThht1wdDSvblXSXdRHACCsGuINAw4OnpqlxOWOPURROS3MZJPycmrxdPBABBgTkvvIl1SrGBiWhTDGY5E2lmryTdJzeRT9REziEkOrEMUAFXHIDX-MwJAIDZUXBOad202MOGTuGsBCokLIgvpHNVSxTJTW-I6Sz8RFonzrHSQUonTOdmJBMTNRCljDU/P0ovMPKiBLsu-A63hMQ41rl44SPHzJuCZiOVadi-example-link
You may have observed that I replaced interface instance
with type instance
. This change was intentional because I added a constraint to the Validate
type. The first argument must extend Record
. Unlike interfaces, Records are indexed types in TypeScript by default.
For more information, refer to these discussions here and here.