0

I have a function isBoolean which checks whether a run time variable is boolean and returns the narrow type of true or false as a type (rather than just boolean).

export type IsBoolean<T> = T extends boolean ? true : false;

export function isBoolean<T extends unknown>(i: T) {
  return (typeof i === "boolean") as IsBoolean<T>;
}

The aim now is to put the function into a structure like this (along with other types):

const types = () => {
  boolean: { 
    name: "boolean",
    type: true as boolean,
    is: isBoolean,
    typeGuard: (v: unknown) => v is boolean => isBoolean(v)
}

type TypesApi = ReturnType<typeof types>;

At this stage there is no problem running the isBoolean function like so:

const yup = types().boolean.is(true);

But to get to the next stage I'm going to have to move away from my inferred types and start typing things more explicitly. What I'm wanting to be able to do is:

const myType = type(t => t.boolean);

To do this I have created a new type called Type:

type Type<T extends any> = {
  name: string;
  type: T;
  is: <V extends unknown>(v: V) => true | false,
  typeGuard: (v: unknown) =>  v is T;
}

and then the type function looks like this:

type TypeDefinition<T extends any> = (defn: TypeApi) => T;

function type<T extends any>(fn: TypeDefinition<T>) {
  return fn(typeApi());
}

This is then aided by explicit typing in the dictionary:

const types = () => ({
  boolean: {
    name: "boolean",
    type: true as boolean,
    is: isBoolean,
    typeGuard: (v: unknown): v is boolean => isBoolean(v),
  } as Type<boolean>
});

Sadly now when we try to use the API style the is function which is an alias for isBoolean no longer works:

const myType = type(t => t.boolean);
// type is now "boolean" instead of "true"
const yup3 = myType.is(true);

What can I do to get back this narrow typing which had before?

Typescript Playground

ken
  • 8,763
  • 11
  • 72
  • 133

3 Answers3

0

You need to type is more specifically, like so:

type Type<T extends any> = {
  name: string;
  type: T;
  is: <V extends unknown>(v: V) => V extends T ? true : false,
  typeGuard: (v: unknown) =>  v is T;
}

After changing that in the Playground, yup3 is now of type true.

Vojtěch Strnad
  • 2,420
  • 2
  • 10
  • 15
  • what you're showing here is what I have in the playground and it does NOT work. – ken Aug 22 '21 at 16:00
  • @ken The definiton for `is` in your playground is `(v: V) => true | false`, not what I wrote. I added a link to the edited playground, it really does work. – Vojtěch Strnad Aug 22 '21 at 18:46
  • Ok I missed the subtly of what you changed; unfortunately this is not going to work for some edge cases (specifically for cases where we have narrow types like `true` and `false`. In this case the type logic needs to resolve to three possible states true, false, and unknown. The goal really is to find a way to preserve the return type which the function uses. Unfortunately to fully type this it requires higher order type generics and that's not yet supported. – ken Aug 22 '21 at 21:27
  • I have brought up this topic here: https://stackoverflow.com/questions/68878279/is-there-a-way-to-type-a-type-utility-in-typescript – ken Aug 22 '21 at 21:33
0

there you have it

all you really need to do is at yow is should be typeof isBoolean. also you do not need all them extends any/ extends unknown

import { Expect, Equal } from "@type-challenges/utils";

export type IsBoolean<T> = T extends boolean ? true : false;

export function isBoolean<T>(i: T) {
  return Boolean(i) as IsBoolean<T>;
}

// Simple tests
const yup = isBoolean(true);
const thatToo = isBoolean(false);
const nope = isBoolean("foobar");

// Putting it in a dictionary

const types = () => ({
  boolean: {
    name: "boolean",
    type: true as true,
    is: isBoolean,
    typeGuard: (v: unknown) => isBoolean(v),
  } as Type<boolean>
});

type TypeApi = ReturnType<typeof types>;
type Type<T> = {
  name: string;
  type: T;
  is: typeof isBoolean,
  typeGuard: (v: T) =>  v is T;
}

// still working
const yup2 = types().boolean.is(true);

// wrapping in an API style
function type<T>(fn: ((t: TypeApi) => T)) {
  return fn(types());
}

// using API
const myType = type(t => t.boolean);
const yup3 = myType.is(true);
const trollmon = myType.is("fa");

enter image description here

enter image description here

  • so the issue with this is that using `typeof isBoolean` works but is static and therefore will _only_ work for boolean, as soon as you add a second type to the API the generalized type **Type** breaks. – ken Aug 22 '21 at 15:59
  • I mean one step at a time. Don’t forget about pure functions and separation of concerns. Less code is not always better. Having them god classes that handles everything is something you should avoid –  Aug 22 '21 at 18:40
  • Do not assume that this is what this is. I simply want strong types which can be understood at runtime and within the type system. This can be very powerful and is very little code. But there's a trick to it that is not solved by "hard coding it". :) – ken Aug 22 '21 at 21:56
  • What ? you know typescript never runs right, all this over work instead of simplifying things is making it more complex to digest as it is getting more and more abstract, and not only that at run time is useless or careless as the one tuning is JavaScript so if you want to make your code really strictly typed, don’t use typescript use JavaScript –  Aug 23 '21 at 04:07
  • To each his own, strong typing eliminates a whole category of bugs and in my opinion is very worth having. I will never use JS again. – ken Aug 23 '21 at 18:52
0

I have solved this solution by giving Type two generic parameters:

export type Type<T extends any, V extends Function> = {
  name: string;
  type: T;
  typeGuard: TypeGuard<T>;
  is: V;
};

And with this definition I define the API surface like so:

export const typeApi = () =>
  ({
    string: {
      name: "string",
      type: "" as string,
      typeGuard: (v: unknown): v is string => isString(v),
      is: isString,
    } as Type<string, typeof isString>,
    boolean: {
      name: "boolean",
      type: true as boolean,
      typeGuard: (v: unknown): v is boolean => isBoolean(v),
      is: isBoolean,
    } as Type<boolean, typeof isBoolean>,

    // ...
} as const);

Then it's only a matter of wrapping this API with the type() function:

type TypeDefinition<T extends any, V extends Function> = 
  (defn: TypeApi) => Type<T, V>;

function type<T extends any, V extends Function>(fn: TypeDefinition<T, V>) {
  const result = fn(typeApi());
  return result;
}

Now the is function is explicitly set for each Type<T,V> and the type utility for a given type can be as complex or simple as it needs to be. So -- regardless of the type chosen -- these types of API calls will result as a type that is consistent between run-time and type system:

const b1 = type(t => t.boolean).is(true); // true
const b2 = type(t => t.boolean).is(false); // true
const b3 = type(t => t.boolean).is("nada"); // false

const t1 = type(t => t.true).is(true); // true
const t2 = type(t => t.true).is(false); // false
const t3 = type(t => t.true).is("nada"); // false
const t4 = type(t => t.true).is(true as boolean); // boolean

Playground

ken
  • 8,763
  • 11
  • 72
  • 133