2

Trying to get the following TypeScript type expression correct to have a local Trying to write a type smart function called getValue(string, defValue?) that returns either a string or the default value if the key isn't found. The function should have the type of string | typeof defaultValue the lookupValue() function has the correct typing to support this.

At this point have tried 4 different variations of the approach, three of which fail in compilation or usage, the last case doesn't fully handle the type inputs but does compile.

//  This function is good -- correctly handling the types
lookupValue<D>(record: string[], key: string, defvalue: D): D | string {
  const index = this.columnHeaders[key];
  const v = index !== undefined ? record[index] : undefined;

  return v === undefined || v === "" ? defvalue : v.trim();
}

someFunction(record: string[]) {

    // -- Test 1
    const getValue = <T>(key: keyof typeof COLUMNS, defvalue = undefined) => lookupValue(record, key, defvalue);

    //   Argument of type '""' is not assignable to parameter of type 'undefined'.
    const bigDef = getvalue("testBig", "something");

    // -- Test 2

    // Type 'undefined' is not assignable to type 'T'.
    const getValue = <T>(key: keyof typeof COLUMNS, defvalue: T = undefined) => lookupValue(record, key, defvalue);

    // -- Test 3

    // Won't compile since the defvalue is "T | undefined" which isn't valid
    const getValue = <T>(key: keyof typeof COLUMNS, defvalue?: T) => lookupValue(record, key, defvalue);

    // -- Test 4

    // Compiles but is wrong since the following getValue "works"
    const getValue = <T = undefined>(key: keyof typeof COLUMNS, defvalue?: T) => lookupValue(record, key, defvalue as T);

    //  Works - but shouldn't
    const foo: string = getValue("test");

}

The objective is to have something that fills this requirement:

    const big = getvalue("testBig");        // Should be type of string | undefined
    const bigDef = getvalue("testBig", "something");        // Should be type string
2
  • Who is COLUMNS ? Also how does T relate to COLUMNS ideally we would want there to be a relation between column type and T otherwise this is no better then any. Commented Feb 5, 2019 at 16:37
  • COLUMNS is an object with additional data. e.g. { name: xxx, address: yyy } so key is "name" | "address" -- not really important for the example Commented Feb 5, 2019 at 18:59

2 Answers 2

3

You could implement dynamically optional argument something like this:

function doSomething<T extends keyof TypeDataSchema>(
  type: T,
  ...[data]: TypeDataSchema[T] extends never ? [] : [TypeDataSchema[T]]
) {

}

interface TypeDataSchema {
  'hello': number
  'bye': never
}

doSomething('bye') // ok
doSomething('bye', 25) // expected 1 argument but got 2
doSomething('hello', 5) // ok
doSomething('hello') // expected 2 argument but got 1
Sign up to request clarification or add additional context in comments.

Comments

2

Typescript has limitations when it comes to optional arguments and generics at the moment. If you add an optional parameter mixed with a generic the compiler always assumes <generic> | undefined. eg:

function doSomething<A>(param1?: A) {
  return param1;
}

In this case param1 is "A | undefined". This wrecks any sort of flow through because the generic aspect doesn't include the undefined aspect instead it's tacked on - even in cases where you call doSomething() with no argument. So the result of doSomething("jello") is string | undefined; which is silly because one would think it would be string. This is also the case for doSomething() which has the return type {} | undefined instead of just undefined.

You can sometimes work around this by hitting Typescript really hard with large metaphorical hammer. Usually the hammer consists of working out the correct type definitions and then casting everything that complains. There's no one size fits all but in your case you can do:

  const getValue = <R = string, A = undefined>(key: string, defvalue: A = (undefined as unknown) as A): R | A =>
    (lookupValue(record, key, defvalue) as unknown) as R | A;

First you set R and A to string and undefined by default. This is so that the compiler assumes that R/A is string/undefined if it has no other info to go on. Then you have to set defvalue: A to stop the compiler from adding a | undefined. Without the | undefined the compiler can do type algebra. You then have to specify that the result of the function is R | A because that's basically what you want. The next step is to tell the compiler to stop inferring what "A" is based on the result of the call to lookupValue because it will infer incorrectly. This is also why we need to use "R" insteads of just string | A. Essentially if you don't case the result of lookupValue (or use string | A as the result type) the compiler is smart enough to see that there's not enough type information so "A" is either "undefined" or "string" depending on what you plunk the result into and either not compile (if you omit the cast) or fail like below if getValue's return type is set to string | A:

const result: string = getValue("testBig");

"A" will infer to string which is wrong because it should be a compile error" "can't assign string | undefined to string". The other case:

const result: string = getValue("testBig");

A will infer to undefined which means const result will be of type string | undefined which is also wrong.

In order to avoid the above we add as unknown) as R | A on the second line to get:

 const getValue = <R = string, A = undefined>(key: string, defvalue: A = (undefined as unknown) as A): R | A =>
    (lookupValue(record, key, defvalue) as unknown) as R | A;

The works correctly for all case I can think of

// ss is number | string
      const ss = getValue("testBig", 1);

// bigDef is string
      const bigDef = getValue("testBig", "something");

// sdf is string | undefined
      const sdf = getValue("testBig", undefined);
      const sdf = getValue("testBig");

// Compile error -> string | undefined can't be assigned to string
      const asdfa: string = getValue("testBig");

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.